条款17:理解特殊成员函数的生成
了解何时生成拷贝/移动/析构函数及其相互影响,合理用 =default。
条款17:理解特殊成员函数的生成
条款17:理解特殊成员函数的生成
什么是特殊成员函数?
特殊成员函数是指 编译器自动生成 的成员函数,主要包括:
- 默认构造函数(Default Constructor)
- 析构函数(Destructor)
- 拷贝构造函数(Copy Constructor)
- 拷贝赋值运算符(Copy Assignment Operator)
C++11 新增:
- 移动构造函数(Move Constructor)
- 移动赋值运算符(Move Assignment Operator)
只有当代码需要调用这些函数,但又没有显式声明时,编译器才会自动生成。
自动生成的规则和特征
- 自动生成的函数默认是 public 和 inline。
- 除析构函数外,它们通常是非虚函数。
- 只有在类未显式声明对应函数时才生成。
- 默认构造函数仅在类无任何用户声明构造函数时才生成。
移动构造函数和移动赋值运算符
函数签名示例:
1
2
3
4
5
class Widget {
public:
Widget(Widget&& rhs); // 移动构造函数
Widget& operator=(Widget&& rhs); // 移动赋值运算符
};
- 移动操作会对类的非静态成员和基类部分调用对应成员的移动操作。
- 如果成员类型不支持移动,退化为拷贝。
- 声明一个移动操作,会阻止编译器生成另一个移动操作。
拷贝操作的规则
- 拷贝构造和拷贝赋值是相互独立的。
- 声明一个不会阻止另一个自动生成。
移动和拷贝操作的交互规则
- 声明拷贝操作会阻止移动操作的自动生成。
- 声明移动操作会阻止拷贝操作的自动生成(编译器将其设置为 delete)。
Rule of Three(三法则)
- 如果声明了拷贝构造、拷贝赋值或析构函数之一,应声明另外两个。
- 目的是保证资源管理正确(内存、文件等)。
移动操作生成的条件
编译器只会在满足下列条件时生成移动构造和移动赋值:
- 未声明拷贝构造函数、拷贝赋值运算符
- 未声明移动构造函数、移动赋值运算符
- 未声明析构函数
析构函数与移动操作
声明析构函数会阻止移动操作生成,但不会阻止拷贝操作生成。
这可能导致性能问题,因为:
- 移动请求被转化为拷贝操作。
- 例如包含大量数据的容器类,拷贝比移动慢很多。
解决办法:显式声明并默认实现移动和拷贝操作。
示例详解
用户声明析构函数,阻止移动生成
1
2
3
4
5
6
7
8
9
10
11
12
class StringTable {
public:
StringTable() { /* 构造函数 */ }
~StringTable() {
// 自定义析构,阻止移动操作自动生成
makeLogEntry("Destroying StringTable object");
}
private:
std::map<int, std::string> values;
};
- 这里编译器不会生成移动构造和移动赋值。
- 移动请求会转为拷贝操作,性能可能严重下降。
解决方式:显式默认特殊成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class StringTable {
public:
StringTable() {}
~StringTable() {
makeLogEntry("Destroying StringTable object");
}
StringTable(StringTable&&) = default; // 显式允许移动构造
StringTable& operator=(StringTable&&) = default; // 显式允许移动赋值
StringTable(const StringTable&) = default; // 显式允许拷贝构造
StringTable& operator=(const StringTable&) = default; // 显式允许拷贝赋值
private:
std::map<int, std::string> values;
};
- 保留了自定义析构功能。
- 同时允许移动和拷贝操作自动生成,性能恢复。
多态基类示例
1
2
3
4
5
6
7
8
9
10
class Base {
public:
virtual ~Base() = default; // 虚析构函数,允许通过基类指针删除派生类对象
Base(Base&&) = default; // 支持移动构造
Base& operator=(Base&&) = default; // 支持移动赋值
Base(const Base&) = default; // 支持拷贝构造
Base& operator=(const Base&) = default; // 支持拷贝赋值
};
移动与拷贝声明示例
1
2
3
4
5
6
7
8
class Widget {
public:
Widget(const Widget&) = delete; // 禁止拷贝构造
Widget& operator=(const Widget&) = delete; // 禁止拷贝赋值
Widget(Widget&&) = default; // 默认移动构造
Widget& operator=(Widget&&) = default; // 默认移动赋值
};
复杂情况:成员模板不会阻止特殊成员生成
1
2
3
4
5
6
7
8
class Widget {
public:
template<typename T>
Widget(const T& rhs); // 模板构造函数
template<typename T>
Widget& operator=(const T& rhs); // 模板赋值运算符
};
- 这些模板版本的构造函数和赋值运算符,不会阻止编译器生成默认的 拷贝构造函数、拷贝赋值运算符、移动构造函数 和 移动赋值运算符,只要其他自动生成的条件都满足。
- 因为模板函数只有在被实例化(即被真正用到)时,才算真正“存在”。
- 这条规则能防止因为模板存在而意外屏蔽掉重要的默认行为(比如你没写拷贝构造,但因为写了模板而被“误当成已有构造函数”)。这在 C++11 之后是一个明确设计。
总结
- 特殊成员函数是编译器自动生成的六个成员函数。
- 移动操作生成规则更严格,声明拷贝或析构函数会阻止生成。
- 拷贝构造和拷贝赋值是相互独立的。
- 用户声明析构函数会阻止移动操作生成,可能导致性能下降。
- 显式用
= default
声明特殊成员函数是良好习惯,表达意图,避免隐式错误。 - 模板成员函数不会阻止特殊成员函数生成。
本文由作者按照 CC BY 4.0 进行授权