目录
[三、浅拷贝 vs 深拷贝](#三、浅拷贝 vs 深拷贝)
[1. 拷贝构造函数参数不用引用 → 无限递归](#1. 拷贝构造函数参数不用引用 → 无限递归)
[2. 忘了const导致无法拷贝const对象](#2. 忘了const导致无法拷贝const对象)
[3. 浅拷贝发生在你没想到的地方](#3. 浅拷贝发生在你没想到的地方)
一、一个崩溃的程序
先看这段代码,你觉得它会崩溃吗?
cpp
class StringWrapper {
private:
char* data;
public:
StringWrapper(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
~StringWrapper() {
delete[] data;
}
void print() { cout << data << endl; }
};
int main() {
StringWrapper s1("Hello");
StringWrapper s2 = s1; // 用s1初始化s2
s1.print();
s2.print();
return 0;
} // 程序在这里崩溃!
运行结果:可能正常输出,也可能输出乱码,最后大概率崩溃。
原因很简单:s1和s2里面的data指针指向了同一块内存 。当程序结束,s2先析构,delete[]了那块内存;然后s1析构,再次delete[]同一块内存------重复释放,程序崩溃。
这就是浅拷贝带来的灾难。
二、拷贝构造函数是什么?
拷贝构造函数是一种特殊的构造函数:
-
参数是本类对象的const引用
-
用已有的对象 去创建新的对象时自动调用
-
如果你不写,编译器会生成一个默认的(逐成员复制)
语法长这样:
cpp
class MyClass {
public:
// 拷贝构造函数
MyClass(const MyClass& other) {
// 拷贝逻辑
}
};
调用时机(三个场景)
cpp
class Demo {
public:
Demo() { cout << "普通构造" << endl; }
Demo(const Demo& other) { cout << "拷贝构造" << endl; }
~Demo() { cout << "析构" << endl; }
};
Demo makeDemo() {
Demo d;
return d; // 场景3:返回值
}
int main() {
Demo a; // 普通构造
Demo b = a; // 场景1:用a初始化b → 拷贝构造
Demo c(a); // 场景2:直接传参 → 拷贝构造
Demo d = makeDemo(); // 场景3:返回值(可能被优化掉,不一定调用)
}
关键点:"="在这里不是赋值,是初始化 。赋值是后面讲的重载operator=。
三、浅拷贝 vs 深拷贝
浅拷贝(默认行为)
编译器生成的默认拷贝构造函数做的事很简单:把每个成员变量的值原样复制。
cpp
// 编译器生成的默认版本(概念上)
StringWrapper(const StringWrapper& other)
: data(other.data) // 只复制指针的值,不复制指针指向的内容
{}
对于int、double这种值类型,浅拷贝没问题。但对于指针,复制的是地址,不是地址里的内容。
浅拷贝的问题:
-
两个对象指向同一块内存
-
一个修改,另一个也跟着变(可能不是你想要的效果)
-
一个释放,另一个变成悬空指针
-
重复释放导致崩溃
深拷贝(正确的做法)
深拷贝的做法:不复制指针的值,而是复制指针指向的内容。
cpp
class StringWrapper {
private:
char* data;
public:
// 普通构造函数
StringWrapper(const char* str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
}
// 拷贝构造函数(深拷贝)
StringWrapper(const StringWrapper& other) {
// 1. 分配新内存
data = new char[strlen(other.data) + 1];
// 2. 复制内容
strcpy(data, other.data);
cout << "深拷贝:" << data << endl;
}
// 析构函数
~StringWrapper() {
delete[] data;
cout << "释放:" << data << endl;
}
void print() { cout << data << endl; }
// 后面会讲赋值运算符重载
};
现在运行之前会崩溃的例子:
cpp
int main() {
StringWrapper s1("Hello");
StringWrapper s2 = s1; // 深拷贝:s2有自己独立的内存
s1.print(); // Hello
s2.print(); // Hello
return 0; // 分别释放两块内存,不冲突
}
内存布局对比:
text
浅拷贝:
s1.data ──→ [H][e][l][l][o][\0]
s2.data ──→ ↑ (指向同一块)
深拷贝:
s1.data ──→ [H][e][l][l][o][\0]
s2.data ──→ [H][e][l][l][o][\0] (另一块内存)
四、什么时候必须自己写拷贝构造函数?
三法则(Rule of Three):如果类需要自定义析构函数,那么它几乎一定也需要自定义拷贝构造函数和拷贝赋值运算符。
具体来说,以下情况必须写拷贝构造函数:
-
类里有指针成员,并且构造函数里用
new分配了内存 -
类里有文件句柄、数据库连接等需要"独占"的资源
-
类里有互斥锁(mutex)(两个对象拥有同一个锁会导致死锁)
一句话:默认的逐成员复制对你的资源管理方式不适用时。
一个反面例子:vector的浅拷贝问题
cpp
class IntVector {
private:
int* arr;
int size;
public:
IntVector(int n) : size(n) {
arr = new int[n];
for(int i=0; i<n; i++) arr[i] = i;
}
~IntVector() { delete[] arr; }
// 没有写拷贝构造函数 → 浅拷贝!
void set(int idx, int val) { arr[idx] = val; }
int get(int idx) { return arr[idx]; }
};
int main() {
IntVector v1(5);
IntVector v2 = v1; // 浅拷贝,v2.arr指向v1.arr同一块内存
v2.set(0, 999); // 修改v2
cout << v1.get(0); // 输出999!v1被意外修改了
// 程序结束,两次delete[]同一块内存 → 崩溃
}
这就是所谓的"意外的共享状态"。
五、完整的例子:安全的动态数组
cpp
#include <iostream>
#include <cstring>
using namespace std;
class SafeArray {
private:
int* data;
int size;
public:
// 普通构造函数
SafeArray(int n) : size(n) {
data = new int[n];
for (int i = 0; i < n; i++) {
data[i] = 0;
}
cout << "构造:分配了" << n << "个int" << endl;
}
// 拷贝构造函数(深拷贝)
SafeArray(const SafeArray& other) : size(other.size) {
data = new int[size];
for (int i = 0; i < size; i++) {
data[i] = other.data[i];
}
cout << "拷贝构造:深拷贝了" << size << "个int" << endl;
}
// 析构函数
~SafeArray() {
delete[] data;
cout << "析构:释放了" << size << "个int" << endl;
}
void set(int idx, int val) {
if (idx >= 0 && idx < size) data[idx] = val;
}
int get(int idx) const {
if (idx >= 0 && idx < size) return data[idx];
return -1;
}
void print() const {
cout << "[";
for (int i = 0; i < size; i++) {
cout << data[i] << (i < size-1 ? ", " : "");
}
cout << "]" << endl;
}
};
int main() {
SafeArray a(5);
for (int i = 0; i < 5; i++) a.set(i, i * 10);
a.print(); // [0, 10, 20, 30, 40]
SafeArray b = a; // 拷贝构造
b.set(0, 999);
cout << "a: "; a.print(); // [0, 10, 20, 30, 40] ← 没被影响
cout << "b: "; b.print(); // [999, 10, 20, 30, 40] ← 独立修改
return 0;
}
输出:
text
构造:分配了5个int
[0, 10, 20, 30, 40]
拷贝构造:深拷贝了5个int
a: [0, 10, 20, 30, 40]
b: [999, 10, 20, 30, 40]
析构:释放了5个int
析构:释放了5个int
完美!两个对象互不干扰,各释放各的内存。
六、三个常见的坑
1. 拷贝构造函数参数不用引用 → 无限递归
cpp
class Bad {
public:
Bad(Bad other) { // ❌ 传值,会再次调用拷贝构造,无限递归
// ...
}
};
参数必须用引用 ,通常是const引用:
cpp
Bad(const Bad& other) { } // ✅
2. 忘了const导致无法拷贝const对象
cpp
class Demo {
public:
Demo(Demo& other) { } // 参数不是const
};
const Demo d1;
Demo d2 = d1; // ❌ 错误!不能将const转为非const引用
3. 浅拷贝发生在你没想到的地方
函数传参也会调用拷贝构造函数:
cpp
void func(SafeArray arr) { // 传值,会调用拷贝构造
// ...
}
SafeArray a(10);
func(a); // 这里发生了一次深拷贝
如果数组很大,深拷贝的开销不小。想避免拷贝?用引用传参:
cpp
void func(const SafeArray& arr) { // 不拷贝
// ...
}
七、这一篇的收获
你现在应该明白:
-
拷贝构造函数:用已有对象创建新对象时调用
-
浅拷贝:默认行为,只复制指针的值,导致两个对象共享内存
-
深拷贝:自己实现,分配新内存并复制内容,对象各自独立
-
三法则:需要析构函数 → 就需要拷贝构造和拷贝赋值
💡 小作业:修改上面的
SafeArray,故意去掉拷贝构造函数,观察程序会出什么问题。然后加上拷贝构造函数,验证深拷贝解决了问题。
下一篇预告 :第5篇《类与对象(四):赋值运算符重载》------=不只是初始化,还有赋值。拷贝构造和赋值运算符有什么区别?什么时候调用哪个?为什么赋值要返回引用?下篇揭晓。