深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是处理对象复制时非常重要的概念,尤其是在对象内部包含指针或引用指向动态分配的内存时。它们的主要区别在于如何处理这些内部资源。
1. 浅拷贝 (Shallow Copy)
情景: 你想把笔记本里的信息给你的朋友。
操作: 你没有把整个笔记本复制一份,而是只复制了笔记本的"目录" ,或者说,你只告诉了你的朋友:"这些信息在我的笔记本的第几页、第几行。"
结果:
-
你的朋友也"拥有"了这些信息,但实际上,他只是知道去哪里看你的笔记本。
-
问题来了:
- 如果你 在你的笔记本里修改了某个信息(比如把"苹果"改成了"香蕉"),那么你的朋友下次按照他手里的目录去看的时候,他看到的也是"香蕉"。因为你们俩的"目录"都指向同一个实际的笔记本。
- 如果你把你的笔记本丢了或者烧了,那么你的朋友手里的目录就彻底没用了,因为他再也找不到那些信息了。
- 更糟糕的是,如果你的朋友也"丢弃"了他手里的目录(他以为他丢弃的是信息),而你又丢弃了你的笔记本,那么同一份信息就被"丢弃"了两次,这会引起混乱甚至错误。
总结: 浅拷贝就像是共享一个链接。大家看到的是同一个内容,内容变了,大家看到的都变。如果源头没了,链接就失效了。(类似于我们经常做的配钥匙,而不是复制房子)
- 含义 : 浅拷贝只复制对象本身的值,而不复制对象内部指向的动态分配的资源。它只是复制了指向这些资源的指针或引用。
- 工作方式 : 当你进行浅拷贝时,新对象会拥有与原始对象相同的成员变量值。如果这些成员变量是基本类型(如
int,char,double),那么它们的值会被直接复制。但如果成员变量是指针或引用,那么新对象会复制这个指针或引用的地址,而不是它所指向的数据。 - 结果 : 原始对象和新对象会共享同一块动态分配的内存。
- 问题 :
- 修改互相影响: 如果通过新对象修改了共享内存中的数据,原始对象也会看到这些修改,反之亦然。
- 二次释放 (Double Free): 当原始对象和新对象都被销毁时,它们会尝试释放同一块内存两次,这会导致程序崩溃或未定义行为。
例子 (C++ 伪代码):
cpp
class MyClass {
public:
int* data;
MyClass(int val) {
data = new int(val); // 动态分配内存
}
// 默认的拷贝构造函数或赋值运算符会执行浅拷贝
// MyClass(const MyClass& other) {
// data = other.data; // 只是复制了指针地址
// }
~MyClass() {
delete data; // 析构时释放内存
}
};
MyClass obj1(10); // obj1.data 指向一块内存,里面是 10
MyClass obj2 = obj1; // 浅拷贝,obj2.data 和 obj1.data 指向同一块内存
// 此时 obj1.data 和 obj2.data 都指向同一个地址
// 如果通过 obj2 修改数据:
*obj2.data = 20;
// 那么 *obj1.data 也会变成 20
// 当 obj1 和 obj2 销毁时,会尝试对同一块内存 delete 两次,导致错误。
2. 深拷贝 (Deep Copy)
情景: 你想把笔记本里的信息给你的朋友。
操作: 你不是只告诉朋友目录,而是把你的笔记本里的所有信息都一字不差地抄写了一份 ,然后用一个全新的笔记本把这些抄写出来的信息装好,再把这个新笔记本给了你的朋友。
结果:
-
现在,你有一个笔记本,你的朋友也有一个笔记本。
-
这两个笔记本里的信息内容一模一样,但它们是完全独立的两份信息。
-
好处:
- 如果你在你的笔记本里修改了某个信息(比如把"苹果"改成了"香蕉"),你的朋友的笔记本里仍然是原来的"苹果"。你们互不影响。
- 如果你把你的笔记本丢了或者烧了,你的朋友的笔记本仍然完好无损,他可以继续看他的信息。
- 你们各自管理自己的笔记本和信息,不会互相干扰,也不会出现"丢弃两次"的问题。
总结: 深拷贝就像是复制一份实体文件。大家都有自己的一份,互不影响。
- 含义 : 深拷贝不仅复制对象本身的值,还会为对象内部指向的动态分配的资源重新分配内存,并将原始对象中的数据复制到新分配的内存中。
- 工作方式: 当你进行深拷贝时,新对象会为所有动态分配的资源创建独立的副本。这意味着如果原始对象有一个指针指向一块内存,新对象会分配一块新的内存,并将原始指针指向的数据复制到这块新内存中,然后新对象的指针指向这块新内存。
- 结果 : 原始对象和新对象拥有完全独立的资源。
- 优点 :
- 独立性: 修改其中一个对象不会影响另一个对象。
- 安全: 不会发生二次释放的问题,因为每个对象都管理自己的资源。
例子 (C++ 伪代码):
cpp
class MyClass {
public:
int* data;
MyClass(int val) {
data = new int(val);
}
// 深拷贝构造函数
MyClass(const MyClass& other) {
data = new int(*other.data); // 为新对象重新分配内存,并复制数据
}
// 深拷贝赋值运算符
MyClass& operator=(const MyClass& other) {
if (this != &other) { // 防止自赋值
delete data; // 释放旧资源
data = new int(*other.data); // 分配新资源并复制数据
}
return *this;
}
~MyClass() {
delete data;
}
};
MyClass obj1(10); // obj1.data 指向一块内存,里面是 10
MyClass obj2 = obj1; // 深拷贝,obj2.data 指向另一块内存,里面也是 10
// 此时 obj1.data 和 obj2.data 指向不同的地址
// 如果通过 obj2 修改数据:
*obj2.data = 20;
// 那么 *obj1.data 仍然是 10,不受影响。
// 当 obj1 和 obj2 销毁时,它们各自释放自己的内存,不会冲突。
区别总结
| 特性 | 浅拷贝 (Shallow Copy) | 深拷贝 (Deep Copy) |
|---|---|---|
| 复制内容 | 复制对象本身的值和内部指针/引用的地址。 | 复制对象本身的值,并为内部指针/引用指向的资源重新分配内存并复制数据。 |
| 资源共享 | 原始对象和新对象共享动态分配的资源。 | 原始对象和新对象拥有独立的动态分配资源。 |
| 修改影响 | 修改其中一个对象会影响另一个对象。 | 修改其中一个对象不会影响另一个对象。 |
| 内存释放 | 容易导致二次释放问题。 | 安全,每个对象管理自己的资源。 |
| 实现方式 | 默认的拷贝构造函数/赋值运算符通常是浅拷贝。 | 需要手动实现拷贝构造函数和赋值运算符,以处理动态资源。 |
| 效率 | 相对高效,因为只复制指针地址。 | 相对低效,因为需要分配新内存并复制所有数据。 |
何时使用
- 浅拷贝: 当对象不包含指向动态分配内存的指针或引用时(例如,只包含基本类型或不涉及资源管理的复合类型),或者你明确希望两个对象共享同一份数据时。
- 深拷贝: 当对象包含指向动态分配内存的指针或引用,并且你需要确保新对象拥有自己独立的数据副本时。这是处理资源管理(如文件句柄、网络连接、动态数组等)的类通常需要的方式。
在 C++ 中,如果你的类管理着动态内存或其他资源,通常需要遵循"三/五/零法则"(Rule of Three/Five/Zero),即如果需要自定义析构函数,那么通常也需要自定义拷贝构造函数和拷贝赋值运算符(或者移动构造函数和移动赋值运算符),以确保正确的深拷贝行为,避免资源泄漏和二次释放问题。
简单来说:
- 浅拷贝 :只复制"地址"或"引用",就像复制了一份地图 。两份地图都指向同一个地方。这个地方变了,两份地图看到的结果都变。如果这个地方被毁了,两份地图都失效。
- 深拷贝 :复制了"地址"指向的实际内容 ,就像复制了一份一模一样的地方 。两份地图指向的是两个独立但内容相同的地方。一个地方变了,另一个地方不受影响。一个地方被毁了,另一个地方依然存在。