本文最后更新于 163 天前,其中的信息可能已经有所发展或是发生改变。
这是一个关于C++中浅拷贝与深拷贝区别的详细解释。
核心概念
- 拷贝:指的是创建一个新对象,并将其初始化为另一个同类型对象的副本的过程。这通常通过拷贝构造函数或拷贝赋值运算符 (
operator=) 来完成。 - 关键区别在于:当对象中包含指针成员,并且指针指向动态分配的内存(堆内存)时,是简单地复制指针的值(地址),还是为新对象重新分配内存并复制指针所指的内容。
1. 浅拷贝(Shallow Copy)
What ?
浅拷贝只复制对象的数据成员的值。对于指针成员,它仅仅复制指针本身(即内存地址),而不是指针所指向的数据。结果是,原始对象和拷贝对象中的指针指向同一块内存地址。
How ?
如果你没有自定义拷贝构造函数或拷贝赋值运算符,C++编译器会为你自动生成一个。这个默认的实现做的就是浅拷贝(按位拷贝)。
示意图:
原始对象 obj1 +----------+ +-----------------+ | pointer -|---->| Dynamic Memory | +----------+ +-----------------+ 浅拷贝后 obj2 (拷贝自 obj1) +----------+ +-----------------+ | pointer -|---->| Dynamic Memory | +----------+ +-----------------+
(obj1.pointer 和 obj2.pointer 的值相同,指向同一块内存)
问题与风险:
- 双重释放:当
obj1和obj2的生命周期结束时,它们的析构函数都会被调用,试图释放同一块内存。这会导致未定义行为,通常是程序崩溃。 - 数据意外修改:通过其中一个对象修改指针指向的数据,会直接影响另一个对象,因为它们共享数据。这往往不是程序员想要的行为。
示例代码:
cpp
#include <iostream>
class ShallowCopy {
public:
int* data;
// 构造函数
ShallowCopy(int value) {
data = new int(value);
}
// 注意:没有自定义拷贝构造函数,编译器会生成一个进行浅拷贝的版本
// 析构函数
~ShallowCopy() {
delete data; // 释放动态内存
}
};
int main() {
ShallowCopy obj1(5);
ShallowCopy obj2 = obj1; // 调用编译器生成的浅拷贝构造函数
std::cout << "obj1.data: " << *obj1.data << std::endl; // 输出 5
std::cout << "obj2.data: " << *obj2.data << std::endl; // 输出 5
// 修改 obj2 的数据,obj1 也被修改了!
*obj2.data = 10;
std::cout << "After modification:" << std::endl;
std::cout << "obj1.data: " << *obj1.data << std::endl; // 输出 10 (!)
std::cout << "obj2.data: " << *obj2.data << std::endl; // 输出 10
// main函数结束时,obj2和obj1依次析构
// obj2析构:delete data; (释放了内存)
// obj1析构:delete data; (!) 再次尝试释放同一块已释放的内存 -> CRASH!
return 0;
}
2. 深拷贝(Deep Copy)
是什么?
深拷贝不仅复制对象的数据成员的值,还会为指针成员重新分配新的内存空间,并将原始对象指针所指的内容完整地复制到这块新内存中。结果是,原始对象和拷贝对象拥有完全独立的数据副本,互不影响。
如何实现?
你必须手动自定义拷贝构造函数和拷贝赋值运算符,在其中实现为新对象分配内存和复制数据的逻辑。
示意图:
text
原始对象 obj1 +----------+ +-----------------+ | pointer -|---->| Dynamic Memory A| +----------+ +-----------------+ 深拷贝后 obj2 (拷贝自 obj1) +----------+ +-----------------+ | pointer -|---->| Dynamic Memory B| +----------+ +-----------------+
(obj2.pointer 指向一块新内存 B,其中的数据是从内存 A 复制过来的)
优点:
- 避免双重释放:每个对象管理自己独立的内存,析构时互不干扰。
- 数据独立性:修改一个对象的数据不会影响另一个对象。
示例代码:
cpp
#include <iostream>
class DeepCopy {
public:
int* data;
// 构造函数
DeepCopy(int value) {
data = new int(value);
}
// 1. 自定义深拷贝构造函数
DeepCopy(const DeepCopy& source) {
data = new int(*(source.data)); // 分配新内存,并复制值
std::cout << "Deep Copy Constructor Called" << std::endl;
}
// 2. 自定义深拷贝赋值运算符 (非常重要!)
DeepCopy& operator=(const DeepCopy& source) {
if (this == &source) { // 自我赋值检查
return *this;
}
delete data; // 释放当前对象原有的内存
data = new int(*(source.data)); // 分配新内存并复制值
std::cout << "Deep Copy Assignment Operator Called" << std::endl;
return *this;
}
// 析构函数
~DeepCopy() {
delete data; // 安全地释放自己独有的内存
}
};
int main() {
DeepCopy obj1(5);
DeepCopy obj2 = obj1; // 调用自定义的深拷贝构造函数
std::cout << "obj1.data: " << *obj1.data << std::endl; // 输出 5
std::cout << "obj2.data: " << *obj2.data << std::endl; // 输出 5
// 修改 obj2 的数据,obj1 不受影响
*obj2.data = 10;
std::cout << "After modification:" << std::endl;
std::cout << "obj1.data: " << *obj1.data << std::endl; // 输出 5
std::cout << "obj2.data: " << *obj2.data << std::endl; // 输出 10
// main函数结束时,析构顺序安全
return 0;
}
总结对比表
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 复制内容 | 基本数据类型的值、指针的地址(值) | 基本数据类型的值、指针所指内容的完整副本 |
| 内存关系 | 原对象和拷贝对象的指针成员共享同一块内存 | 原对象和拷贝对象的指针成员拥有独立的内存 |
| 实现方式 | 编译器默认生成 | 需要程序员手动自定义拷贝构造函数和拷贝赋值运算符 |
| 析构风险 | 高风险,易导致双重释放错误 | 安全,每个对象管理自己的内存 |
| 数据独立性 | 弱,一方修改会影响另一方 | 强,双方修改互不影响 |
| 性能 | 快(仅复制地址) | 慢(需要分配新内存并复制数据) |
| 适用场景 | 对象中不包含指针,或指针指向只读/共享内存等情况 | 对象中包含指针,且指针指向需要独立管理的动态分配内存 |
黄金法则(Rule of Three)
如果你的类需要自定义析构函数(通常是因为需要释放动态内存),那么它几乎总是也需要自定义拷贝构造函数和拷贝赋值运算符来实现深拷贝。这就是著名的“Rule of Three”(三法则)。
在现代C++(C++11及以后)中,这个概念扩展为“Rule of Five”,增加了移动构造函数和移动赋值运算符,但核心思想不变。
现代C++的最佳实践
为了避免手动管理内存的复杂性,应优先使用智能指针(如 std::unique_ptr, std::shared_ptr)。它们能自动管理内存生命周期,从而在很多情况下避免了手动实现深拷贝的必要性。
cpp
#include <memory>
class ModernClass {
public:
std::unique_ptr<int> data; // 使用智能指针
ModernClass(int value) : data(std::make_unique<int>(value)) {}
// 不需要自定义析构函数、拷贝构造函数和拷贝赋值运算符!
// unique_ptr 禁止拷贝,但支持移动语义,语义更清晰安全。
};
// 如果需要“拷贝”智能指针管理的对象,可以显式地进行:
// ModernClass obj2(...);
// *(obj2.data) = *(obj1.data); // 复制值,但内存仍然是独立的