深浅拷贝是 C++入门最大拦路虎,也是面试必考高频题。
很多新人写自定义类、动态内存,程序随机崩溃、双重释放、内存错乱,99% 的根源都是深浅拷贝没有搞懂。
一、什么是浅拷贝
如果你没有手动写拷贝构造函数、赋值运算符重载,编译器会默认帮你生成一份拷贝逻辑,这份默认逻辑就是浅拷贝。
浅拷贝行为:逐字节拷贝成员变量的值,不会单独分配堆内存
如果类里面有指针成员指向堆内存,拷贝之后,两个对象的指针,指向同一块堆地址。
带来两个致命问题:
1. 两个对象析构的时候,都会对同一个堆地址执行delete → 二次释放,程序直接崩溃
2. 一个对象修改堆里面的数据,另一个对象同步跟着被改动,数据互相干扰
浅拷贝复现代码(运行直接崩溃)
#include
#include
using namespace std;
class Person {
public:
char* name;
// 构造函数,堆上开辟内存存放名字
Person(const char* n) {
name = new char[strlen(n)+1];
strcpy(name, n);
}
// 析构,释放堆内存
~Person() {
delete[] name;
cout << "内存释放" << endl;
}
};
int main() {
Person p1("张三");
Person p2 = p1; // 调用编译器默认拷贝构造,浅拷贝!
// p1.name 和 p2.name 指向同一块堆内存
return 0;
// 函数结束,先销毁p2,释放内存;再销毁p1,再次释放同一块内存 → 崩溃
}
二、什么是深拷贝
深拷贝核心:拷贝的时候,重新开辟一块全新的堆内存,再把源数据复制过去
两个对象的内存完全独立,指针指向两块不同的堆空间,互不干扰。
析构的时候各自释放自己的内存,不会重复释放。
三、手写工业级深拷贝完整代码
#include
#include
using namespace std;
class Person {
public:
char* name;
// 构造函数
Person(const char* n) {
name = new char[strlen(n)+1];
strcpy(name, n);
}
// 深拷贝构造函数
Person(const Person& p) {
// 开辟新空间
name = new char[strlen(p.name)+1];
// 拷贝内容
strcpy(name, p.name);
cout << "执行深拷贝构造" << endl;
}
// 深拷贝赋值运算符重载
Person& operator=(const Person& p) {
// 防御自赋值:p = p 的情况,直接返回
if(this == &p) return *this;
// 先释放当前对象旧内存
delete[] name;
// 开辟新内存、拷贝数据
name = new char[strlen(p.name)+1];
strcpy(name, p.name);
return *this;
}
// 析构函数
~Person() {
delete[] name;
cout << "内存释放" << endl;
}
};
int main() {
Person p1("张三");
Person p2 = p1; // 调用我们手写的深拷贝构造
// 修改p2的名字,不会影响p1
p2.name[0] = '李';
cout << "p1名字:" << p1.name << endl; // 张三
cout << "p2名字:" << p2.name << endl; // 李四
return 0;
}
四、深浅拷贝一句话对比
浅拷贝:拷贝指针地址,多个对象共享同一份堆资源,危险,容易双重释放崩溃
深拷贝:拷贝数据内容,单独分配资源,对象内存完全独立,安全隔离
五、什么时候必须手动实现深拷贝?
只要你的自定义类里面包含指向堆内存的指针成员,就必须手动实现这三个函数,也就是C++经典三法则:
1. 拷贝构造函数
2. 赋值运算符重载 operator=
3. 析构函数
C++11之后还有移动语义,但是新手先吃透三法则。
六、常见拓展疑问
1. 为什么string、vector我们不用管深浅拷贝?
STL标准库的string、vector内部,已经封装好了深拷贝逻辑,编译器默认拷贝是安全的,不需要我们手动处理。
2. 如果类里面没有指针、没有堆资源,全部是普通内置成员(int、double),浅拷贝完全够用,不用自己写拷贝构造。
3. 移动语义和深浅拷贝是什么关系?
移动语义(move)是转移资源所有权,不拷贝数据,性能更高,是C++11为了优化大块内存拷贝新增的特性,属于深浅拷贝之后进阶内容。
七、工程避坑总结
1. 自定义类含有指针、堆资源,不要依赖编译器默认拷贝,默认一定是浅拷贝,埋下崩溃隐患;
2. 写赋值重载,第一句一定要判断自赋值,防止自己赋值自己导致提前释放内存;
3. 开发优先使用STL容器、string,尽量减少裸new、裸char*手动管理堆内存,从源头规避深浅拷贝BUG;
4. 面试手写深浅拷贝,自赋值判断是考官重点看的得分点,漏掉直接扣分。
吃透深浅拷贝,你就能解决新手阶段80%的内存随机崩溃问题。