概述
C++ 类的特殊成员函数控制着对象的创建、复制、移动和销毁。理解构造函数(普通构造、拷贝构造、移动构造)和赋值运算符(拷贝赋值、移动赋值)的区别与联系,是掌握 C++ 资源管理的关键。
一、特殊成员函数概览
C++11 之后,一个类有六大特殊成员函数:
| 函数 |
作用 |
形式 |
| 默认构造函数 |
无参创建对象 |
T() |
| 析构函数 |
销毁对象 |
~T() |
| 拷贝构造函数 |
用左值初始化新对象 |
T(const T&) |
| 移动构造函数 |
用右值初始化新对象 |
T(T&&) |
| 拷贝赋值运算符 |
用左值赋值给已存在对象 |
T& operator=(const T&) |
| 移动赋值运算符 |
用右值赋值给已存在对象 |
T& operator=(T&&) |
二、构造函数家族
2.1 默认构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class String {
private:
char* data;
size_t len;
public:
// 默认构造函数
String() : data(nullptr), len(0) {
std::cout << "Default constructor\n";
}
};
String s1; // 调用默认构造函数
String s2 = String(); // 调用默认构造函数(不是赋值!)
String s3{}; // 调用默认构造函数(现代风格)
|
2.2 拷贝构造函数(左值构造)
触发场景:用一个已存在的左值对象来初始化一个新对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
class String {
private:
char* data;
size_t len;
public:
String(const char* str = "") {
len = strlen(str);
data = new char[len + 1];
memcpy(data, str, len + 1);
}
~String() {
delete[] data;
}
// 拷贝构造函数(左值构造)
String(const String& other) : len(other.len) {
std::cout << "Copy constructor (lvalue)\n";
data = new char[len + 1]; // 分配新内存
memcpy(data, other.data, len + 1); // 深拷贝
}
};
String s1("hello");
String s2 = s1; // 拷贝构造:用左值 s1 初始化新对象 s2
String s3(s1); // 拷贝构造:用左值 s1 初始化新对象 s3
String s4 = String(s1); // 拷贝构造(可能是拷贝消除)
|
关键点:
- 参数是
const T&(左值引用)
- 必须深拷贝,否则两个对象指向同一资源,析构时会 double free
2.3 移动构造函数(右值构造)
触发场景:用一个右值来初始化一个新对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
class String {
private:
char* data;
size_t len;
public:
// 移动构造函数(右值构造)
String(String&& other) noexcept : data(other.data), len(other.len) {
std::cout << "Move constructor (rvalue)\n";
other.data = nullptr; // 源对象置空,防止重复释放
other.len = 0;
}
};
String s1("hello");
String s2 = std::move(s1); // 移动构造:s1 被转为右值
String s3 = get_string(); // 移动构造:返回值是右值
// 函数返回值优化
String create_string() {
String temp("world");
return temp; // 可能触发移动构造(或RVO)
}
|
关键点:
- 参数是
T&&(右值引用)
- 窃取资源,不分配新内存
- 将源对象置于"有效但未定义"状态
- 应标记
noexcept 以利于标准库优化
三、赋值运算符家族
3.1 拷贝赋值运算符
触发场景:用一个左值赋值给一个已存在的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
class String {
public:
// 拷贝赋值运算符
String& operator=(const String& other) {
std::cout << "Copy assignment\n";
if (this == &other) { // 自赋值检查
return *this;
}
// 释放旧资源
delete[] data;
// 深拷贝新资源
len = other.len;
data = new char[len + 1];
memcpy(data, other.data, len + 1);
return *this; // 支持链式赋值
}
};
String s1("hello");
String s2;
s2 = s1; // 拷贝赋值:s1 是左值,s2 已存在
|
3.2 移动赋值运算符
触发场景:用一个右值赋值给一个已存在的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
class String {
public:
// 移动赋值运算符
String& operator=(String&& other) noexcept {
std::cout << "Move assignment\n";
if (this == &other) { // 自赋值检查
return *this;
}
// 释放旧资源
delete[] data;
// 窃取资源
data = other.data;
len = other.len;
// 源对象置空
other.data = nullptr;
other.len = 0;
return *this;
}
};
String s1("hello");
String s2;
s2 = std::move(s1); // 移动赋值:s1 转为右值
s2 = get_string(); // 移动赋值:返回值是右值
|
四、区别与联系
4.1 构造 vs 赋值的根本区别
| 特性 |
构造函数 |
赋值运算符 |
| 对象状态 |
对象不存在 |
对象已存在 |
| 触发时机 |
初始化时 |
赋值时 |
| 资源管理 |
无需释放旧资源 |
必须先释放旧资源 |
| 自身检查 |
不需要 |
必须检查自赋值 |
1
2
3
4
5
6
7
|
String s1("hello"); // 构造:s1 不存在 → 存在
String s2 = s1; // 拷贝构造:s2 不存在 → 存在
String s3;
s3 = s1; // 拷贝赋值:s3 已存在,先释放旧资源,再拷贝
s3 = s3; // 自赋值!赋值运算符必须处理这种情况
|
4.2 左值 vs 右值的区别
1
2
3
4
5
6
7
8
9
10
11
|
String s1("hello");
// 左值:有名字、有地址、可取地址
String& ref = s1; // s1 是左值
String s2 = s1; // s1 是左值 → 拷贝构造
s2 = s1; // s1 是左值 → 拷贝赋值
// 右值:无名字、临时、不可取地址
String s3 = String("world"); // String("world") 是右值 → 可能移动构造或RVO
String s4 = std::move(s1); // std::move(s1) 是右值 → 移动构造
s4 = get_string(); // 返回值是右值 → 移动赋值
|
4.3 完整示例:追踪所有调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
#include <iostream>
#include <utility>
class Trace {
public:
int* data;
// 默认构造
Trace() : data(new int(0)) {
std::cout << "Default constructor\n";
}
// 带参构造
Trace(int v) : data(new int(v)) {
std::cout << "Parameterized constructor: " << v << "\n";
}
// 析构函数
~Trace() {
std::cout << "Destructor: " << (data ? *data : -1) << "\n";
delete data;
}
// 拷贝构造(左值构造)
Trace(const Trace& other) : data(new int(*other.data)) {
std::cout << "Copy constructor (lvalue): " << *other.data << "\n";
}
// 移动构造(右值构造)
Trace(Trace&& other) noexcept : data(other.data) {
std::cout << "Move constructor (rvalue): " << *data << "\n";
other.data = nullptr;
}
// 拷贝赋值
Trace& operator=(const Trace& other) {
std::cout << "Copy assignment: " << *other.data << "\n";
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
// 移动赋值
Trace& operator=(Trace&& other) noexcept {
std::cout << "Move assignment: " << *other.data << "\n";
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
Trace create_trace(int v) {
return Trace(v); // RVO 或移动构造
}
int main() {
std::cout << "=== 1. 默认构造 ===\n";
Trace t1;
std::cout << "\n=== 2. 带参构造 ===\n";
Trace t2(42);
std::cout << "\n=== 3. 拷贝构造(左值构造) ===\n";
Trace t3 = t2; // 左值 t2 → 拷贝构造
std::cout << "\n=== 4. 移动构造(右值构造) ===\n";
Trace t4 = std::move(t2); // t2 转为右值 → 移动构造
std::cout << "\n=== 5. 拷贝赋值 ===\n";
t1 = t3; // 左值 t3 → 拷贝赋值
std::cout << "\n=== 6. 移动赋值 ===\n";
t1 = create_trace(100); // 返回值是右值 → 移动赋值
std::cout << "\n=== 7. 析构 ===\n";
return 0;
}
|
可能的输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
=== 1. 默认构造 ===
Default constructor
=== 2. 带参构造 ===
Parameterized constructor: 42
=== 3. 拷贝构造(左值构造) ===
Copy constructor (lvalue): 42
=== 4. 移动构造(右值构造) ===
Move constructor (rvalue): 42
=== 5. 拷贝赋值 ===
Copy assignment: 42
=== 6. 移动赋值 ===
Parameterized constructor: 100
Move assignment: 100
=== 7. 析构 ===
Destructor: 100
Destructor: -1
Destructor: 42
Destructor: -1
Destructor: 0
|
五、Rule of Three / Five / Zero
5.1 Rule of Three(C++03)
如果类需要自定义以下任一函数,则三个都需要自定义:
1
2
3
4
5
6
7
8
9
10
11
|
class Resource {
int* data;
public:
~Resource() { delete data; }
Resource(const Resource& other) { data = new int(*other.data); }
Resource& operator=(const Resource& other) {
delete data;
data = new int(*other.data);
return *this;
}
};
|
5.2 Rule of Five(C++11)
在 Rule of Three 基础上,增加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
class Resource {
int* data;
public:
~Resource() { delete data; }
Resource(const Resource& other) : data(new int(*other.data)) {}
Resource& operator=(const Resource& other) {
if (this != &other) {
delete data;
data = new int(*other.data);
}
return *this;
}
Resource(Resource&& other) noexcept : data(other.data) {
other.data = nullptr;
}
Resource& operator=(Resource&& other) noexcept {
if (this != &other) {
delete data;
data = other.data;
other.data = nullptr;
}
return *this;
}
};
|
5.3 Rule of Zero(现代 C++ 推荐)
使用智能指针和标准库容器,让编译器自动生成所有函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#include <memory>
#include <string>
class ModernResource {
std::unique_ptr<int> data;
std::string name;
public:
ModernResource() : data(std::make_unique<int>(0)) {}
ModernResource(int v, std::string n)
: data(std::make_unique<int>(v)), name(std::move(n)) {}
// 不需要自定义任何特殊成员函数!
// 编译器自动生成的版本完全正确
};
|
六、编译器自动生成规则
6.1 成员函数的生成规则(C++11)
| 用户声明 |
默认构造 |
析构 |
拷贝构造 |
拷贝赋值 |
移动构造 |
移动赋值 |
| 无 |
✓ |
✓ |
✓ |
✓ |
✓ |
✓ |
| 默认构造 |
✗ |
✓ |
✓ |
✓ |
✓ |
✓ |
| 析构 |
✓ |
✗ |
✓ |
✓ |
✗ |
✗ |
| 拷贝构造 |
✗ |
✓ |
✗ |
✓ |
✗ |
✗ |
| 拷贝赋值 |
✓ |
✓ |
✓ |
✗ |
✗ |
✗ |
| 移动构造 |
✗ |
✓ |
✗ |
✗ |
✗ |
✗ |
| 移动赋值 |
✓ |
✓ |
✗ |
✗ |
✗ |
✗ |
6.2 = default 和 = delete
1
2
3
4
5
6
7
8
9
10
11
12
13
|
class Widget {
public:
Widget() = default; // 显式要求编译器生成
Widget(const Widget&) = delete; // 禁止拷贝构造
Widget& operator=(const Widget&) = delete; // 禁止拷贝赋值
Widget(Widget&&) = default; // 显式生成移动构造
Widget& operator=(Widget&&) = default; // 显式生成移动赋值
~Widget() = default;
};
Widget w1; // OK
Widget w2 = w1; // 错误:拷贝构造被删除
Widget w3 = std::move(w1); // OK:移动构造
|
七、常见错误与陷阱
7.1 返回局部对象的引用
1
2
3
4
5
6
7
8
9
10
11
|
// ❌ 错误:返回局部变量的引用
const String& bad_func() {
String local("hello");
return local; // 悬空引用!
}
// ✅ 正确:返回值(触发移动或RVO)
String good_func() {
String local("hello");
return local;
}
|
7.2 移动后使用源对象
1
2
3
4
5
|
String s1("hello");
String s2 = std::move(s1);
// ❌ 危险:s1 处于"有效但未定义"状态
std::cout << s1.data; // 可能崩溃或输出垃圾
|
7.3 忘记自赋值检查
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// ❌ 错误:没有自赋值检查
String& operator=(const String& other) {
delete[] data; // 如果 this == &other,data 已被删除!
data = new char[other.len + 1];
memcpy(data, other.data, other.len + 1); // 读取已释放的内存!
return *this;
}
// ✅ 正确:先检查自赋值
String& operator=(const String& other) {
if (this == &other) return *this;
delete[] data;
data = new char[other.len + 1];
memcpy(data, other.data, other.len + 1);
return *this;
}
|
7.4 copy-and-swap 惯用法
一个优雅的实现方式,同时解决自赋值和异常安全:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
class String {
public:
void swap(String& other) noexcept {
std::swap(data, other.data);
std::swap(len, other.len);
}
// 统一的拷贝赋值(通过值传递)
String& operator=(String other) noexcept {
swap(other); // 交换资源
return *this;
// other 析构时自动释放旧资源
}
};
// 使用
String s1("hello");
s1 = String("world"); // 移动构造参数,然后交换
s1 = s1; // 自赋值安全:拷贝一份,然后交换,没有问题
|
八、总结对照表
| 操作 |
函数签名 |
触发条件 |
对象状态 |
资源处理 |
| 拷贝构造 |
T(const T&) |
左值初始化新对象 |
对象不存在 |
深拷贝 |
| 移动构造 |
T(T&&) |
右值初始化新对象 |
对象不存在 |
窃取资源 |
| 拷贝赋值 |
T& operator=(const T&) |
左值赋值已存在对象 |
对象已存在 |
释放旧+深拷贝 |
| 移动赋值 |
T& operator=(T&&) |
右值赋值已存在对象 |
对象已存在 |
释放旧+窃取 |
记忆口诀:
- 构造 = 新生 → 不需要清理旧资源
- 赋值 = 改造 → 必须先清理旧资源
- 左值 = 拷贝 → 别人的东西不能抢
- 右值 = 移动 → 临时的东西可以偷
参考资料