C++ 智能指针
智能指针:C++ 内存管理
一、为什么需要智能指针?
传统内存管理的痛点
看下面这段代码,我们明明写了 delete,但内存还是泄漏了:
void Func() {
int* array1 = new int[10];
int* array2 = new int[10]; // 如果这里抛异常...
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl; // 或者这里抛异常...
delete[] array1;
delete[] array2;
}
问题本质:异常就像“突然的跳槽”,程序执行路径被打断,后面的 delete 永远等不到执行的机会。
传统解决方案的“丑陋”
void Func() {
int* array1 = new int[10];
int* array2 = new int[10];
try {
// 业务逻辑
}
catch (...) {
delete[] array1; // 到处都要写,代码膨胀3倍
delete[] array2;
throw;
}
delete[] array1;
delete[] array2;
}
这就像出门前反复检查门窗——既繁琐又容易遗漏。
二、RAII:智能指针的“灵魂思想”
什么是 RAII?
Resource Acquisition Is Initialization —— 资源获取即初始化。
通俗点说:把资源“包养”给对象,让对象的生命周期来管理资源的一生。
template<class T>
class SmartPtr {
public:
// 构造函数:获取资源
SmartPtr(T* ptr) : _ptr(ptr) {}
// 析构函数:释放资源(自动调用,异常也不怕!)
~SmartPtr() {
cout << "释放资源:" << _ptr << endl;
delete _ptr;
}
// 像指针一样使用
T& operator*() { return *_ptr; }
T* operator->() { return _ptr; }
private:
T* _ptr;
};
RAII 的威力
void Func() {
SmartPtr<int> sp1(new int[10]); // 自动管理
SmartPtr<int> sp2(new int[10]); // 自动管理
// 哪怕这里抛异常,析构函数也会自动释放内存!
Divide(len, time);
}
// 离开作用域,sp1和sp2自动释放,就像“自动垃圾分类”
类比:RAII 就像请了一个靠谱的管家,你不用操心什么时候打扫房间(释放内存),管家(析构函数)会在你离开时自动搞定。
三、C++ 标准库智能指针全家福
对比一览表
| 智能指针 | 核心特点 | 使用场景 | |
|---|---|---|---|
auto_ptr |
拷贝时转移管理权,原指针悬空 | 强烈不建议使用 | |
unique_ptr |
独享所有权,不允许拷贝 | 不需要共享资源时 | |
shared_ptr |
引用计数,支持拷贝 | 需要多个指针共享资源 | |
weak_ptr |
不增加引用计数,配合 shared_ptr | 解决循环引用 |
1. auto_ptr:C++98 的“失败尝试”
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // 管理权转移!ap1变成空壳
ap1->_year++; // 访问空指针
形象比喻:auto_ptr 就像一个“不负责任的转让”。你把房子(资源)交给中介(ap2)后,原来的钥匙(ap1)就失效了,但你不知道,还拿着旧钥匙去开门——结果当然打不开!
结论:永远不要使用 auto_ptr,它已经被 C++11 抛弃。
2. unique_ptr:独占资源
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 编译错误!不允许拷贝
unique_ptr<Date> up3(move(up1)); // 移动语义,up1主动放弃
特点:
- 拷贝构造函数被
=delete删除 - 只能通过
std::move转移所有权 - 性能极佳,没有引用计数开销
形象比喻:unique_ptr 就像一个“独占钥匙的保险箱”。只有一个人能持有钥匙,想给别人?可以,但你得先把钥匙交出去(move),自己就不再拥有了。
适用场景:工厂函数返回值、容器中管理资源、不需要共享的场景。
3. shared_ptr:共享资源的“众筹模式”
shared_ptr<Date> sp1(new Date);
shared_ptr<Date> sp2(sp1); // 拷贝,引用计数+1
shared_ptr<Date> sp3(sp2); // 再拷贝,引用计数+1
cout << sp1.use_count() << endl; // 输出 3
sp2.reset(); // 引用计数减到2
sp3.reset(); // 引用计数减到1
// sp1离开作用域,引用计数变0,资源释放
引用计数原理
template<class T>
class shared_ptr {
private:
T* _ptr; // 指向资源
int* _pcount; // 指向堆上的引用计数(注意:不能是静态成员!)
};
为什么引用计数要在堆上
shared_ptr<Date> sp1(new Date); // 引用计数 = 1
shared_ptr<Date> sp2(sp1); // 引用计数 = 2
形象比喻:shared_ptr 就像“共享单车”。一辆车(资源)可以被多人共享使用,车上有二维码(引用计数)记录当前有多少人持有钥匙。每多一个人扫码(拷贝),计数+1;每还一辆,计数-1。当计数归零时,车就被回收维护了。
4. weak_ptr:打破循环引用的“破局者”
循环引用问题(内存泄漏的元凶)
struct ListNode {
int _data;
shared_ptr<ListNode> _next; // 问题所在
shared_ptr<ListNode> _prev; // 问题所在
~ListNode() { cout << "~ListNode()" << endl; }
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2的引用计数变成2
n2->_prev = n1; // n1的引用计数变成2
// 离开作用域:n1和n2析构,引用计数各减到1
// 但节点永远不会释放!因为_next和_prev还在互相持有!
}
循环引用的死锁图解:
n1 (引用计数=2) ←─── n2 (引用计数=2)
↓ ↑
_next ─────────────→ _prev
两个节点互相指着对方,谁也不肯先松手 → 内存泄漏!
形象比喻:这就像两个互相“甩锅”的人。A 说“B 不放手我就不放”,B 说“A 不放手我就不放”——结果两人都被困住了,谁也走不了。
解决方案:weak_ptr
struct ListNode {
int _data;
weak_ptr<ListNode> _next; // 不增加引用计数
weak_ptr<ListNode> _prev; // 不增加引用计数
};
int main() {
shared_ptr<ListNode> n1(new ListNode);
shared_ptr<ListNode> n2(new ListNode);
n1->_next = n2; // n2的引用计数还是1
n2->_prev = n1; // n1的引用计数还是1
// 离开作用域,引用计数归零,正常释放!
}
weak_ptr 的特点:
- 不参与资源管理(不实现 RAII)
- 不增加
shared_ptr的引用计数 - 不能直接访问资源,需要调用
lock()获得shared_ptr
weak_ptr<ListNode> wp = n1; // 绑定到 shared_ptr
if (auto sp = wp.lock()) { // 尝试获取 shared_ptr
sp->_data = 100; // 安全访问
} else {
cout << "资源已被释放" << endl;
}
四、删除器:定制资源的“释放方式”
问题:智能指针默认用 delete,但 new[] 怎么办?
// ❌ 错误:用 delete 释放 new[],程序崩溃
shared_ptr<Date> sp(new Date[10]);
解决方案对比
| 方案 | 代码 | 适用场景 |
|---|---|---|
| 特化版本 | shared_ptr<Date[]> sp(new Date[5]) |
数组资源 |
| 仿函数 | shared_ptr<Date> sp(new Date[5], DeleteArray<Date>()) |
需要重用的删除逻辑 |
| 函数指针 | shared_ptr<Date> sp(new Date[5], DeleteArrayFunc<Date>) |
C风格函数 |
| Lambda | shared_ptr<Date> sp(new Date[5], [](Date* p){delete[] p;}) |
简单场景,推荐! |
管理非内存资源
// 管理文件指针
shared_ptr<FILE> sp(fopen("test.txt", "r"),
[](FILE* p) {
cout << "关闭文件" << endl;
fclose(p);
}
);
// 管理数据库连接
shared_ptr<Connection> sp(new Connection("localhost"),
[](Connection* p) {
p->disconnect();
delete p;
}
);
形象比喻:删除器就像“垃圾分类指南”。不同的资源(干垃圾、湿垃圾、可回收)需要用不同的方式处理,删除器告诉智能指针具体怎么做。
五、shared_ptr 的线程安全问题
两层含义
| 层面 | 是否安全 | 说明 |
|---|---|---|
| 引用计数的操作 | 安全 | 使用原子操作或互斥锁保护 |
| 指向对象的操作 | 不安全 | 需要用户自己加锁 |
shared_ptr<AA> p(new AA);
// 多线程中同时操作 p 指向的对象,需要加锁
thread t1([&]() {
for (int i = 0; i < 100000; i++) {
shared_ptr<AA> copy(p); // 引用计数操作是安全的
// 但访问对象需要加锁
lock_guard<mutex> lock(mtx);
copy->_a1++;
}
});
形象比喻:shared_ptr 像个负责任的小区物业,它保证“入住人数统计”(引用计数)准确无误。但住户(对象)之间打架,它可管不了,需要你自己协调。
六、智能指针选择指南
开始
│
▼
是否需要共享资源?
│
├── 否 ──→ unique_ptr(独占、零开销)
│
└── 是 ──→ shared_ptr
│
▼
是否有循环引用风险?
│
├── 是 ──→ 使用 weak_ptr 打破循环
│
└── 否 ──→ 直接使用 shared_ptr
最佳实践
- 优先使用
unique_ptr:性能好,语义清晰 - 需要共享时用
shared_ptr:注意避免循环引用 - 用
weak_ptr打破循环:观察者模式、父子关系等场景 - 使用
make_shared而不是new:auto sp1 = make_shared<Date>(2024, 9, 11); // 推荐 shared_ptr<Date> sp2(new Date(2024, 9, 11)); // 不推荐 - 永远不要用
auto_ptr
七、内存泄漏:沉默的资源杀手
什么是内存泄漏?
申请的内存因为设计错误,失去了控制,再也无法释放。
类比:内存泄漏就像家里水龙头没关紧,水(内存)一滴一滴地流走。短期看不出问题,但时间长了,水费(内存)会让你崩溃!
泄漏的危害
// 普通程序:泄漏1G也没事,反正马上就结束
int main() {
char* ptr = new char[1024 * 1024 * 1024]; // 1G
return 0; // 进程结束,系统回收内存
}
// 长期运行的程序:泄漏哪怕1KB都很可怕
// 运行一个月后,内存被耗尽,程序卡死!
如何避免?
| 策略 | 方法 | 效果 |
|---|---|---|
| 事前预防 | 使用智能指针、RAII思想 | 最推荐 |
| 良好习惯 | new/delete 配对、资源管理规范 | 容易遗漏 |
| 事后检测 | VLD、Valgrind 等工具 | 上线前检查 |
八、总结:智能指针的核心要点
// 核心思想:RAII
// 用对象生命周期管理资源,资源自动释放,异常也不怕
// 三兄弟对比
unique_ptr<T> // 独生子:独占资源,不能拷贝
shared_ptr<T> // 共享资源:引用计数,自动释放
weak_ptr<T> // 弱引用:不参与管理,打破循环
// 一句话口诀
// "独用 unique,共用 shared,weak 打破循环,auto 是个坑"
记住:现代 C++ 中,裸指针只用来观察,不拥有资源。拥有资源的地方,请交给智能指针!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)