6. 深拷贝与浅拷贝:默认拷贝构造函数的陷阱、自定义拷贝构造函数实现深拷贝、资源管理
拷贝构造函数,说白了就是用一个对象去初始化另一个同类型的对象。听起来很简单对吧?但这里头的水,比你想象的要深得多。我见过太多人在这个坑里摔得鼻青脸肿,包括我自己刚入行那会儿。
6.1 默认拷贝构造函数:编译器替你做的“好事”
如果你没有显式定义拷贝构造函数,编译器会帮你生成一个默认的。这个默认版本做的事情很简单——逐字节复制。也就是把源对象的每个成员变量,原封不动地拷贝到目标对象中。
对于基本类型(int、double、指针等),这没问题。但一旦涉及到动态内存,麻烦就来了。
核心概念:浅拷贝(Shallow Copy)只复制指针的值,不复制指针指向的内存。深拷贝(Deep Copy)会复制指针指向的实际数据。
6.2 浅拷贝的陷阱:一个经典的崩溃案例
来看一段代码,我当年在实习时就写过类似的:
class StringBad {
private:
char* str;
int len;
public:
StringBad(const char* s) {
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
~StringBad() {
delete[] str; // 释放内存
}
};
int main() {
StringBad s1("Hello");
StringBad s2 = s1; // 使用默认拷贝构造函数
// 程序结束时,s1和s2的析构函数都会执行
// 同一块内存被释放两次 —— 崩溃!
return 0;
}
为什么会这样?默认拷贝构造函数把s1.str的地址直接复制给了s2.str。两个对象指向同一块堆内存。当它们离开作用域时,析构函数会先后释放同一块内存。第二次释放时,程序直接崩溃。
警告:浅拷贝导致的“双重释放”(double free)是C++中最常见的崩溃原因之一。我曾经在一个线上服务中排查了整整两天,最后发现就是这个问题。
6.3 自定义拷贝构造函数:实现深拷贝
解决方案很直接——我们自己写拷贝构造函数,让它做深拷贝:
class StringGood {
private:
char* str;
int len;
public:
StringGood(const char* s) {
len = strlen(s);
str = new char[len + 1];
strcpy(str, s);
}
// 自定义拷贝构造函数 —— 深拷贝
StringGood(const StringGood& other) {
len = other.len;
str = new char[len + 1]; // 分配新内存
strcpy(str, other.str); // 复制数据
// 现在每个对象都有自己的独立内存
}
~StringGood() {
delete[] str;
}
};
深拷贝的核心就两句话:先分配自己的内存,再复制数据过去。这样每个对象都拥有独立的内存空间,析构时各删各的,互不干扰。
6.4 资源管理的三/五法则
说到资源管理,就不得不提C++的三/五法则(Rule of Three/Five)。这个法则我建议你刻在脑子里:
| 需要自定义的资源操作 | 必须同时定义的函数 |
|---|---|
| 析构函数 | 拷贝构造函数、拷贝赋值运算符 |
| 拷贝构造函数 | 析构函数、拷贝赋值运算符 |
| 拷贝赋值运算符 | 析构函数、拷贝构造函数 |
说白了,如果你需要手动管理资源(new/delete、文件句柄、互斥锁等),这三个函数就得一起上。C++11之后还加上了移动构造函数和移动赋值运算符,变成了五法则。
个人建议:能用标准库容器(std::string、std::vector)就别自己管理内存。它们已经实现了正确的拷贝语义,你直接用就行,省心省力。
6.5 知识体系总览
下面这张图把深拷贝与浅拷贝的核心逻辑串起来了,你可以对照着理解:
6.6 避坑指南与最佳实践
我总结了几条实战经验,希望能帮你少走弯路:
- 默认拷贝构造函数不是万能的——只要类里有指针成员,你就得警惕。我曾经接手过一个项目,整个代码库因为一个浅拷贝bug,导致内存泄漏了半年没人发现。
- 拷贝赋值运算符也要深拷贝——很多人只关注拷贝构造函数,忘了赋值操作。记住,
s2 = s1和String s2 = s1是两回事,前者调用赋值运算符,后者调用拷贝构造函数。 - 考虑使用智能指针——C++11的
std::shared_ptr和std::unique_ptr能帮你自动管理资源。但要注意,shared_ptr的拷贝是共享所有权,不是深拷贝,语义不同。 - 禁用拷贝语义——如果某个类不应该被拷贝(比如管理互斥锁的类),用
= delete明确禁止:MyClass(const MyClass&) = delete;。
一句话总结:浅拷贝复制指针,深拷贝复制数据。有动态资源的地方,就要有深拷贝。这是C++资源管理的基石,也是你从新手走向资深工程师的必经之路。
公众号:蓝海资料掘金营,微信deep3321