在C++中,特殊成员函数(special member functions)是指由编译器自动生成(或隐式声明)的类成员函数,包括默认构造函数、析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符。这些函数在特定情况下会由编译器自动提供,但其生成规则受到类定义和用户提供的声明的影响。以下是C++中特殊成员函数的生成规则的详细说明,基于C++11及以后的标准。

-w700

1. 特殊成员函数的种类

C++中的特殊成员函数包括以下六种:

  1. 默认构造函数 (T::T();)
    • 无参数的构造函数,用于创建对象。
  2. 析构函数 (T::~T();)
    • 用于清理对象资源。
  3. 拷贝构造函数 (T::T(const T&);)
    • 用于通过复制已有对象来构造新对象。
  4. 拷贝赋值运算符 (T& operator=(const T&);)
    • 用于将一个对象的内容复制到另一个已有对象。
  5. 移动构造函数 (C++11 引入, T::T(T&&);)
    • 用于通过移动已有对象的资源来构造新对象。
  6. 移动赋值运算符 (C++11 引入, T& operator=(T&&);)
    • 用于将一个对象的资源移动到另一个已有对象。

这些函数的生成规则受到类中用户定义的成员函数、成员变量以及其他因素的影响。

2. 生成规则详解

2.1 默认构造函数

  • 生成条件
    • 如果类中没有定义任何构造函数,编译器会生成一个默认构造函数。
    • 默认构造函数的行为是:
      • 对类的数据成员执行默认初始化(对于内置类型不初始化,类类型调用其默认构造函数)。
    • 如果用户定义了任何构造函数(包括拷贝构造函数、移动构造函数等),编译器不会生成默认构造函数
  • 抑制生成的情况
    • 用户显式定义了任何构造函数。
    • 如果类中有非静态成员变量或基类无法默认构造(例如,成员变量是引用类型或const成员,且未在初始化列表中初始化)。
  • 显式声明为default
    • 用户可以用= default显式要求编译器生成默认构造函数,例如:
      class MyClass {
      public:
          MyClass() = default; // 显式要求生成默认构造函数
      };
  • 显式声明为delete
    • 可以用= delete禁止编译器生成默认构造函数:
      class MyClass {
      public:
          MyClass() = delete; // 禁止默认构造
      };

2.2 析构函数

  • 生成条件
    • 如果用户没有定义析构函数,编译器会自动生成一个默认析构函数。
    • 默认析构函数的行为是:
      • 按逆序调用类成员和基类的析构函数。
      • 对于内置类型成员,不执行任何操作。
    • 默认生成的析构函数是noexcept(true)(C++11起)。
  • 抑制生成的情况
    • 用户显式定义了析构函数(即使是= default)。
  • 虚析构函数
    • 如果类是基类,建议用户显式定义虚析构函数,否则多态删除可能导致未定义行为。编译器不会自动生成虚析构函数。
  • 显式声明为defaultdelete
    • 类似默认构造函数,可以用= default= delete控制析构函数的生成。

2.3 拷贝构造函数

  • 生成条件
    • 如果用户没有定义拷贝构造函数、移动构造函数或移动赋值运算符,编译器会生成一个默认的拷贝构造函数。
    • 默认拷贝构造函数的行为是:
      • 对类的数据成员和基类执行逐成员拷贝(调用成员的拷贝构造函数或内置类型的逐位拷贝)。
  • 抑制生成的情况(C++11及以后):
    • 用户定义了移动构造函数或移动赋值运算符。
    • 用户显式声明拷贝构造函数为= delete
    • 类中有不可拷贝的成员(例如,引用类型、const成员、或成员类型没有可访问的拷贝构造函数)。
  • 显式声明为defaultdelete
    • 可以用= default要求生成默认拷贝构造函数,或用= delete禁止生成。

2.4 拷贝赋值运算符

  • 生成条件
    • 如果用户没有定义拷贝赋值运算符、移动构造函数或移动赋值运算符,编译器会生成一个默认的拷贝赋值运算符。
    • 默认拷贝赋值运算符的行为是:
      • 对类的数据成员和基类执行逐成员赋值(调用成员的赋值运算符或内置类型的赋值)。
  • 抑制生成的情况
    • 用户定义了移动构造函数或移动赋值运算符。
    • 类中有不可赋值的成员(例如,引用类型、const成员、或成员类型没有可访问的赋值运算符)。
    • 用户显式声明拷贝赋值运算符为= delete
  • 显式声明为defaultdelete
    • 类似其他特殊成员函数,可以用= default= delete控制。

2.5 移动构造函数(C++11及以后)

  • 生成条件
    • 如果用户没有定义拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符或析构函数,编译器会生成一个默认的移动构造函数。
    • 默认移动构造函数的行为是:
      • 对类的数据成员和基类执行逐成员移动(调用成员的移动构造函数或内置类型的逐位拷贝)。
  • 抑制生成的情况
    • 用户定义了拷贝构造函数、拷贝赋值运算符、移动赋值运算符或析构函数。
    • 类中有不可移动的成员(例如,引用类型、const成员、或成员类型没有可访问的移动构造函数)。
    • 用户显式声明移动构造函数为= delete
  • 显式声明为defaultdelete
    • 可以用= default要求生成默认移动构造函数,或用= delete禁止生成。

