【C++】C++11:智能指针
目录
一、为什么需要智能指针?
二、RAII与智能指针的设计思想
2.1 RAII资源管理思想
2.2 智能指针的基本设计
三、C++标准库智能指针的使用
3.1 auto_ptr(已废弃)
3.2 unique_ptr
3.3 shared_ptr
3.4 weak_ptr
3.5 删除器的使用
3.6 make_shared
四、智能指针的原理与模拟实现
4.1 auto_ptr的实现原理
4.2 unique_ptr的实现原理
4.3 shared_ptr的实现原理
4.4 weak_ptr的实现原理
五、shared_ptr的循环引用问题
5.1 循环引用的产生
5.2 weak_ptr解决方案
六、shared_ptr的线程安全问题
七、内存泄漏与智能指针
7.1 内存泄漏的概念与危害
7.2 如何避免内存泄漏
八、总结
一、为什么需要智能指针?
在传统的C++编程中,我们使用new和delete手动管理内存,但在异常处理的场景下,很容易出现内存泄漏问题。
问题示例:异常导致的内存泄漏
double Divide(int a, int b)
{if (b == 0){throw "Divide by zero condition!";}return (double)a / (double)b;
}void Func()
{int* array1 = new int[10];int* array2 = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){// 如果new array2时抛异常,array1无法释放// 如果Divide抛异常,两个array都无法释放delete[] array1;delete[] array2;throw;}delete[] array1;delete[] array2;
}
问题分析:
- 如果
new array2时抛异常,array1无法释放 - 如果
Divide函数抛异常,两个array都无法释放 - 需要复杂的异常处理逻辑来保证资源释放
解决方案:使用智能指针,利用RAII思想自动管理资源。
二、RAII与智能指针的设计思想
2.1 RAII资源管理思想
RAII(Resource Acquisition Is Initialization)是一种重要的资源管理思想:
- 核心概念:资源获取即初始化,利用对象的生命周期管理资源
- 资源类型:内存、文件句柄、网络连接、互斥锁等
- 工作机制:
- 构造函数中获取资源
- 析构函数中释放资源
- 资源在对象生命周期内保持有效
2.2 智能指针的基本设计
智能指针基于RAII思想,并重载运算符模拟指针行为:
template<class T>
class SmartPtr
{
public:// RAII:构造函数获取资源SmartPtr(T* ptr) : _ptr(ptr) {}// RAII:析构函数释放资源~SmartPtr(){cout << "delete[] " << _ptr << endl;delete[] _ptr;}// 重载运算符,模拟指针行为T& operator*() { return *_ptr; }T* operator->() { return _ptr; }T& operator[](size_t i) { return _ptr[i]; }private:T* _ptr;
};// 使用示例
void Func()
{SmartPtr<int> sp1 = new int[10]; // 构造时获取资源SmartPtr<int> sp2 = new int[10];for (size_t i = 0; i < 10; i++){sp1[i] = sp2[i] = i; // 像指针一样使用}// 函数结束时自动调用析构函数释放资源
}
三、C++标准库智能指针的使用
C++11在<memory>头文件中提供了多种智能指针:
3.1 auto_ptr(已废弃)
特点:拷贝时转移资源管理权
问题:被拷贝对象悬空,容易导致访问错误
auto_ptr<Date> ap1(new Date);
auto_ptr<Date> ap2(ap1); // ap1变为空指针
// ap1->_year++; // 错误!ap1已悬空
注意:C++11已废弃auto_ptr,不建议使用
3.2 unique_ptr
特点:独占所有权,不支持拷贝,只支持移动
unique_ptr<Date> up1(new Date);
// unique_ptr<Date> up2(up1); // 错误!不支持拷贝
unique_ptr<Date> up3(std::move(up1)); // 支持移动,up1变为空指针
适用场景:不需要共享所有权的资源管理
3.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; // 输出:3sp1->_year++; // 所有shared_ptr共享同一资源
cout << sp2->_year << endl; // 输出相同值
3.4 weak_ptr
特点:不支持RAII,即不管理资源生命周期,不增加引用计数
shared_ptr<string> sp1(new string("hello"));
weak_ptr<string> wp = sp1; // 不增加引用计数cout << wp.use_count() << endl; // 输出:1
cout << wp.expired() << endl; // 检查是否过期if (auto sp2 = wp.lock()) { // 尝试获取shared_ptrcout << *sp2 << endl; // 安全访问资源
}
3.5 删除器的使用
默认情况:智能指针使用delete释放资源
定制删除器:支持自定义资源释放方式
// 函数指针删除器
template<class T>
void DeleteArrayFunc(T* ptr) { delete[] ptr; }// 仿函数删除器
template<class T>
class DeleteArray {
public:void operator()(T* ptr) { delete[] ptr; }
};// lambda表达式删除器
auto delArrObj = [](Date* ptr) { delete[] ptr; };// 使用删除器
unique_ptr<Date, void(*)(Date*)> up1(new Date[5], DeleteArrayFunc<Date>);
shared_ptr<Date> sp1(new Date[5], DeleteArray<Date>());// 管理文件资源
shared_ptr<FILE> sp2(fopen("test.txt", "r"), [](FILE* ptr) { cout << "fclose:" << ptr << endl; fclose(ptr); });
特殊语法:对于new[]的简化处理
unique_ptr<Date[]> up2(new Date[5]); // 自动使用delete[]
shared_ptr<Date[]> sp2(new Date[5]); // 自动使用delete[]
3.6 make_shared
// 传统方式
shared_ptr<Date> sp1(new Date(2024, 9, 11));// make_shared方式
shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);
auto sp3 = make_shared<Date>(2024, 9, 11); // 更简洁
优势:
- 内存分配优化:对象和控制块( shared_ptr 内部用来管理对象生命周期和存储元数据的数据结构,包含引用计数、删除器等信息)一次分配
- 异常安全:避免内存泄漏
- 代码简洁
四、智能指针的原理与模拟实现
4.1 auto_ptr的实现原理
核心思想:拷贝时转移资源管理权
template<class T>
class auto_ptr {
public:auto_ptr(T* ptr) : _ptr(ptr) {}auto_ptr(auto_ptr<T>& sp) : _ptr(sp._ptr) {sp._ptr = nullptr; // 管理权转移}~auto_ptr() {if (_ptr) {delete _ptr;}}private:T* _ptr;
};
缺陷:被拷贝对象悬空,容易导致错误
4.2 unique_ptr的实现原理
核心思想:禁止拷贝,只支持移动
template<class T>
class unique_ptr {
public:explicit unique_ptr(T* ptr) : _ptr(ptr) {}~unique_ptr() {if (_ptr) {delete _ptr;}}// 删除拷贝构造和拷贝赋值unique_ptr(const unique_ptr<T>& sp) = delete;unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;// 支持移动语义unique_ptr(unique_ptr<T>&& sp) : _ptr(sp._ptr) {sp._ptr = nullptr;}private:T* _ptr;
};
4.3 shared_ptr的实现原理
核心思想:引用计数管理共享所有权
- 每一份被管理的资源都有一个引用计数,所以引用计数用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new一个引用计数出来。
- 多个shared_ptr指向资源时就
++引用计数,shared_ptr对象析构时就--引用计数,引用计数减到0时则析构资源。

template<class T>
class shared_ptr {
public:explicit shared_ptr(T* ptr = nullptr) : _ptr(ptr), _pcount(new int(1)) {}shared_ptr(const shared_ptr<T>& sp) : _ptr(sp._ptr), _pcount(sp._pcount) {++(*_pcount); // 引用计数增加}void release() {if (--(*_pcount) == 0) { // 引用计数减少delete _ptr; // 释放资源delete _pcount; // 释放引用计数_ptr = nullptr;_pcount = nullptr;}}~shared_ptr() {release();}int use_count() const { return *_pcount; }private:T* _ptr;int* _pcount; // 引用计数在堆上
};
4.4 weak_ptr的实现原理
核心思想:不参与引用计数管理
template<class T>
class weak_ptr {
public:weak_ptr() : _ptr(nullptr) {}weak_ptr(const shared_ptr<T>& sp) : _ptr(sp.get()) {}// 不增加引用计数
private:T* _ptr;
};
五、shared_ptr的循环引用问题
5.1 循环引用的产生
问题场景:双向链表或父子对象相互引用
- 如下图所示,n1和n2析构后,管理两个节点的引用计数减到1
- 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
- _next什么时候析构呢,_next是左边节点的成员,左边节点释放,_next就析构了。
- 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
- _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
- 至此逻辑上形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
- 把ListNode结构体中的_next和_prev改成weak_ptr,weak_ptr绑定到shared_ptr时不会增加它的引用计数,_next和_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

struct ListNode {int _data;shared_ptr<ListNode> _next;shared_ptr<ListNode> _prev;~ListNode() { cout << "~ListNode()" << endl; }
};void test_cycle() {shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);n1->_next = n2; // n2引用计数变为2n2->_prev = n1; // n1引用计数变为2// n1和n2析构后,引用计数都变为1// 相互等待对方释放,导致内存泄漏
}
5.2 weak_ptr解决方案
struct ListNode {int _data;weak_ptr<ListNode> _next; // 使用weak_ptrweak_ptr<ListNode> _prev; // 使用weak_ptr~ListNode() { cout << "~ListNode()" << endl; }
};void test_solution() {shared_ptr<ListNode> n1(new ListNode);shared_ptr<ListNode> n2(new ListNode);n1->_next = n2; // n2引用计数仍为1n2->_prev = n1; // n1引用计数仍为1// n1和n2析构后,引用计数都变为0,正常释放
}
六、shared_ptr的线程安全问题
- shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
// 使用原子操作保证引用计数线程安全
template<class T>
class shared_ptr {
private:T* _ptr;atomic<int>* _pcount; // 原子引用计数
};// 或者使用互斥锁
mutex mtx;
auto func = [&]() {for (size_t i = 0; i < n; ++i) {shared_ptr<AA> copy(p);{unique_lock<mutex> lk(mtx);copy->_a1++;copy->_a2++;}}
};
- shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
// 一个简单的计数器类
class Counter {
public:Counter() : count(0) {}void increment() {std::lock_guard<std::mutex> lock(mtx); // 加锁,lock_guard出作用域析构自动解锁count++;}int getCount() const {std::lock_guard<std::mutex> lock(mtx); return count;}private:mutable std::mutex mtx; int count;
};void threadFunction(std::shared_ptr<Counter> counter) {for (int i = 0; i < 10000; ++i) {counter->increment();}
}int main() {std::shared_ptr<Counter> counter = std::make_shared<Counter>();// 创建多个线程来增加计数器std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(threadFunction, counter);}// 等待所有线程完成for (auto& th : threads) {th.join();}// 输出最终的计数值std::cout << "Final count: " << counter->getCount() << std::endl;return 0;
}
七、内存泄漏与智能指针
7.1 内存泄漏的概念与危害
内存泄漏:程序未能释放不再使用的内存
危害程度:
- 短期程序:影响较小,进程结束自动回收
- 长期运行程序:操作系统、服务端程序等,内存泄漏会导致:
- 可用内存不断减少
- 系统响应变慢
- 最终卡死或崩溃
// 短期程序内存泄漏示例
int main() {char* ptr = new char[1024 * 1024 * 1024]; // 1GB内存cout << (void*)ptr << endl;// 忘记delete,但程序结束自动回收return 0;
}
7.2 如何避免内存泄漏
预防措施:
- 编码规范:申请与释放匹配
- 智能指针:RAII自动管理
- 代码审查:定期检查资源管理
- 检测工具:Valgrind、dmalloc等
最佳实践:
// 不好的做法
void bad_func() {int* ptr = new int[100];// ... 可能抛异常delete[] ptr; // 异常时无法执行
}// 好的做法
void good_func() {unique_ptr<int[]> ptr(new int[100]);// ... 异常时自动释放// 或者使用vector更好vector<int> arr(100);
}
八、总结
智能指针核心要点
| 智能指针类型 | 所有权语义 | 拷贝语义 | 适用场景 |
|---|---|---|---|
unique_ptr | 独占所有权 | 禁止拷贝,只支持移动 | 不需要共享的资源 |
shared_ptr | 共享所有权 | 支持拷贝,引用计数 | 需要共享的资源 |
weak_ptr | 不拥有所有权 | 不增加引用计数 | 解决循环引用 |
使用建议
- 默认选择:优先使用
unique_ptr - 共享资源:需要共享时使用
shared_ptr - 循环引用:使用
weak_ptr打破循环 - 数组管理:使用
unique_ptr<T[]>或定制删除器 - 性能考虑:使用
make_shared提高效率
重要注意事项
// 正确使用示例
auto sp1 = make_shared<Date>(2024, 9, 11);
unique_ptr<Date[]> up1(new Date[5]);
weak_ptr<Date> wp1 = sp;
weak_ptr<Date> wp2(sp);
shared_ptr<Date> sp2(new Date(2024, 9, 11));
unique_ptr<Date> up2(new Date(2024, 9, 11)); // 错误使用示例:智能指针的构造函数是explicit构造函数!
// shared_ptr<Date> sp2 = new Date(2024, 9, 11);
// unique_ptr<Date> up2 = new Date(2024, 9, 11); // weak_ptr 的情况比较特殊,它不能直接接管原始指针,必须从 shared_ptr 构造
// weak_ptr<Date> wp1 = new Date(2024, 9, 11);
// weak_ptr<Date> wp2(new Date(2024, 9, 11));
