Skip to content

特殊成员函数

概述

特殊成员函数也叫静态成员函数(SMFs, Static Member Functions),指一些具有特殊语义和默认行为的成员函数,通常用于对象的生命周期管理、拷贝控制和移动操作等。特殊成员函数只会在它们被调用和用户显式定义时生成。

特殊成员函数共有六种:

  • 默认构造函数(Default constructor)

    • 创建的对象未指定任何参数,也未实例化任何成员变量
  • 析构函数(Destructor)

    • 在对象离开作用域后将其销毁
  • 拷贝构造函数(Copy constructor)

    • 基于现有对象进行逐成员拷贝来创建新的拷贝副本
  • 赋值运算符(Copy assignment operator)

    • 将现有对象替换为另一个现有对象的运算符
  • 移动构造函数(Move constructor)

    • 基于现有对象进行逐成员移动来创建新的拷贝副本
  • 移动赋值运算符(Move assignment operator)

    • 将现有对象移动至另一个现有对象的运算符
class Widget {
  public:
    /* Takes no parameters and creates a new object. */
    Widget();                             // default constructor

    /* Creates a new object as member-wise copy of another. */
    Widget(const Widget& w);              // Copy constructor

    /*Assigns an already exisiting object to another. */
    Widget& operator = (const Widget& w); // Copy assignment operator

    /* Called and delete the object when it goes out of scope. */
    ~Widget();                            // Destructor

    Widget(Widget&& rhs);                 // Move constructor
    Widget& operator = (Widget&& rhs);    // Move assignment operator
};

最后两个特殊成员函数是我们前面所不曾见过的,我们将在这个章节学习它们。

拷贝与赋值(Copy & Copy assignment)

默认情况下(未重载),拷贝构造函数在被调用时会对对象的每个成员变量进行拷贝,即逐成员拷贝(member-wise copy)。但这样足够了吗?

如果一个成员变量是指针变量,那么逐成员拷贝就会使新对象的这个成员变量指向相同的已分配的数据,而不是生成一个新的拷贝。

template<typename Type>
vector<Type>::vector<Type>(const vector::vector<Type>& other) : 
  _size(other.size),
  _capacity(other.capacity),
  _elems(other.elems) { }

在上面的拷贝构造中,针对_elems的拷贝就仅仅只是简单地复制了指针或引用。如果成员变量elems是一个指针,那么拷贝操作后的新对象和传入这个拷贝构造函数的other对象在底层上将共享同一块内存空间。具体表现就是新对象的成员变量elems在底层上与旧对象otherelems指向同一个数组。

这就是我们通常所说的浅拷贝(shallow copy),与之对应的就是深拷贝(deep copy)

深拷贝会创建一个与原始对象完全相同且与之相互独立的全新副本。与类的普通成员函数类似,深拷贝的拷贝构造函数需要用户在源文件.cpp中自行实现,以覆盖编译器默认的浅拷贝构造。

更多有关浅/深拷贝的内容,可参考Shallow Copy and Deep Copy in C++ | GeeksForGeeks

default & delete

用法与作用

在定义类时,可通过显式定义为特殊成员函数设置为默认行为或移除这个成员函数的功能。

  • default用于在用户自定义了构造函数后保留编译器默认生成的构造函数

  • delete用于声明禁用该特殊成员函数

class PasswordManager {
  public:
    /* Keep the default copy constructor (if we declare other constructors). 
    /* Declaring any user-defined constructor will make the default disappear without this! */
    PasswordManager() = default;    //     
    PasswordManager(const PasswordManager& pm) = default;
    ~PasswordManager();
    // other method ...

    PasswordManager(const PasswordManager& rhs) = delete;
    PasswordManager& operator = (const PasswordManager& rhs) = delete;
    /* Now copying isn't a possible operation. */

  private:
    // some important member vars.
};

部分应用场景

资源类显式禁用拷贝语义

当一个类内部持有“唯一所有权”资源时(如设备句柄、文件句柄、线程回调状态、互斥量等),通常应显式禁用拷贝构造和拷贝赋值,避免对象被意外复制。

例如,下面这个类是一个音频引擎,它内部持有设备句柄,因此需要显式禁用拷贝构造和拷贝赋值。

class AudioEngine {
  public:
    AudioEngine(const AudioEngine&) = delete;
    AudioEngine& operator=(const AudioEngine&) = delete;
};

这样做的意义在于:

同步与互斥 - 操作系统

  • 防止“浅拷贝”后两个对象共享同一底层资源

  • 避免析构阶段重复释放(double free / double close)

  • 避免多个实例同时操作同一回调状态导致数据竞争

  • 让误用在编译期直接报错,而不是在运行期暴露问题

简而言之,= delete不是“少一个功能”,而是对类语义的约束——该类型只允许唯一控制其资源,不允许按值复制。

与“实现深拷贝”的对比

很多初学者都会一个疑问:既然浅拷贝有风险,为什么不总是实现深拷贝?

