C++11 : 智能指针
目录
- C++11 : 智能指针
- 引言
- 1. 智能指针的使用场景分析
- 2. RALL和智能指针的设计思路
- 3. C++标准库智能指针的使用
- 4. 智能指针的原理
- 5. `shared_ptr`和`weak_ptr`
- 5.1 shared_ptr循环引用问题
- 5.2 `weak_ptr`
- 6. `shared_ptr`的线程安全问题
- 7. C++11和`boost`中智能指针的关系
- 8. 内存泄露
引言
智能指针不仅能够有效避免内存泄漏,还能简化代码逻辑,提升程序的健壮性和可维护性。C++11标准库提供了多种智能指针类型,包括unique_ptr
、shared_ptr
和weak_ptr
,分别适用于不同的资源管理场景。
本文将详细介绍智能指针的使用场景、设计原理、标准库实现以及常见问题(如循环引用和线程安全),并通过丰富的代码示例帮助读者深入理解其工作机制和最佳实践。
1. 智能指针的使用场景分析
-
在下面的程序中我们可以看到,使用
new
分配内存后虽然调用了delete
,但由于异常抛出导致后续的delete
未能执行,从而引发内存泄漏。为了避免这种情况,我们需要在new
之后捕获异常,并在捕获到异常时释放内存后再重新抛出异常。然而,new
本身也可能抛出异常,如果连续多个new
或后续操作(如Divide
)都可能抛出异常,手动处理会非常麻烦。而将智能指针应用于这种场景,可以极大地简化问题的处理。 -
代码示例:
#include <iostream> #include <exception> // 标准异常头文件 using namespace std;/*** @brief 执行整数除法运算,返回浮点数结果* @param a 被除数* @param b 除数* @return double 除法结果* @throws const char* 当除数为0时抛出字符串异常*/ double Divide(int a, int b) {// 检查除数是否为0if (b == 0){// 抛出字符串类型的异常,表示除零错误throw "Divide by zero condition!";}else{// 将两个整数转换为double类型后执行除法return (double)a / (double)b;} }/*** @brief 演示异常处理和资源管理的函数* @note 该函数展示了原始指针的资源管理问题*/ void Func() {// 动态分配两个整型数组int* array1 = new int[10]; // 可能抛出bad_alloc异常int* array2 = new int[10]; // 可能抛出bad_alloc异常try{// 获取用户输入int len, time;cin >> len >> time;// 调用Divide函数并输出结果cout << Divide(len, time) << endl;}catch (...) // 捕获所有类型的异常{// 在异常发生时释放已分配的内存cout << "delete []" << array1 << endl;cout << "delete []" << array2 << endl;delete[] array1;delete[] array2;// 重新抛出捕获到的异常,由上层调用者处理throw; }// 正常执行时的资源释放cout << "delete []" << array1 << endl;delete[] array1;cout << "delete []" << array2 << endl;delete[] array2; }/*** @brief 主函数,程序的入口* @return int 程序退出码*/ int main() {try{// 调用可能抛出异常的Func函数Func();}catch (const char* errmsg) // 捕获字符串类型的异常{// 处理Divide函数抛出的字符串异常cout << errmsg << endl;}catch (const exception& e) // 捕获标准异常{// 处理标准库异常cout << e.what() << endl;}catch (...) // 捕获所有其他未知异常{// 处理未被上述catch块捕获的异常cout << "未知异常" << endl;}return 0; }
2. RALL和智能指针的设计思路
-
RAII(Resource Acquisition Is Initialization)资源申请立即初始化 是一种管理资源的类的设计思想,本质是利用对象生命周期来管理获取到的动态资源(如内存、文件指针、网络连接、互斥锁等),避免资源泄漏。RAII 在获取资源时把资源委托给一个对象,控制对资源的访问,资源在对象的生命周期内始终保持有效,最后在对象析构时释放资源,确保资源正常释放,避免泄漏问题。
-
智能指针 类除了满足 RAII 的设计思路,还需方便资源访问,因此会像迭代器类一样重载
operator*
、operator->
、operator[]
等运算符,以简化资源操作。 -
代码示例:
#include <iostream> using namespace std;/*** @brief 智能指针模板类,实现RAII(资源获取即初始化)机制* @tparam T 指针指向的数据类型*/ template<class T> class SmartPtr { public:/*** @brief 构造函数,获取资源所有权* @param ptr 需要管理的原始指针*/SmartPtr(T* ptr):_ptr(ptr) // 初始化成员指针{}/*** @brief 析构函数,自动释放资源*/~SmartPtr() {cout << "delete[] " << _ptr << endl; // 打印释放信息delete[] _ptr; // 释放数组内存}/*** @brief 重载解引用运算符** @return 返回指针指向的对象的引用*/T& operator*() {return *_ptr; // 返回指针指向的对象}/*** @brief 重载箭头运算符->* @return 返回原始指针*/T* operator->() {return _ptr; // 返回原始指针}/*** @brief 重载下标运算符[]* @param i 数组索引* @return 返回数组元素的引用*/T& operator[](size_t i) {return _ptr[i]; // 返回数组元素}private:T* _ptr; // 管理的原始指针 };/*** @brief 除法函数* @param a 被除数* @param b 除数* @return 返回除法结果(double类型)* @throw 当除数为0时抛出异常*/ double Divide(int a, int b) {// 检查除数是否为0if (b == 0) {throw "Divide by zero condition!"; // 抛出字符串异常} else {return (double)a / (double)b; // 执行除法运算} }/*** @brief 演示函数,展示智能指针和异常处理的使用*/ void Func() {// 使用智能指针管理动态分配的数组// 当函数结束时,智能指针会自动释放内存SmartPtr<int> sp1 = new int[10]; // 管理10个int的数组SmartPtr<int> sp2 = new int[10]; // 管理另一个10个int的数组// 初始化数组for (size_t i = 0; i < 10; i++) {sp1[i] = sp2[i] = i; // 使用重载的[]运算符赋值}int len, time;cin >> len >> time; // 输入两个整数cout << Divide(len, time) << endl; // 调用除法函数并输出结果 }/*** @brief 主函数*/ int main() {try {Func(); // 调用演示函数}catch (const char* errmsg) { // 捕获字符串异常cout << errmsg << endl;}catch (const exception& e) { // 捕获标准异常cout << e.what() << endl;}catch (...) { // 捕获所有其他异常cout << "未知异常" << endl;}return 0; // 程序正常结束 }
3. C++标准库智能指针的使用
-
C++标准库中的智能指针都在
<memory>
头文件中,包含该头文件即可使用。智能指针有多种(除weak_ptr
外均符合 RAII 原则和指针式访问行为),其核心区别在于解决拷贝时的设计思路不同。 -
auto_ptr
(C++98):拷贝时转移资源管理权,导致被拷贝对象悬空,易引发访问错误。C++11 后已废弃,强烈不建议使用。 -
unique_ptr
(C++11):禁止拷贝,仅支持移动,适用于无需拷贝的场景。 -
shared_ptr
(C++11):支持拷贝和移动,采用引用计数实现,适用于需共享资源的场景。 -
weak_ptr
(C++11):不管理资源生命周期,专为解决shared_ptr
循环引用导致的内存泄漏问题而设计。 -
智能指针默认通过
delete
释放资源,若管理非new
分配的资源需自定义删除器(可调用对象)。unique_ptr
和shared_ptr
特化了[]
版本以支持动态数组(如unique_ptr<Date[]>(new Date[5])
)。 -
其他特性:
make_shared<Type>(args...)
:shared_ptr
还可以用初始化资源对象的值直接构造资源对象,效率优于显式new
。operator bool
:允许通过if(ptr)
判断智能指针是否为空。如未管理资源则返回false
,否则返回ture
。unique_ptr
和shared_ptr
构造函数均标记为explicit
,禁止普通指针隐式转换成智能指针。
-
代码示例:
#include <iostream> #include <memory> // 包含智能指针的头文件 using namespace std;// 定义一个简单的日期类 struct Date {int _year; // 年int _month; // 月int _day; // 日// 构造函数,带默认参数(默认初始化为1年1月1日)Date(int year = 1, int month = 1, int day = 1):_year(year) // 初始化年,_month(month) // 初始化月,_day(day) // 初始化日{}// 析构函数,输出提示信息~Date(){cout << "~Date()" << endl;} };int main() {// 1. auto_ptr 示例(C++98中的智能指针,现已废弃)auto_ptr<Date> ap1(new Date); // 创建auto_ptr管理Date对象// 拷贝时管理权限转移,被拷贝对象ap1变为空指针(这是auto_ptr的特性)auto_ptr<Date> ap2(ap1); // ap1的所有权转移给ap2// 危险操作:ap1现在已经是空指针,访问会导致未定义行为// ap1->_year++; // 如果取消注释,程序可能会崩溃// 2. unique_ptr 示例(C++11引入,独占所有权的智能指针)unique_ptr<Date> up1(new Date); // 创建unique_ptr管理Date对象// unique_ptr不支持拷贝构造(因为要保证独占所有权)// unique_ptr<Date> up2(up1); // 这行会编译错误// 支持移动语义(所有权转移),但转移后up1变为空指针unique_ptr<Date> up3(move(up1)); // 使用move将up1的所有权转移给up3// 3. shared_ptr 示例(C++11引入,共享所有权的智能指针)shared_ptr<Date> sp1(new Date); // 创建shared_ptr管理Date对象// 支持拷贝构造(引用计数增加)shared_ptr<Date> sp2(sp1); // sp2和sp1共享所有权shared_ptr<Date> sp3(sp2); // sp3也加入共享// 输出当前引用计数(应该是3,因为sp1、sp2、sp3共享同一个对象)cout << sp1.use_count() << endl; // 输出: 3// 通过智能指针访问成员(使用->操作符)sp1->_year++; // 修改年份// 输出验证所有共享指针看到的是同一个对象cout << sp1->_year << endl; // 输出修改后的年份cout << sp2->_year << endl; // 同上cout << sp3->_year << endl; // 同上// shared_ptr也支持移动语义(所有权转移)shared_ptr<Date> sp4(move(sp1)); // 将sp1的所有权转移给sp4// 此时sp1变为空指针,引用计数减少// sp2、sp3、sp4仍然共享对象return 0;// 程序结束时,所有智能指针会自动释放它们管理的对象// 释放顺序与创建顺序相反,会调用Date的析构函数 }
#include <iostream> #include <memory> // 包含智能指针的头文件 using namespace std;// 假设的Date类(原代码中未定义,这里补充以便理解) class Date {// 日期类的具体实现(示例中未使用具体成员) };// 方案1:函数模板形式的删除器(用于delete[]动态数组) template<class T> void DeleteArrayFunc(T* ptr) {delete[] ptr; // 释放动态数组内存 }// 方案2:仿函数形式的删除器(用于delete[]动态数组) template<class T> class DeleteArray { public:void operator()(T* ptr) { // 重载函数调用运算符delete[] ptr; // 释放动态数组内存} };// 专门用于FILE指针的仿函数删除器 class Fclose { public:void operator()(FILE* ptr) {cout << "fclose:" << ptr << endl; // 打印关闭的文件指针fclose(ptr); // 关闭文件} };int main() {/* * 问题背景:* 直接使用智能指针管理动态数组会导致未定义行为(崩溃)* 因为默认的删除器使用delete而非delete[]*/// unique_ptr<Date> up1(new Date[10]); // 错误!会导致内存泄漏// shared_ptr<Date> sp1(new Date[10]); // 错误!会导致内存泄漏/* * 解决方案1:使用特化版本* unique_ptr和shared_ptr都提供了对数组的特化版本* 这些版本默认使用delete[]进行释放*/unique_ptr<Date[]> up1(new Date[5]); // 使用特化的unique_ptr数组版本shared_ptr<Date[]> sp1(new Date[5]); // C++17起支持的shared_ptr数组版本/* * 解决方案2:自定义删除器* 注意:unique_ptr和shared_ptr对删除器的支持方式不同* unique_ptr - 通过模板参数指定删除器类型* shared_ptr - 通过构造函数参数指定删除器对象*/// 2.1 使用仿函数作为删除器unique_ptr<Date, DeleteArray<Date>> up2(new Date[5]); // 模板参数指定删除器类型shared_ptr<Date> sp2(new Date[5], DeleteArray<Date>()); // 构造函数参数传递删除器对象// 2.2 使用函数指针作为删除器unique_ptr<Date, void(*)(Date*)> up3(new Date[5], DeleteArrayFunc<Date>);shared_ptr<Date> sp3(new Date[5], DeleteArrayFunc<Date>);// 2.3 使用lambda表达式作为删除器auto delArrOBJ = [](Date* ptr) { delete[] ptr; }; // 定义lambda删除器unique_ptr<Date, decltype(delArrOBJ)> up4(new Date[5], delArrOBJ); // decltype获取lambda类型shared_ptr<Date> sp4(new Date[5], delArrOBJ); // 直接传递lambda对象/* * 自定义删除器的其他应用:管理文件资源*/// 3.1 使用仿函数管理FILE指针shared_ptr<FILE> sp5(fopen("Test.cpp", "r"), Fclose());// 3.2 使用lambda管理FILE指针shared_ptr<FILE> sp6(fopen("Test.cpp", "r"), [](FILE* ptr) {cout << "fclose:" << ptr << endl; // 打印调试信息fclose(ptr); // 关闭文件});return 0; }
#include <iostream> #include <memory> // 包含智能指针的头文件 using namespace std;// 假设有一个Date类(这里需要提前定义) class Date { public:Date(int year, int month, int day) : year_(year), month_(month), day_(day) {} private:int year_;int month_;int day_; };int main() {// 1. 使用new直接构造shared_ptr(不推荐方式,因为可能造成内存泄漏)shared_ptr<Date> sp1(new Date(2024, 9, 11));// 解释:这里直接使用new创建Date对象,然后传给shared_ptr构造函数// 问题:如果shared_ptr构造失败,new分配的内存可能泄漏// 2. 使用make_shared构造shared_ptr(推荐方式)shared_ptr<Date> sp2 = make_shared<Date>(2024, 9, 11);// 解释:make_shared一次性分配内存并构造对象,更高效且安全// 3. 使用auto和make_shared构造shared_ptrauto sp3 = make_shared<Date>(2024, 9, 11);// 解释:auto自动推导类型,代码更简洁// 4. 创建空的shared_ptrshared_ptr<Date> sp4;// 解释:默认构造的shared_ptr为空,不管理任何对象// 5. 检查shared_ptr是否为空// if (sp1.operator bool()) // 显式调用方式if (sp1) // 隐式转换为bool,检查是否管理对象cout << "sp1 is not nullptr" << endl;if (!sp4) // 检查空指针cout << "sp4 is nullptr" << endl; // 原代码注释有误,应为sp4// 6. 错误的构造方式(编译会报错)// shared_ptr<Date> sp5 = new Date(2024, 9, 11);// 解释:shared_ptr的构造函数是explicit的,不能隐式转换// 正确做法:使用make_shared或显式构造// 7. 错误的unique_ptr构造方式(编译会报错)// unique_ptr<Date> sp6 = new Date(2024, 9, 11);// 解释:unique_ptr也不能从裸指针隐式转换// 正确做法:使用make_unique(C++14)或显式构造return 0; }
4. 智能指针的原理
-
auto_ptr
的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr
的思路是不支持拷贝。 -
大家重点要看看
shared_ptr
是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要new
一个引用计数出来。多个shared_ptr
指向资源时就++
引用计数,shared_ptr
对象析构时就--
引用计数,引用计数减到0
时代表当前析构的shared_ptr
是最后一个管理资源的对象,则析构资源。 -
-
代码示例:(模拟实现智能指针)
// 命名空间 bit,用于封装智能指针实现 namespace bit {// auto_ptr 模板类(C++98标准中的智能指针,现已弃用)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<T>& operator=(auto_ptr<T>& ap){// 检测是否为自赋值if (this != &ap){// 释放当前对象管理的资源if (_ptr)delete _ptr;// 转移资源所有权_ptr = ap._ptr;ap._ptr = nullptr; // 原对象置空}return *this;}// 析构函数~auto_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 解引用运算符重载(使对象用起来像指针)T& operator*(){return *_ptr;}// 箭头运算符重载T* operator->(){return _ptr;}private:T* _ptr; // 管理的原生指针};// unique_ptr 模板类(独占所有权的智能指针)template<class T>class unique_ptr{public:// explicit 防止隐式转换explicit unique_ptr(T* ptr):_ptr(ptr){}// 析构函数~unique_ptr(){if (_ptr){cout << "delete:" << _ptr << endl;delete _ptr;}}// 解引用运算符重载T& operator*(){return *_ptr;}// 箭头运算符重载T* operator->(){return _ptr;}// 禁用拷贝构造(=delete)unique_ptr(const unique_ptr<T>& sp) = delete;// 禁用赋值运算符(=delete)unique_ptr<T>& operator=(const unique_ptr<T>& sp) = delete;// 移动构造函数(支持移动语义)unique_ptr(unique_ptr<T>&& sp):_ptr(sp._ptr){sp._ptr = nullptr; // 移动后原对象置空}// 移动赋值运算符unique_ptr<T>& operator=(unique_ptr<T>&& sp){// 释放当前资源delete _ptr;// 接管新资源_ptr = sp._ptr;sp._ptr = nullptr; // 移动后原对象置空}private:T* _ptr; // 管理的原生指针};// shared_ptr 模板类(共享所有权的智能指针)template<class T>class shared_ptr{public:// 构造函数explicit shared_ptr(T* ptr = nullptr): _ptr(ptr), _pcount(new int(1)) // 引用计数初始化为1{}// 带删除器的构造函数template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new int(1)) // 引用计数初始化为1, _del(del) // 自定义删除器{}// 拷贝构造函数shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount) // 共享引用计数,_del(sp._del){++(*_pcount); // 引用计数加1}// 释放资源函数void release(){if (--(*_pcount) == 0) // 引用计数减1{// 当引用计数为0时,释放资源_del(_ptr); // 调用删除器delete _pcount; // 释放引用计数内存_ptr = nullptr;_pcount = nullptr;}}// 赋值运算符重载shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr) // 避免自赋值{release(); // 释放当前资源// 接管新资源_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount); // 引用计数加1_del = sp._del;}return *this;}// 析构函数~shared_ptr(){release();}// 获取原生指针T* get() const{return _ptr;}// 获取引用计数int use_count() const{return *_pcount;}// 解引用运算符重载T& operator*(){return *_ptr;}// 箭头运算符重载T* operator->(){return _ptr;}private:T* _ptr; // 管理的原生指针int* _pcount; // 引用计数指针//atomic<int>* _pcount; // 线程安全的引用计数(实际可用这个)function<void(T*)> _del = [](T* ptr) {delete ptr; }; // 默认删除器};/** 注意:这里的shared_ptr和weak_ptr是以最简洁的方式实现的,* 只能满足基本功能。weak_ptr的lock等功能无法实现。* 要实现完整功能需要把引用计数拿出来放到一个单独类型,* shared_ptr和weak_ptr都要存储指向这个类的对象才能实现。* 有兴趣可以去翻翻源代码。*/template<class T>class weak_ptr{public:weak_ptr(){}// 从shared_ptr构造weak_ptr(const shared_ptr<T>& sp):_ptr(sp.get()){}// 赋值运算符重载weak_ptr<T>& operator=(const shared_ptr<T>& sp){_ptr = sp.get();return *this;}private:T* _ptr = nullptr; // 观察的指针(不增加引用计数)}; }// 测试代码 int main() {// 测试auto_ptr(管理权转移)bit::auto_ptr<Date> ap1(new Date);bit::auto_ptr<Date> ap2(ap1); // 管理权转移后ap1悬空// ap1->_year++; // 错误:ap1已悬空// 测试unique_ptrbit::unique_ptr<Date> up1(new Date);// bit::unique_ptr<Date> up2(up1); // 错误:不支持拷贝bit::unique_ptr<Date> up3(move(up1)); // 支持移动语义(移动后up1悬空)// 测试shared_ptrbit::shared_ptr<Date> sp1(new Date);bit::shared_ptr<Date> sp2(sp1); // 支持拷贝(引用计数增加)bit::shared_ptr<Date> sp3(sp2); // 支持拷贝(引用计数增加)cout << sp1.use_count() << endl; // 输出引用计数sp1->_year++;cout << sp1->_year << endl;cout << sp2->_year << endl;cout << sp3->_year << endl;return 0; }
5. shared_ptr
和weak_ptr
5.1 shared_ptr循环引用问题
-
shared_ptr
大多数情况下管理资源非常合适,支持 RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放从而引发内存泄漏,因此我们需要认识循环引用的场景和资源未释放的原因,并学会使用weak_ptr
解决该问题。 -
如图所示场景,
n1
和n2
析构后,管理两个节点的引用计数减到 1。 -
右边的节点何时释放?左边节点中的
_next
管理着它,_next
析构后,右边的节点就会释放。而_next
何时析构?_next
是左边节点的成员,左边节点释放后_next
才会析构。左边节点何时释放?它由右边节点中的_prev
管理,_prev
析构后,左边节点才会释放。_prev
何时析构?_prev
是右边节点的成员,右边节点释放后_prev
才会析构。 -
至此,逻辑上形成闭环式的循环引用,导致双方都无法释放,最终造成内存泄漏。解决方法是将
ListNode
结构体中的_next
和_prev
改为weak_ptr
,由于weak_ptr
绑定到shared_ptr
时不会增加其引用计数,_next
和_prev
不再参与资源释放管理,从而成功打破循环引用,解决该问题。 -
-
代码示例:
#include <iostream> #include <memory> // 包含智能指针的头文件using namespace std;// 定义双向链表的节点结构体 struct ListNode {int _data; // 节点存储的数据// 使用 shared_ptr 管理下一个节点(会导致循环引用问题)std::shared_ptr<ListNode> _next;// 使用 shared_ptr 管理前一个节点(会导致循环引用问题)std::shared_ptr<ListNode> _prev;/** 解决方案:将下面两行取消注释,改用 weak_ptr* 使用 weak_ptr 可以打破 shared_ptr 的循环引用* weak_ptr 不会增加引用计数,不会影响资源的释放*/// std::weak_ptr<ListNode> _next;// std::weak_ptr<ListNode> _prev;// 析构函数,用于观察对象是否被正确释放~ListNode(){cout << "~ListNode()" << endl;} };int main() {// 创建两个链表节点,使用 shared_ptr 管理std::shared_ptr<ListNode> n1(new ListNode);std::shared_ptr<ListNode> n2(new ListNode);// 打印初始引用计数(应该是1)cout << "n1引用计数: " << n1.use_count() << endl;cout << "n2引用计数: " << n2.use_count() << endl;// 建立双向链接(这会创建循环引用)n1->_next = n2; // n2的引用计数增加为2n2->_prev = n1; // n1的引用计数增加为2// 打印链接后的引用计数(应该是2)cout << "链接后n1引用计数: " << n1.use_count() << endl;cout << "链接后n2引用计数: " << n2.use_count() << endl;/** 程序结束时问题:* 1. n1的引用计数从2减为1(因为n2->_prev还持有引用)* 2. n2的引用计数从2减为1(因为n1->_next还持有引用)* 3. 两个节点都无法被释放,导致内存泄漏*//** 错误示例:weak_ptr不能直接管理资源* weak_ptr必须绑定到shared_ptr,不能单独使用*/// std::weak_ptr<ListNode> wp(new ListNode); // 编译错误return 0; }
5.2 weak_ptr
-
weak_ptr
不支持 RAII,也不支持直接访问资源。 根据文档,weak_ptr
构造时只能绑定到shared_ptr
,而不能直接绑定到资源,且绑定到shared_ptr
时不会增加其引用计数,从而解决循环引用问题。 -
weak_ptr
未重载operator*
和operator->
,因为它不参与资源管理。如果其绑定的shared_ptr
已释放资源,weak_ptr
访问资源将非常危险。weak_ptr
提供expired()
检查资源是否过期,use_count()
获取shared_ptr
的引用计数。 -
若需访问资源,可调用
lock()
返回一个管理该资源的shared_ptr
:若资源已释放,则返回空shared_ptr
;若未释放,则通过返回的shared_ptr
安全访问资源。 -
代码示例:
#include <iostream> #include <memory> // 智能指针头文件 #include <string> // 字符串头文件 using namespace std;int main() {// 1. 创建一个 shared_ptr<string> sp1,指向新分配的字符串 "111111"std::shared_ptr<string> sp1(new string("111111"));// 2. 创建另一个 shared_ptr<string> sp2,与 sp1 共享所有权(引用计数 +1)std::shared_ptr<string> sp2(sp1);// 3. 创建一个 weak_ptr<string> wp,观察 sp1 管理的对象(不增加引用计数)std::weak_ptr<string> wp = sp1;// 4. 检查 wp 是否过期(即观察的对象是否已被释放)cout << "wp.expired(): " << wp.expired() << endl; // 输出 0(false),因为 sp1 和 sp2 仍然持有对象// 5. 输出 wp 观察的对象的引用计数(即 sp1 和 sp2 的引用计数)cout << "wp.use_count(): " << wp.use_count() << endl; // 输出 2(sp1 和 sp2)// 6. sp1 指向新的字符串 "222222",原对象引用计数减 1(sp2 仍然持有原对象)sp1 = make_shared<string>("222222");// 7. 再次检查 wp 是否过期(sp2 仍然持有原对象,未过期)cout << "wp.expired(): " << wp.expired() << endl; // 输出 0(false)// 8. 输出 wp 观察的对象的引用计数(仅 sp2 持有原对象)cout << "wp.use_count(): " << wp.use_count() << endl; // 输出 1(sp2)// 9. sp2 也指向新的字符串 "333333",原对象引用计数减 1(无 shared_ptr 持有原对象)sp2 = make_shared<string>("333333");// 10. 检查 wp 是否过期(原对象已被释放,过期)cout << "wp.expired(): " << wp.expired() << endl; // 输出 1(true)// 11. 输出 wp 观察的对象的引用计数(原对象已释放,引用计数为 0)cout << "wp.use_count(): " << wp.use_count() << endl; // 输出 0// 12. wp 重新观察 sp1 管理的新对象("222222")wp = sp1;// 13. 使用 wp.lock() 获取一个 shared_ptr,引用计数 +1(sp1 和 sp3 共同持有 "222222")auto sp3 = wp.lock(); // 等价于 shared_ptr<string> sp3 = wp.lock();// 14. 检查 wp 是否过期(sp1 和 sp3 持有对象,未过期)cout << "wp.expired(): " << wp.expired() << endl; // 输出 0(false)// 15. 输出 wp 观察的对象的引用计数(sp1 和 sp3)cout << "wp.use_count(): " << wp.use_count() << endl; // 输出 2// 16. 通过 sp3 修改字符串内容(追加 "###")*sp3 += "###";// 17. 输出 sp1 管理的字符串(与 sp3 共享同一对象,内容已被修改)cout << "*sp1: " << *sp1 << endl; // 输出 "222222###"return 0; }
6. shared_ptr
的线程安全问题
-
shared_ptr
的引用计数对象在堆上,如果多个shared_ptr
对象在多个线程中进行拷贝或析构时会访问并修改引用计数,此时会存在线程安全问题,因此shared_ptr
的引用计数需要通过加锁或原子操作来保证线程安全。 -
shared_ptr
指向的对象本身也可能存在线程安全问题,但这一问题的管理不属于shared_ptr
的职责范围,而应由使用shared_ptr
的外部代码进行线程安全控制。例如,以下程序可能出现崩溃或资源未释放的情况,若将bit::shared_ptr
的引用计数从int*
改为atomic<int>*
,则可确保引用计数的线程安全性,或通过互斥锁实现同步保护。 -
代码示例:
#include <iostream> #include <thread> #include <mutex> using namespace std;// 定义一个简单的AA类 struct AA {int _a1 = 0; // 成员变量1,初始化为0int _a2 = 0; // 成员变量2,初始化为0// 析构函数,对象销毁时调用~AA(){cout << "~AA()" << endl; // 打印析构信息} };int main() {// 创建一个指向AA对象的智能指针bit::shared_ptr<AA> p(new AA); // 假设bit是自定义命名空间const size_t n = 100000; // 循环次数mutex mtx; // 互斥锁,用于线程同步// 定义一个lambda函数,将被多个线程执行auto func = [&]() // 捕获所有外部变量引用{for (size_t i = 0; i < n; ++i) // 循环n次{// 创建一个智能指针拷贝(会增加引用计数)bit::shared_ptr<AA> copy(p); // 拷贝构造,引用计数+1{ // 加锁作用域开始// 使用unique_lock加锁(比lock_guard更灵活)unique_lock<mutex> lk(mtx);// 通过拷贝的智能指针访问和修改对象成员copy->_a1++; // 原子操作递增_a1copy->_a2++; // 原子操作递增_a2} // 加锁作用域结束,自动释放锁// copy离开作用域,引用计数自动-1}};// 创建两个线程执行相同的函数thread t1(func);thread t2(func);// 等待线程结束t1.join();t2.join();// 打印最终结果cout << p->_a1 << endl; // 输出_a1的值cout << p->_a2 << endl; // 输出_a2的值cout << p.use_count() << endl; // 输出当前引用计数(应该是1)return 0;// main函数结束时,p离开作用域,引用计数减为0,AA对象被销毁 }
7. C++11和boost
中智能指针的关系
- Boost库是为C++语言标准库提供扩展的一些C++程序库的总称,Boost社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,Boost社区的发起人Dawes本人就是C++标准委员会的成员之一。在Boost库的开发中,Boost社区也在这个方向上取得了丰硕的成果,C++11及之后的新语法和库有很多都是从Boost中来的。
- C++98中产生了第一个智能指针
auto_ptr
;C++ Boost给出了更实用的scoped_ptr
、scoped_array
、shared_ptr
、shared_array
和weak_ptr
等;C++ TR1引入了shared_ptr
等,但需要注意的是TR1并不是标准版; - C++11引入了
unique_ptr
、shared_ptr
和weak_ptr
,需要注意的是unique_ptr
对应Boost的scoped_ptr
,并且这些智能指针的实现原理是参考Boost中的实现的。
8. 内存泄露
-
什么是内存泄漏
内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常导致程序未能执行释放操作。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
-
内存泄漏的危害
内存泄漏的危害:普通程序运行一会就结束了,出现内存泄漏问题也不大,进程正常结束时页表的映射关系解除,物理内存也会被释放;但对于长期运行的程序(如操作系统、后台服务、长时间运行的客户端等),内存泄漏影响很大,随着泄漏累积会导致可用内存不断减少,系统响应变慢甚至卡死。
-
解决方案:
- 预防措施:工程前期应建立良好的设计规范,养成编码时配对释放内存的习惯(但异常场景仍可能出问题,需结合智能指针管理);
- 使用智能指针(如RAII机制)管理资源,特殊场景可自行实现资源管理工具;
- 检测工具:定期使用内存泄漏检测工具(尤其是项目上线前),但需注意工具的可靠性。
总结:内存泄漏非常常见,解决分为两类——
- 事前预防(如智能指针);
- 事后查错(如泄漏检测工具)。