【C++ 从基础到项目实战】C++(六):拷贝控制——浅拷贝与深拷贝,兼谈智能指针
📌 阅读时长:22分钟 | 关键词:C++、拷贝构造函数、浅拷贝、深拷贝、赋值运算符、智能指针、unique_ptr、shared_ptr
引言
上一篇文章我们学会了创建类、构造对象。但有一个容易踩坑的核心问题被有意跳过了:当一个对象被赋值给另一个,或者作为参数传递时,到底发生了什么?如果你的类里有指针成员,浅拷贝会给你带来毁灭性的行为——两个对象共用一个资源,一个销毁了,另一个就变成悬挂指针。这篇文章就来把这个坑填平。
一、默认拷贝:自带的"浅拷贝"陷阱
编译器会自动生成一个拷贝构造函数,但它只做浅拷贝——逐字节复制,指针复制的是地址,不是地址指向的内容:
classMyClass{public:int*data;MyClass(intvalue){data=newint(value);}// 编译器自动生成类似这样的拷贝构造函数:// MyClass(const MyClass &other) : data(other.data) {} ← 浅拷贝!};intmain(){MyClassobj1(10);MyClass obj2=obj1;// 浅拷贝:obj2.data 和 obj1.data 指向同一块内存std::cout<<*obj1.data<<std::endl;// 10std::cout<<*obj2.data<<std::endl;// 10// 💀 问题:两个对象指向同一块内存,析构时会 double delete!}浅拷贝的三大危害
| 问题 | 原因 | 后果 |
|---|---|---|
| 双重释放 | 两个指针指向同一块内存,析构时各自 delete 一次 | 程序崩溃 |
| 数据互相干扰 | 通过一个对象修改值,另一个"也变了" | 逻辑错误 |
| 悬挂指针 | 一个对象先销毁释放了内存,另一个还在用 | 未定义行为 |
二、深拷贝:自己动手,丰衣足食
深拷贝= 不仅拷贝指针本身,还要新分配一块内存,把内容一起拷过去:
classMyClass{public:int*data;MyClass(intvalue){data=newint(value);}// 自定义深拷贝构造函数MyClass(constMyClass&other){data=newint(*other.data);// 新分配内存,拷贝值}~MyClass(){deletedata;}};intmain(){MyClassobj1(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在声明时),还有一种情况是赋值已有对象:
classMyClass{public:int*data;MyClass(intvalue){data=newint(value);}// 深拷贝构造函数MyClass(constMyClass&other){data=newint(*other.data);}// 深拷贝赋值运算符MyClass&operator=(constMyClass&other){if(this!=&other){// ⚠️ 防止自赋值deletedata;// 先释放已有资源data=newint(*other.data);// 再分配新资源并拷贝}return*this;}~MyClass(){deletedata;}};intmain(){MyClassobj1(10),obj2(20);obj2=obj1;// 调用赋值运算符(深拷贝)}四、三五法则(Rule of Three)
如果一个类需要自定义析构函数,那么几乎一定也需要自定义拷贝构造函数和拷贝赋值运算符:
Rule of Three(C++98): 析构函数 + 拷贝构造函数 + 拷贝赋值运算符 ↓ Rule of Five(C++11): 再 + 移动构造函数 + 移动赋值运算符// 完整的三件套classDataArray{private:int*arr;intsize;public:DataArray(ints):size(s),arr(newint[s]){}DataArray(constDataArray&o):size(o.size),arr(newint[o.size]){// 拷贝构造std::copy(o.arr,o.arr+size,arr);}DataArray&operator=(constDataArray&o){// 拷贝赋值if(this!=&o){delete[]arr;size=o.size;arr=newint[size];std::copy(o.arr,o.arr+size,arr);}return*this;}~DataArray(){delete[]arr;}// 析构};五、智能指针:告别手动 delete
C++11 引入智能指针,自动管理内存,从根本上避免浅拷贝/忘记 delete 的坑。
5.1 unique_ptr:独占所有权
unique_ptr不可复制,只能移动,确保只有一个指针拥有对象:
#include<memory>intmain(){autop1=std::make_unique<int>(10);// C++14 推荐写法// std::unique_ptr<int> p2 = p1; // ❌ 不可复制!autop2=std::move(p1);// ✅ 所有权转移// p1 现在为空if(p1)std::cout<<*p1;// 不会执行elsestd::cout<<"p1 已空"<<std::endl;std::cout<<*p2<<std::endl;// 10// p2 离开作用域自动 delete}5.2 shared_ptr:共享所有权 + 引用计数
多个shared_ptr可共享同一块内存,最后一个释放时才 delete:
#include<memory>intmain(){autop1=std::make_shared<int>(10);{autop2=p1;// 引用计数 1→2std::cout<<*p2<<std::endl;// 10}// p2 离开,引用计数 2→1std::cout<<*p1<<std::endl;// 10(内存还在)}// p1 离开,引用计数 1→0,自动 delete5.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 篇。关注我,不错过后续更新。