关键在于类的语义目标不同:

  • 若类型语义是“唯一所有权”(设备、文件、socket、互斥量、线程句柄等),应优先= delete禁拷贝

  • 若类型语义是“值对象可复制”(如自定义容器、图像缓冲、矩阵等),应实现深拷贝并保证副本相互独立

可用一个判断标准:

  • “复制后,两个对象同时存在且各自析构”是否天然合理?

    • 不合理:禁拷贝(= delete

    • 合理:实现拷贝构造 + 拷贝赋值(深拷贝)

因此,= delete与深拷贝不是互斥技巧,而是两种不同的设计选择:前者强调禁止复制以保护资源所有权,后者强调允许复制并保持值语义

约定俗成的规则

默认的特殊成员函数在很多情况下都能正常工作,特别是当类没有管理任何资源时。如果默认的SFM功能能够满足需求,就不需要自定义新功能!只有编译器生成的默认功能无法满足需求,我们才应定义新的。

通常情况下,我们在使用动态分配的内存时(如指向堆中数据的指针),默认的功能就无法满足需求了。

当一个类管理了某些资源(如动态分配的内存、文件句柄、网络连接等)时,大多数情况下就必须定义析构函数拷贝构造函数拷贝赋值运算符。这三个成员函数合在一起,通常被称为“三大法则”(The Rule of 3)。

这三个特殊成员函数有一个需要进行自定义就意味着你正在手动管理某些资源;接下来就需要我们自己来处理这些资源的创建分配使用以及销毁事宜。这就意味着,这三个SMF但凡有一个需要进行自定义,那剩下的两个也逃不了。

移动和移动赋值(std::move)

概述

假设我们想要将一个字符串表复制到一个新的字符串表只中,且后续我们不再需要原来的字符串表。

class StringTable {
  public:
    StringTable() {}
    StringTable(const StringTable& st) {}
    // other member functions, like insertion, look up, etc.
    // no move/dtor functionality

  private:
    std::map<int, std::string> values;
};

根据前面所学的知识,我们可能会选择使用拷贝构造函数创建一个新的拷贝副本,然后将原先的对象删除。

但这样高效吗?

拷贝构造函数会从成员变量values中逐个拷贝std::map中的值,这个过程是低效的。

这个时候就应该学习使用SMFs中的最后两个:移动构造函数移动赋值函数了。

移动构造函数和移动赋值运算符是C++11引入的两种特殊成员函数,它们允许将资源从一个对象“移动”到另一个对象,而不是复制。移动语义可以提高程序的性能,特别是在处理大型对象或涉及动态分配资源的对象时。

与拷贝类似的是,移动也是逐成员的。

注意移动操作的参数是一个右值引用

#include <iostream>
#include <utility> // for std::swap

class MyClass {
  public:
    // Default constructor
    MyClass() : data(new int[100]) {
        std::cout << "Default constructor\n";
    }

    // Copy constructor
    MyClass(const MyClass& other) : data(new int[100]) {
        std::cout << "Copy constructor\n";
        std::copy(other.data, other.data + 100, data);
    }

    // Copy assignment operator
    MyClass& operator=(const MyClass& other) {
        std::cout << "Copy assignment operator\n";
        if (this == &other) return *this;
        MyClass temp(other); // Create a temporary object using the copy constructor
        std::swap(data, temp.data); // Swap resources
        return *this;
    }

    // Move constructor
    MyClass(MyClass&& other) noexcept : data(other.data) {
        std::cout << "Move constructor\n";
        other.data = nullptr; // Ensure the old object no longer owns the resource
    }

    // Move assignment operator
    MyClass& operator=(MyClass&& other) noexcept {
        std::cout << "Move assignment operator\n";
        if (this == &other) return *this;
        MyClass temp(std::move(other)); // Create a temporary object using the move constructor
        std::swap(data, temp.data); // Swap resources
        return *this;
    }

    // Destructor
    ~MyClass() {
        std::cout << "Destructor\n";
        delete[] data;
    }

    // Other member functions...
  private:
    int* data;
};

int main() {
    MyClass obj1; // Calls the default constructor
    MyClass obj2 = obj1; // Calls the copy constructor
    MyClass obj3;
    obj3 = obj1; // Calls the copy assignment operator

    MyClass obj4 = MyClass(); // Calls the move constructor
    MyClass obj5;
    obj5 = MyClass(); // Calls the move assignment operator

    return 0;
}

注意事项

只有在以下情况编译器才会自动生成移动构造函数和操作符:

  • 未声明任何复制操作

  • 未声明任何移动操作

  • 未声明析构函数

用户声明任何一项都意味着取消C++默认的生成操作。

当然也可通过显式声明支持移动操作:

Widget(Widget&& ) = default;
Widget& operator=(Widget&&) = default;    // support moving

Widget(Widget& ) = default;
Widget& operator=(Widget&) = default;     // support copying