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