网站Logo 踏雪无痕的博客

c++中class的特殊成员函数

liubang
6
2025-12-14

c++中class的特殊成员函数

一、c++中的 three/five/zero 规则

three/five/zero 规则针对的都是class的特殊成员函数(析构函数拷贝构造拷贝赋值移动构造移动赋值)。

一旦手动定义了某些特殊成员函数,就要把同一组里该定义/该禁用的也一起处理好,否则很容易出现浅拷贝、双重释放、性能退化或 move 被抑制等问题。

rule_of_three

c++11以前,还没有引入移动语义。如果一个类需要你自己写:析构函数 / 拷贝构造 / 拷贝赋值 三者之一,那你要把另外两个也写上,否则将会使用编译器自动生成的,这大概率会造成错误。

rule_of_five

c++11以后,移动语义被引入,由于用户自定义的(包括使用= default= delete声明的)析构函数拷贝构造函数拷贝赋值运算符会阻止移动构造函数移动赋值运算符隐式定义,因此任何希望实现移动语义的类都必须显式声明所有这五个特殊成员函数

rule_of_zero

所有权/资源管理类尽量不要自定义析构、拷/移构造、拷/移赋值——把资源交给标准库类型(RAII)成员去管理,你的类就能0 特殊成员函数也自动正确。
典型写法:用 std::string / std::vector / std::unique_ptr 等成员承载资源,编译器生成的默认拷贝/移动/析构就是对成员逐个处理,语义正确、也更不容易写错。

一句话总结

一旦开始自定义这些特殊成员函数,就要意识到 3/5/0 规则在提醒你:别只写一半,否则编译器生成的另一半默认操作很可能在资源语义上是错的,或者把 move 给抑制掉。


二、拷贝构造函数原型

拷贝构造函数原型
拷贝构造函数的参数列表要满足:

  • 第一个参数必须是T& / const T& / volatile T& / const volatile T&(不能是T)
  • 其余参数(如果有)都必须有默认值

如果写 T(T other) 这种按值传参的拷贝构造函数,这是不合法的。因为如果拷贝构造按值传参,那么为了把实参拷贝进形参 other,又得先调用拷贝构造,逻辑上会陷入拷贝构造需要拷贝构造的递归泥潭。标准从语法层面把它排除掉。

struct X
{
    X(const X& other); // 最常见的写法
//  X(X other);  // Error: incorrect parameter type
};
 
union Y
{
    Y(const Y& other, int num = 1); // copy constructor with multiple parameters
//  Y(const Y& other, int num);     // Error: `num` has no default argument
};

每当一个对象从同一类型的另一个对象初始化(通过直接初始化或拷贝初始化)时,会调用拷贝构造函数(除非重载解析选择了更优的匹配或者调用被省略),这包括:

  • 初始化:T a = b;T a(b);,其中b的类型为T
  • 函数参数传递:f(a);,其中a的类型为T,且f是void f(T t)
  • 函数返回:在函数如T f()内部return a;,其中a的类型为T,且该类型没有移动构造函数

三、拷贝赋值运算符重载原型

拷贝赋值运算符重载原型

  1. 参数列表可以是 T / T& / const T& / volatile T& / const volatile T&(const T是非法的)
  2. 返回类型可以是任何类型,但更偏好 T&,因为可以支持链式赋值a=b=c(b=c 的结果T&还能继续赋给 a)
struct X
{
    X& operator=(const X& other);     // 最常见的写法
    X operator=(X other);       // pass-by-value is allowed
//  X operator=(const X other); // Error: incorrect parameter type
};
 
union Y
{
    // copy assignment operators can have syntaxes not listed above,
    // as long as they follow the general function declaration syntax
    // and do not viloate the restrictions listed above
    auto operator=(Y& other) -> Y&;       // OK: trailing return type
    Y& operator=(this Y& self, Y& other); // OK: explicit object parameter
//  Y& operator=(Y&, int num = 1);        // Error: has other non-object parameters
};

拷贝赋值运算符在重载解析被选中时调用,例如当已定义的对象出现在赋值表达式的左侧时:T a;a = b,其中b的类型为T

四、移动构造函数原型

移动构造函数原型

  • 把类类型记为T,移动构造的第一个参数必须是
    T&& / const T&& / volatile T&& / const volatile T&&,并且如果还有别的参数,都要有默认值。
struct X
{
    X(X&& other); // 最常见的写法
    X(const X&& other);// 合法但是没用,不能“偷走”一个 const 对象的资源。
//  X(X other);   // Error: incorrect parameter type
};
 
union Y
{
    Y(Y&& other, int num = 1); // move constructor with multiple parameters
//  Y(Y&& other, int num);     // Error: `num` has no default argument
};

移动构造函数通常在对象从相同类型的右值(xvalueprvalue)(直至C++17)或xvalue(自C++17起)初始化时被调用,包括:

  • 初始化T a = std::move(b); 或 T a(std::move(b));,其中b的类型为T
  • 函数参数传递f(std::move(a));,其中a的类型为T,且fvoid f(T t);
  • 函数返回:在如T f()的函数内部return a;,其中a的类型为T且拥有移动构造函数。

当初始化器为prvalue时,移动构造函数的调用常被优化掉(直至C++17)或从不进行(自C++17起),参见copy elision

移动构造函数通常转移参数持有的资源(例如指向动态分配对象的指针文件描述符TCP套接字线程句柄等),而非复制它们,并将参数置于某种有效但不确定的状态。
由于移动构造函数不改变参数的生命周期,析构函数通常会在稍后对参数调用。例如,从std::stringstd::vector移动可能导致参数变为空。对于某些类型,如std::unique_ptr,移动后的状态有明确规定。

五、移动赋值运算符重载原型

移动赋值运算符重载原型

  • 只能有一个参数,并且该参数类型(记为 T)必须是
    T&& / const T&& / volatile T&& / const volatile T&&
  • 返回类型:可以是任意类型,但更偏好 T&
struct X
{
    X& operator=(X&& other);    // 最常见的写法
//  X operator=(const X other); // Error: incorrect parameter type
};
 
union Y
{
    // move assignment operators can have syntaxes not listed above,
    // as long as they follow the general function declaration syntax
    // and do not viloate the restrictions listed above
    auto operator=(Y&& other) -> Y&;       // OK: trailing return type
    Y& operator=(this Y&& self, Y& other); // OK: explicit object parameter
//  Y& operator=(Y&&, int num = 1);        // Error: has other non-object parameters
};
动物装饰