← 目录 / 第十二章 · 类与面向对象 / 12.6 拷贝构造函数

12.6 拷贝构造函数

"复制一个对象"听起来很简单,但当对象内部有指针时就会踩坑——两个对象共用同一块内存,一个析构了,另一个就崩掉了。本节彻底讲清楚浅拷贝与深拷贝的区别。

本页目录
12.6.1 什么时候会"复制对象"

在 C++ 里,以下三种情况都会触发对象的复制,此时拷贝构造函数会被自动调用:

12.6.2 拷贝构造函数的语法

拷贝构造函数是一种特殊的构造函数,它接受同类型对象的常量引用作为参数。如果你不写,C++ 会自动生成一个——逐字段复制所有成员变量,这就是"浅拷贝"。

C++ · 拷贝构造函数的写法
1class Student {
2public:
3 string name;
4 int score;
5
6 // 普通构造函数
7 Student(string n, int s) : name(n), score(s) {}
8
9 // 拷贝构造函数:参数是 const 同类型引用
10 Student(const Student& other) : name(other.name), score(other.score) {
11 cout << "拷贝了 " << other.name << endl;
12 }
13};
14
15int main() {
16 Student a("Alice", 95);
17 Student b = a; // 触发拷贝构造 → 输出:拷贝了 Alice
18 Student c(a); // 同上,两种写法等价
19}
⚠️
参数必须是引用const Student&),不能写成值传递(Student other)。如果写成值传递,调用拷贝构造时又需要复制参数,又会调用拷贝构造……无限递归,直接崩溃。
12.6.3 浅拷贝的陷阱

对于只有普通成员变量(intstring 等)的类,C++ 自动生成的浅拷贝完全够用。问题出在类里有指针成员的时候。

浅拷贝只复制指针的值(地址),不复制指针指向的内容——两个对象的指针最终指向同一块内存

浅拷贝(危险) 深拷贝(安全) 对象 a data → 0x1A2B 对象 b data → 0x1A2B 堆内存 0x1A2B "Hello" b 析构时 delete 这块内存 a 析构时再次 delete → 崩溃 💥 对象 a data → 0x1A2B 对象 b data → 0x3C4D 堆内存 0x1A2B "Hello" 堆内存 0x3C4D "Hello" 各自独立,互不干扰 ✅ 浅拷贝复制指针地址,深拷贝在堆上新建一份独立的内容
浅拷贝b.dataa.data 指向同一块堆内存。b 析构时 delete 了这块内存,a 析构时再次 delete 同一地址,程序崩溃。
深拷贝:在堆上为 b 单独分配一块新内存,复制内容。两者互相独立,各自析构互不影响。

看一个完整的会崩溃的例子,理解浅拷贝的危险:

C++ · 浅拷贝导致的 double-free 崩溃
1class Buffer {
2public:
3 int* data; // 指针成员,指向堆上的数组
4 int size;
5
6 Buffer(int n) : size(n) {
7 data = new int[n]; // 在堆上分配内存
8 }
9 ~Buffer() { delete[] data; } // 析构时释放
10 // ⚠️ 没有自定义拷贝构造函数,使用编译器默认的浅拷贝
11};
12
13int main() {
14 Buffer a(10);
15 Buffer b = a; // 浅拷贝:b.data 和 a.data 指向同一块内存
16} // b 先析构:delete[] data(0x1A2B)
17 // a 再析构:delete[] data(0x1A2B)← 💥 double-free,程序崩溃!
12.6.4 深拷贝:自己写拷贝构造函数

解决办法是自己写拷贝构造函数,在里面为新对象单独申请一块内存,再把内容复制过去,而不是直接复制指针地址。

C++ · 自定义深拷贝构造函数
1class Buffer {
2public:
3 int* data;
4 int size;
5
6 Buffer(int n) : size(n) {
7 data = new int[n]();
8 }
9
10 // 深拷贝:重新申请内存,复制内容
11 Buffer(const Buffer& other) : size(other.size) {
12 data = new int[size]; // ① 新开一块内存
13 copy(other.data, other.data + size, data); // ② 复制内容
14 }
15
16 ~Buffer() { delete[] data; }
17};
18
19int main() {
20 Buffer a(10);
21 a.data[0] = 42;
22 Buffer b = a; // 深拷贝:b 有独立的内存
23 b.data[0] = 99; // 修改 b 不影响 a
24 cout << a.data[0]; // 输出:42(a 不受影响)
25} // 析构时两者各自释放自己的内存,安全 ✅
❌ 浅拷贝(默认行为)
直接复制指针的值,两个对象共用同一块堆内存。
・修改 b 的数据会影响 a
・其中一个析构后,另一个指向已释放的内存
・第二次析构触发 double-free,程序崩溃
✅ 深拷贝(自定义)
为新对象单独申请内存,复制内容。
・两个对象完全独立
・修改任意一个不影响另一个
・各自析构各自的内存,安全
12.6.5 三五法则

C++ 有一条经验法则叫"三法则(Rule of Three)"

如果你的类需要手动管理资源(比如内部有 new 出来的指针),那么以下三个函数必须同时自定义,缺一不可:

C++ · 三法则:三个函数必须配套
1class Buffer {
2public:
3 int* data; int size;
4
5 ~Buffer() { delete[] data; } // ① 析构函数:释放内存
6
7 Buffer(const Buffer& other) { // ② 拷贝构造函数
8 size = other.size;
9 data = new int[size]; // 新开内存,深拷贝
10 copy(other.data, other.data+size, data);
11 }
12
13 Buffer& operator=(const Buffer& other) { // ③ 拷贝赋值运算符
14 if (this == &other) return *this; // 防止自赋值 a = a
15 delete[] data; // 先释放旧内存
16 size = other.size;
17 data = new int[size]; // 新开内存,深拷贝
18 copy(other.data, other.data+size, data);
19 return *this;
20 }
21};
22
23int main() {
24 Buffer a(10), b(5);
25 Buffer c = a; // 触发 ② 拷贝构造
26 b = a; // 触发 ③ 拷贝赋值(b 已存在,不是初始化)
27}
💡
拷贝构造 vs 拷贝赋值,傻傻分不清?
区别只在于对象是否已经存在
Buffer c = a; — c 是新建的,调用拷贝构造函数(对象从无到有)
b = a; — b 已经存在,调用拷贝赋值运算符(对象已有,覆盖掉旧值)
拷贝赋值要多做一步:先 delete 掉旧内存,再深拷贝新内容。

道理很简单:写了析构函数说明类里有手动管理的资源,拷贝时和赋值时同样需要深度处理——三件事是配套的,缺任何一个都会埋下隐患。

C++11 之后还引入了移动语义,扩展为"五法则(Rule of Five)",在三法则基础上再加上移动构造函数和移动赋值运算符。竞赛阶段了解三法则即可,五法则属于更高阶的工程实践。

💡
竞赛中的实用建议:在 CSP-J / NOIP 中,尽量避免在类里直接管理裸指针。用 vectorstring 等 STL 容器替代手动 new,它们自己实现了深拷贝,完全不需要你操心三法则,代码更简洁也更安全。
📖
本节小结
・拷贝构造函数在"用对象初始化对象"、"值传递"、"值返回"三种情况下自动触发。
・默认浅拷贝只复制指针地址,类含指针成员时会导致 double-free 崩溃。
・深拷贝:在拷贝构造函数里为新对象重新分配内存并复制内容。
・遵循三法则:写了析构函数,就同时写拷贝构造和拷贝赋值运算符。
・竞赛中优先用 STL 容器,避免手动管理内存。