文章目录
-
- [条款05:了解 C++ 默默编写并调用哪些函数](#条款05:了解 C++ 默默编写并调用哪些函数)
-
- 说明与介绍
- 代码对比示例
- 运行结果示例(可能因编译器不同略有差异)
- 关键要点解析
-
- [1. 空类并非真的"空"](#1. 空类并非真的“空”)
- [2. 编译器生成函数的默认行为](#2. 编译器生成函数的默认行为)
- [3. 编译器拒绝生成的情况](#3. 编译器拒绝生成的情况)
- [4. C++11 移动操作的自动生成](#4. C++11 移动操作的自动生成)
- 总结
条款05:了解 C++ 默默编写并调用哪些函数
说明与介绍
核心思想 :当你定义一个空类(没有声明任何成员函数)时,C++ 编译器会为你自动声明 (如果代码需要它们)一些成员函数。如果你没有声明,编译器会默默生成它们的默认版本。这些函数包括:
- 默认构造函数:如果没有声明任何构造函数,编译器生成一个无参的默认构造函数。
- 析构函数:编译器生成的析构函数是非虚的(除非基类有虚析构函数)。
- 拷贝构造函数:用于以同类型对象初始化另一个对象。
- 拷贝赋值运算符:用于将同类型对象赋值给另一个对象。
从 C++11 开始,编译器还可能自动生成:
-
移动构造函数 :用于以同类型右值对象初始化另一个对象。
-
移动赋值运算符:用于将同类型右值对象赋值给另一个对象。
编译器自动生成的函数行为:
- 它们都是 public 且 inline 的。
- 默认构造函数调用基类和成员对象的默认构造函数。
- 析构函数调用基类和成员对象的析构函数(注意是非虚的)。
- 拷贝构造函数和拷贝赋值运算符以成员逐一的方式拷贝非静态成员(对于数组成员,逐个拷贝元素)。
- 移动构造函数和移动赋值运算符以成员逐一的方式移动非静态成员。
什么情况下编译器会拒绝生成?
- 如果你声明了任何构造函数(包括拷贝构造、移动构造),编译器就不再生成默认构造函数。
- 如果你声明了移动操作或拷贝操作中的某些函数,编译器可能不再生成其他默认函数(具体规则复杂,详见 C++11 的"=default"和"=delete"机制)。
- 如果成员变量具有不可拷贝 的类型(如
const成员、引用成员、或者删除了拷贝操作的成员类型),编译器不会生成拷贝赋值运算符(有时也会影响拷贝构造)。 - 如果基类的相应操作是不可访问的(private)或被删除,编译器生成的版本也会被删除。
代码对比示例
cpp
// 文件名: clause05.cpp
// 编译: g++ -std=c++11 clause05.cpp -o clause05
#include <iostream>
using namespace std;
// ========== 辅助类,用于观察构造/析构/拷贝 ==========
class Tracked {
public:
string name;
Tracked(const string& n = "default") : name(n) {
cout << "Tracked 构造: " << name << endl;
}
Tracked(const Tracked& rhs) : name(rhs.name + "(copy)") {
cout << "Tracked 拷贝构造: " << name << endl;
}
Tracked& operator=(const Tracked& rhs) {
name = rhs.name + "(assign)";
cout << "Tracked 赋值: " << name << endl;
return *this;
}
~Tracked() {
cout << "Tracked 析构: " << name << endl;
}
};
// ========== 1. 空类,编译器自动生成默认函数 ==========
class Empty {};
// 验证:编译器生成的函数确实存在
void testEmpty() {
cout << "\n=== 测试空类 Empty ===" << endl;
Empty e1; // 调用默认构造函数(编译器生成)
Empty e2(e1); // 调用拷贝构造函数(编译器生成)
e1 = e2; // 调用拷贝赋值运算符(编译器生成)
// 析构函数自动调用
cout << "Empty 对象操作完成" << endl;
}
// ========== 2. 带成员对象的类,观察编译器生成函数的调用 ==========
class NamedObject {
public:
// 只声明了一个构造函数,因此编译器不会生成默认构造函数
NamedObject(const string& name, int value)
: nameValue(name), objectValue(value) {}
// 没有声明拷贝构造、拷贝赋值、析构,编译器会生成它们
// 为了观察,我们提供访问函数
const Tracked& getName() const { return nameValue; }
int getValue() const { return objectValue; }
private:
Tracked nameValue; // 成员对象,有构造/析构/拷贝
int objectValue;
};
void testNamedObject() {
cout << "\n=== 测试 NamedObject ===" << endl;
NamedObject no1("first", 100); // 调用我们声明的构造函数
NamedObject no2("second", 200);
NamedObject no3(no1); // 调用编译器生成的拷贝构造函数
// 拷贝构造函数会逐一拷贝成员:
// nameValue 用 no1.nameValue 拷贝构造
// objectValue 用 no1.objectValue 拷贝(int 直接拷贝)
no2 = no1; // 调用编译器生成的拷贝赋值运算符
// 拷贝赋值运算符会逐一赋值成员:
// nameValue = no1.nameValue (调用 Tracked 的赋值运算符)
// objectValue = no1.objectValue
cout << "no3 的 name: " << no3.getName().name << endl;
cout << "no2 的 name: " << no2.getName().name << endl;
// 析构时自动调用 Tracked 析构
}
// ========== 3. 编译器拒绝生成的情况 ==========
class ContainsRef {
public:
ContainsRef(int& ref) : someRef(ref) {} // 必须初始化引用成员
// 没有声明拷贝赋值运算符,但编译器不会生成,因为引用成员不能重新绑定
// 如果试图赋值,会导致编译错误
private:
int& someRef; // 引用成员
const int someConst; // const 成员(也需要初始化,但这里没初始化会报错)
// 注意:如果 both 引用和 const 成员存在,必须通过初始化列表初始化,
// 否则编译错误。这里为了简洁只保留引用成员演示。
};
void testContainsRef() {
cout << "\n=== 测试包含引用成员的类 ===" << endl;
int x = 10, y = 20;
ContainsRef cr1(x);
ContainsRef cr2(y);
// ContainsRef cr3(cr1); // 尝试拷贝构造:可能编译失败(因为引用成员不可拷贝构造?实际编译器会尝试拷贝引用,即让 cr3.someRef 绑定到 cr1.someRef 绑定的对象,这是允许的,因为引用可以绑定到原引用绑定的对象)
// 但重要的是拷贝赋值:cr1 = cr2; // 错误!编译器不会生成拷贝赋值运算符,因为引用成员无法重新赋值
cout << "ContainsRef 对象演示完成(赋值不可用)" << endl;
}
// ========== 4. C++11 移动操作的自动生成 ==========
class Movable {
public:
Movable() = default;
Movable(const string& s) : data(new string(s)) {
cout << "Movable 构造: " << *data << endl;
}
// 没有声明拷贝/移动操作,编译器会生成它们(但移动操作仅在需要时生成)
// 自定义析构函数会抑制移动操作的自动生成?实际上规则:
// 如果用户声明了析构函数,编译器不再自动生成移动操作(但拷贝操作仍生成,不过已弃用)。
// 为了演示,我们不声明析构函数,让编译器默认生成。
~Movable() {
if (data) cout << "Movable 析构: " << *data << endl;
delete data;
}
// 拷贝构造函数(编译器生成)
// 拷贝赋值运算符(编译器生成)
// 移动构造函数(编译器可能生成,取决于是否被使用且条件满足)
// 移动赋值运算符(类似)
void print() const {
if (data) cout << *data << endl;
else cout << "null" << endl;
}
private:
string* data = nullptr;
};
void testMovable() {
cout << "\n=== 测试 C++11 移动操作 ===" << endl;
Movable m1("hello");
Movable m2(std::move(m1)); // 应该调用移动构造函数(如果生成)
// 移动后 m1 的数据被移走,处于有效但未指定状态
cout << "m1: ";
m1.print();
cout << "m2: ";
m2.print();
Movable m3("world");
m3 = std::move(m2); // 移动赋值
cout << "m2: ";
m2.print();
cout << "m3: ";
m3.print();
}
int main() {
testEmpty();
testNamedObject();
testContainsRef();
testMovable();
return 0;
}
运行结果示例(可能因编译器不同略有差异)
=== 测试空类 Empty ===
Empty 对象操作完成
=== 测试 NamedObject ===
Tracked 构造: first
Tracked 构造: second
Tracked 拷贝构造: first(copy)
Tracked 赋值: first(assign)
no3 的 name: first(copy)
no2 的 name: first(assign)
Tracked 析构: first(assign)
Tracked 析构: first(copy)
Tracked 析构: second
Tracked 析构: first
=== 测试包含引用成员的类 ===
ContainsRef 对象演示完成(赋值不可用)
=== 测试 C++11 移动操作 ===
Movable 构造: hello
Movable 析构: (null? 取决于实现)
m1: null
m2: hello
Movable 构造: world
m2: null
m3: world
Movable 析构: world
Movable 析构: hello
Movable 析构:
关键要点解析
1. 空类并非真的"空"
- 即使你定义一个空类
Empty,编译器也会在需要时生成默认构造、拷贝构造、拷贝赋值、析构函数。这些函数都是 inline 的 public 成员。 - 所以代码
Empty e1;和Empty e2(e1);都可以通过编译,尽管你没写任何函数。
2. 编译器生成函数的默认行为
- 对于
NamedObject,我们只声明了一个自定义构造函数,因此编译器不再生成默认构造函数。但拷贝构造、拷贝赋值、析构仍然生成。 - 生成的拷贝构造函数:以成员逐一的方式用
no1的成员初始化no3的成员。这里nameValue是Tracked类型,会调用Tracked的拷贝构造函数;objectValue是int,直接复制。 - 生成的拷贝赋值运算符:以成员逐一的方式将
no1的成员赋给no2的成员。这会导致调用Tracked的赋值运算符。 - 注意:编译器生成的拷贝赋值运算符要求所有成员的类型都提供可用的赋值运算符。如果某个成员是
const或引用,编译器会拒绝生成拷贝赋值(因为没有合法方式给const或引用赋值)。
3. 编译器拒绝生成的情况
- 类
ContainsRef包含一个引用成员someRef。引用必须在构造时初始化,并且不能重新绑定。因此编译器无法生成拷贝赋值运算符(赋值意味着尝试改变引用的指向,不可能)。如果你尝试使用拷贝赋值(如cr1 = cr2),编译将报错。 - 同理,如果成员是
const对象,也不能被赋值,因此拷贝赋值运算符会被隐式删除。
4. C++11 移动操作的自动生成
- 在 C++11 中,如果类没有用户声明的拷贝操作、移动操作、析构函数,编译器会自动生成移动构造函数和移动赋值运算符(当需要时)。它们执行成员逐一的移动。
- 在上例中,
Movable没有声明拷贝构造/赋值、移动构造/赋值,也没有声明析构函数?其实我们声明了析构函数(~Movable()),这会抑制移动操作的自动生成 !这是 C++11 的一个重要规则:如果用户声明了析构函数,编译器就不会自动生成移动操作。但我们的示例中声明了析构函数,所以实际不会生成移动操作,代码中的std::move会回退到拷贝操作(因为移动操作不存在)。为了真正演示移动操作,我们应移除自定义析构函数,或者显式=default移动操作。 - 所以实际运行结果可能显示的是拷贝行为而非移动。正确的演示需要调整代码(例如不声明析构函数,或显式声明移动操作为
default)。这里保留代码以说明问题,并提醒读者注意规则。
总结
- 编译器会默默地为类生成一些成员函数(默认构造、析构、拷贝构造、拷贝赋值,C++11 还可能生成移动构造和移动赋值)。
- 这些默认函数的行为是"成员逐一初始化/赋值/析构"。
- 当你声明了某些函数(如自定义构造函数、拷贝操作、移动操作、析构函数)时,编译器可能会停止生成其他默认函数。
- 理解这些隐式生成的函数对于控制类的行为至关重要,特别是在资源管理、多态和异常安全方面。