📌 阅读时长:22分钟 | 关键词:C++、拷贝构造函数、浅拷贝、深拷贝、赋值运算符、智能指针、unique_ptr、shared_ptr
引言
上一篇文章我们学会了创建类、构造对象。但有一个容易踩坑的核心问题被有意跳过了:当一个对象被赋值给另一个,或者作为参数传递时,到底发生了什么? 如果你的类里有指针成员,浅拷贝会给你带来毁灭性的行为------两个对象共用一个资源,一个销毁了,另一个就变成悬挂指针。这篇文章就来把这个坑填平。
一、默认拷贝:自带的"浅拷贝"陷阱
编译器会自动生成一个拷贝构造函数 ,但它只做浅拷贝 ------逐字节复制,指针复制的是地址,不是地址指向的内容:
cpp
class MyClass {
public:
int *data;
MyClass(int value) {
data = new int(value);
}
// 编译器自动生成类似这样的拷贝构造函数:
// MyClass(const MyClass &other) : data(other.data) {} ← 浅拷贝!
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 浅拷贝:obj2.data 和 obj1.data 指向同一块内存
std::cout << *obj1.data << std::endl; // 10
std::cout << *obj2.data << std::endl; // 10
// 💀 问题:两个对象指向同一块内存,析构时会 double delete!
}
浅拷贝的三大危害
| 问题 | 原因 | 后果 |
|---|---|---|
| 双重释放 | 两个指针指向同一块内存,析构时各自 delete 一次 | 程序崩溃 |
| 数据互相干扰 | 通过一个对象修改值,另一个"也变了" | 逻辑错误 |
| 悬挂指针 | 一个对象先销毁释放了内存,另一个还在用 | 未定义行为 |
二、深拷贝:自己动手,丰衣足食
深拷贝 = 不仅拷贝指针本身,还要新分配一块内存,把内容一起拷过去:
cpp
class MyClass {
public:
int *data;
MyClass(int value) {
data = new int(value);
}
// 自定义深拷贝构造函数
MyClass(const MyClass &other) {
data = new int(*other.data); // 新分配内存,拷贝值
}
~MyClass() {
delete data;
}
};
int main() {
MyClass obj1(10);
MyClass obj2 = obj1; // 深拷贝:各自拥有独立的内存
*obj1.data = 20;
std::cout << *obj2.data << std::endl; // 仍然 = 10,互不影响 ✅
}
浅拷贝 vs 深拷贝图解
浅拷贝:
obj1.data ──→ [内存块: 10] ←── obj2.data (两个指针指向同一块)
深拷贝:
obj1.data ──→ [内存块: 10]
obj2.data ──→ [内存块: 10] (各自独立)
三、拷贝赋值运算符
除了拷贝构造(obj2 = obj1 在声明时),还有一种情况是赋值已有对象:
cpp
class MyClass {
public:
int *data;
MyClass(int value) { data = new int(value); }
// 深拷贝构造函数
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; }
};
int main() {
MyClass obj1(10), obj2(20);
obj2 = obj1; // 调用赋值运算符(深拷贝)
}
四、三五法则(Rule of Three)
如果一个类需要自定义析构函数 ,那么几乎一定也需要自定义拷贝构造函数 和拷贝赋值运算符:
Rule of Three(C++98):
析构函数 + 拷贝构造函数 + 拷贝赋值运算符
↓
Rule of Five(C++11):
再 + 移动构造函数 + 移动赋值运算符
cpp
// 完整的三件套
class DataArray {
private:
int *arr;
int size;
public:
DataArray(int s) : size(s), arr(new int[s]) {}
DataArray(const DataArray &o) : size(o.size), arr(new int[o.size]) { // 拷贝构造
std::copy(o.arr, o.arr + size, arr);
}
DataArray &operator=(const DataArray &o) { // 拷贝赋值
if (this != &o) {
delete[] arr;
size = o.size;
arr = new int[size];
std::copy(o.arr, o.arr + size, arr);
}
return *this;
}
~DataArray() { delete[] arr; } // 析构
};
五、智能指针:告别手动 delete
C++11 引入智能指针,自动管理内存,从根本上避免浅拷贝/忘记 delete 的坑。
5.1 unique_ptr:独占所有权
unique_ptr 不可复制,只能移动,确保只有一个指针拥有对象:
cpp
#include <memory>
int main() {
auto p1 = std::make_unique<int>(10); // C++14 推荐写法
// std::unique_ptr<int> p2 = p1; // ❌ 不可复制!
auto p2 = std::move(p1); // ✅ 所有权转移
// p1 现在为空
if (p1) std::cout << *p1; // 不会执行
else std::cout << "p1 已空" << std::endl;
std::cout << *p2 << std::endl; // 10
// p2 离开作用域自动 delete
}
5.2 shared_ptr:共享所有权 + 引用计数
多个 shared_ptr 可共享同一块内存,最后一个释放时才 delete:
cpp
#include <memory>
int main() {
auto p1 = std::make_shared<int>(10);
{
auto p2 = p1; // 引用计数 1→2
std::cout << *p2 << std::endl; // 10
} // p2 离开,引用计数 2→1
std::cout << *p1 << std::endl; // 10(内存还在)
} // p1 离开,引用计数 1→0,自动 delete
5.3 裸指针 vs 智能指针
| 特性 | 裸指针 T* |
unique_ptr |
shared_ptr |
|---|---|---|---|
| 所有权 | 无约束 | 独占 | 共享 |
| 复制 | ✅ | ❌(只能移动) | ✅(引用计数+1) |
| 自动释放 | ❌ 需手动 delete | ✅ | ✅ |
| 循环引用 | 无 | 无 | ⚠️ 需 weak_ptr 解决 |
| 性能开销 | 无 | 极小 | 引用计数有额外开销 |
💡 日常开发原则:能用 unique_ptr 就别用 shared_ptr,能用智能指针就别用裸指针。
小结
| 序号 | 知识点 | 一句话总结 |
|---|---|---|
| 1 | 浅拷贝 | 只拷地址不拷内容,两个对象共用内存→双重释放 |
| 2 | 深拷贝 | 新分配内存+拷贝内容,各自独立 |
| 3 | 拷贝赋值 | operator= 实现深赋值,注意自赋值检查 |
| 4 | 三五法则 | 自定义析构时,记得也自定义拷贝构造和赋值 |
| 5 | unique_ptr | 独占所有权,不可复制,移动转移 |
| 6 | shared_ptr | 共享所有权,引用计数为0时自动释放 |
下一篇文章,我们将进入面向对象最强大的特性------继承与多态:如何复用代码、如何用虚函数实现"同一个接口,不同的行为"。
本文是「C++ 从基础到项目实战」系列的第 6 篇。关注我,不错过后续更新。