2.6 移动赋值运算符(C++11及以后)

  • 生成条件
    • 如果用户没有定义拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符或析构函数,编译器会生成一个默认的移动赋值运算符。
    • 默认移动赋值运算符的行为是:
      • 对类的数据成员和基类执行逐成员移动赋值(调用成员的移动赋值运算符或内置类型的赋值)。
  • 抑制生成的情况
    • 用户定义了拷贝构造函数、拷贝赋值运算符、移动构造函数或析构函数。
    • 类中有不可移动赋值的成员(例如,引用类型、const成员、或成员类型没有可访问的移动赋值运算符)。
    • 用户显式声明移动赋值运算符为= delete
  • 显式声明为defaultdelete
    • 类似其他特殊成员函数,可以用= default= delete控制。

3. C++11及以后的变化

C++11引入了移动语义(move semantics)和= default/= delete语法,显著影响了特殊成员函数的生成规则:

  • 移动语义
    • 移动构造函数和移动赋值运算符的引入使得编译器在生成规则上更加复杂。特别是,定义了移动相关函数会抑制拷贝构造函数和拷贝赋值运算符的生成,反之亦然。
  • = default= delete
    • = default允许用户显式要求编译器生成默认实现,保持与自动生成相同的语义,但可以提高代码可读性。
    • = delete允许用户显式禁止某些特殊成员函数的生成,防止不希望的隐式操作。
  • noexcept
    • 默认生成的移动构造函数和移动赋值运算符通常是noexcept的(如果所有成员和基类的移动操作都是noexcept)。
    • 默认构造函数、拷贝构造函数和拷贝赋值运算符是否为noexcept取决于成员和基类的构造/赋值操作。

4. 特殊成员函数生成规则的总结表

以下是C++11及以后特殊成员函数生成的简要总结:

特殊成员函数
默认生成条件抑制生成的情况
默认构造无用户定义的构造函数用户定义了任何构造函数,或有不可默认构造的成员
析构总是生成(除非用户定义)用户定义了析构函数
拷贝构造无用户定义的拷贝构造、移动构造、移动赋值用户定义了移动构造、移动赋值,或有不可拷贝的成员
拷贝赋值无用户定义的拷贝赋值、移动构造、移动赋值用户定义了移动构造、移动赋值,或有不可赋值的成员
移动构造无用户定义的拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数用户定义了拷贝构造、拷贝赋值、移动赋值、析构函数,或有不可移动的成员
移动赋值无用户定义的拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数用户定义了拷贝构造、拷贝赋值、移动构造、析构函数,或有不可移动赋值的成员

5. 注意事项和最佳实践

  • Rule of Three/Five/Zero
    • Rule of Three(C++98/03):如果类需要自定义析构函数、拷贝构造函数或拷贝赋值运算符,通常需要定义这三者。
    • Rule of Five(C++11及以后):加上移动构造函数和移动赋值运算符,通常需要定义全部五个特殊成员函数。
    • Rule of Zero:尽量让类不定义任何特殊成员函数,依赖编译器生成或使用标准库(如std::unique_ptr)管理资源。
  • 虚析构函数
    • 如果类可能作为基类,显式定义虚析构函数以避免未定义行为。
  • = default的使用
    • 使用= default可以清晰表达意图,同时保留编译器生成的默认行为,适合简单的类。
  • = delete的使用
    • 使用= delete可以禁止不需要的操作,例如防止拷贝单例类。
  • 成员变量的影响
    • 引用类型、const成员或不可拷贝/移动的成员会影响特殊成员函数的生成。
  • 异常规范
    • 注意默认生成的移动操作是否为noexcept,这对标准库容器(如std::vector)的性能有影响。

6. 示例代码

以下是一个示例,展示特殊成员函数的生成规则:

#include <iostream>

class MyClass {
public:
    // 默认构造函数(显式声明为 default)
    MyClass() = default;

    // 拷贝构造函数(显式声明为 default)
    MyClass(const MyClass&) = default;

    // 移动构造函数(显式声明为 default)
    MyClass(MyClass&&) = default;

    // 拷贝赋值运算符(显式声明为 default)
    MyClass& operator=(const MyClass&) = default;

    // 移动赋值运算符(显式声明为 default)
    MyClass& operator=(MyClass&&) = default;

    // 析构函数(显式声明为 default)
    ~MyClass() = default;

    void print() const { std::cout << "MyClass object\n"; }
};

class NoCopyClass {
public:
    NoCopyClass() = default;
    NoCopyClass(const NoCopyClass&) = delete; // 禁止拷贝构造
    NoCopyClass& operator=(const NoCopyClass&) = delete; // 禁止拷贝赋值
};

int main() {
    MyClass a;
    MyClass b = a; // 调用拷贝构造函数
    MyClass c = std::move(a); // 调用移动构造函数
    b = c; // 调用拷贝赋值
    a = std::move(c); // 调用移动赋值
    a.print();

    // NoCopyClass x;
    // NoCopyClass y = x; // 错误:拷贝构造函数被删除
    return 0;
}

7. 常见问题

  • 为什么移动构造函数不生成?
    • 如果定义了拷贝构造函数或析构函数,编译器不会生成移动构造函数,因为它假设用户可能需要自定义资源管理。
  • 为什么拷贝构造函数被抑制?
    • 定义了移动构造函数或移动赋值运算符会导致拷贝操作被抑制,鼓励使用移动语义。
  • 如何确保移动操作高效?
    • 确保移动操作是noexcept,否则标准库容器可能退回到拷贝操作。