C++11 : 智能指针

article/2025/7/2 14:13:40

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_ptrshared_ptrweak_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_ptrshared_ptr 特化了 [] 版本以支持动态数组(如 unique_ptr<Date[]>(new Date[5]))。

  • 其他特性

    • make_shared<Type>(args...)shared_ptr还可以用初始化资源对象的值直接构造资源对象,效率优于显式 new
    • operator bool:允许通过 if(ptr) 判断智能指针是否为空。如未管理资源则返回false,否则返回ture
    • unique_ptrshared_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_ptrweak_ptr

5.1 shared_ptr循环引用问题

  • shared_ptr 大多数情况下管理资源非常合适,支持 RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放从而引发内存泄漏,因此我们需要认识循环引用的场景和资源未释放的原因,并学会使用 weak_ptr 解决该问题。

  • 如图所示场景,n1n2 析构后,管理两个节点的引用计数减到 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_ptrscoped_arrayshared_ptrshared_arrayweak_ptr等;C++ TR1引入了shared_ptr等,但需要注意的是TR1并不是标准版;
  • C++11引入了unique_ptrshared_ptrweak_ptr,需要注意的是unique_ptr对应Boost的scoped_ptr,并且这些智能指针的实现原理是参考Boost中的实现的。

8. 内存泄露

  • 什么是内存泄漏

    内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存,一般是忘记释放或者发生异常导致程序未能执行释放操作。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

  • 内存泄漏的危害

    内存泄漏的危害:普通程序运行一会就结束了,出现内存泄漏问题也不大,进程正常结束时页表的映射关系解除,物理内存也会被释放;但对于长期运行的程序(如操作系统、后台服务、长时间运行的客户端等),内存泄漏影响很大,随着泄漏累积会导致可用内存不断减少,系统响应变慢甚至卡死。

  • 解决方案

    1. 预防措施:工程前期应建立良好的设计规范,养成编码时配对释放内存的习惯(但异常场景仍可能出问题,需结合智能指针管理);
    2. 使用智能指针(如RAII机制)管理资源,特殊场景可自行实现资源管理工具;
    3. 检测工具:定期使用内存泄漏检测工具(尤其是项目上线前),但需注意工具的可靠性。

    总结:内存泄漏非常常见,解决分为两类——

    • 事前预防(如智能指针);
    • 事后查错(如泄漏检测工具)。

http://www.hkcw.cn/article/URqbhjiNDN.shtml

相关文章

嵌入式开发之STM32学习笔记day16

STM32F103C8T6 I2C通信协议 1 I2C简介 I2C&#xff08;Inter-Integrated Circuit&#xff09;是一种两线制的串行通信协议&#xff0c;广泛应用于微控制器与外围设备之间的数据传输&#xff0c;它支持多主多从的通信模式&#xff0c;允许多个设备连接在同一总线上&#xff0c;…

Redis数据类型操作命令

Redis通用命令 keys&#xff1a;查看符合模板的所有key 因为keys命令使用的是模糊查序&#xff0c;比较耗性能&#xff0c;由于有redis是单线程&#xff0c;因此在生成情况下不建议使用该命令。del&#xff1a;删除一个或者多个keyexists&#xff1a;判断一个key是否存在expi…

Leetcode 2123. 使矩阵中的 1 互不相邻的最小操作数

1.题目基本信息 1.1.题目描述 给你一个 下标从 0 开始 的矩阵 grid。每次操作&#xff0c;你可以把 grid 中的 一个 1 变成 0 。 如果一个矩阵中&#xff0c;没有 1 与其它的 1 四连通&#xff08;也就是说所有 1 在上下左右四个方向上不能与其他 1 相邻&#xff09;&#x…

STL解析——list的使用

目录 1.简介 2.构造函数 3.迭代器 3.1封装 3.2迭代器分类 4.排序性能 4.1链式与数组 4.2缓存读取 1.简介 STL容器中提供的list容器也是一种顺序容器&#xff0c;底层实现方式是带头双向链表&#xff0c;这种实现方式能比单链表更高效的访问数据。 下面围绕部分重要接口…

数据库系统概论(十一)SQL 集合查询 超详细讲解(附带例题表格对比带你一步步掌握)

数据库系统概论&#xff08;十一&#xff09;SQL 集合查询 超详细讲解&#xff08;附带例题表格对比带你一步步掌握&#xff09; 前言一、什么是集合查询&#xff1f;二、集合操作的三种类型1. 并操作2. 交操作3. 差操作 三、使用集合查询的前提条件四、常见问题与注意事项五、…

数学建模期末速成 最短路径

关键词&#xff1a;Dijkstra算法 Floyd算法 例题 已知有6个村庄&#xff0c;各村的小学生人数如表所列&#xff0c;各村庄间的距离如图所示。现在计划建造一所医院和一所小学&#xff0c;问医院应建在哪个村庄才能使最远村庄的人到医院看病所走的路最短&#xff1f;又问小学建…

MonitorSDK_监测用户行为(点击、页面路由变化、页面浏览量变化)

点击事件监测 为了实现用户点击事件的监控和数据埋点&#xff0c;可以通过监听全局的 mousedown 和 touchstart 事件&#xff0c;收集用户交互数据&#xff0c;并将其上报到服务器。 export default function onClick(){[mousedown, touchstart].forEach( eventType > { …

NE555输出PWM驱动NMOS控制灯光电路Multisim仿真

仿真电路&#xff1a; 遇到的一些问题&#xff1a; 1、NE555怎么产生PWM波形&#xff1f; 解&#xff1a; 555定时器频率计算器_555定时器频率在线计算_电路参数计算 - 电子发烧友(www.elecfans.com) 这个在线工具可以通过设定频率、占空比、电阻&#xff0c;从而求出电阻值…

ThinkPrune:在RL中引入长度限制,在保持性能一致或略有提升下,显著提升推理效率

摘要&#xff1a;我们提出了THINKPRUNE&#xff0c;这是一种简单而有效的方法&#xff0c;用于缩短长思考型大语言模型&#xff08;LLMs&#xff09;的思考长度。这些模型被发现常常会产生低效且冗余的思考过程。现有的关于减少思考长度的初步探索主要集中在迫使思考过程提前结…

重温经典算法——堆排序

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl 基本原理 堆排序是一种基于二叉堆的排序算法&#xff0c;时间复杂度为O(n log n)。堆排序核心步骤包括构建最大堆和反复取出堆顶元素排序&#xff1a;首先从最后一个非叶子…

PyTorch——卷积层(3)

conv_arithmetic/README.md at master vdumoulin/conv_arithmetic GitHub out_channel1 out_channel2

5.29 自学测试 Linux基础 Day4

一、Linux操作系统介绍 1.操作系统介绍&#xff1a; 管理计算机硬件与软件资源的计算机程序&#xff0c;同时也是计算机系统的内核与基石。 2.常见的操作系统 桌面操作系统&#xff1a;Windows系列、Linux、MacOS 嵌入式操作系统&#xff1a;Linux 服务器操作系统&#x…

推荐一款使用html开发桌面应用的工具——mixone

简介 mixone是开发桌面应用&#xff08;Win、Mac、Linux&#xff09;的一款工具、其基于electron实现。其拥有简单的工程结构。以为熟悉前端开发的程序员可以很轻松的开发出桌面应用&#xff0c;它比electron的其他框架更简单&#xff0c;因为那些框架基本上还需要了解electro…

leetcode hot100 二叉树(二)

书接上回&#xff1a;https://blog.csdn.net/weixin_74129837/article/details/148367615?spm1001.2014.3001.5501 8.验证二叉搜索树 维护一个min_val和max_val&#xff0c;限制当前结点的合法值范围。min_val和max_val动态变化。 class Solution { public:bool check(Tree…

【Linux】基础文件IO

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;Linux 前言 无论是日常使用还是系统管理&#xff0c;文件是Linux系统中最核心的概念之一。对于初学者来说&#xff0c;理解文件是如何被创建、读取、写入以及存储…

MYSQL MGR高可用

1&#xff0c;MYSQL MGR高可用是什么 简单来说&#xff0c;MySQL MGR 的核心目标就是&#xff1a;确保数据库服务在部分节点&#xff08;服务器&#xff09;发生故障时&#xff0c;整个数据库集群依然能够继续提供读写服务&#xff0c;最大限度地减少停机时间。 2. 核心优势 v…

【java面试】MySQL篇

MySQL篇 一、总体结构二、优化&#xff08;一&#xff09;定位慢查询1.1 开源工具1.2Mysql自带的慢日志查询1.3 总结 &#xff08;二&#xff09;定位后优化2.1 优化2.2 总结 &#xff08;三&#xff09;索引3.1 索引3.2 索引底层数据结构——B树3.3 总结 &#xff08;四&#…

头像预览和上传

在写一个项目的时候&#xff0c;遇到了头像修改这个功能的需求&#xff0c;在最开始的学习中发现可以通过type为file的input文件读取图片&#xff0c;然后将其转换为DataUrl格式&#xff0c;最终作为Ima元素的src即可在页面上展示图片。但到后面开始写交互的时候发现DataUrl格式…

解锁效率新高度:Agent Zero智能助手框架

探索Agent Zero AI框架&#xff1a;您的个性化智能助手 在迅速发展的科技世界&#xff0c;Agent Zero AI框架为我们揭开了一个全新的大门。被设计成能够与用户同步成长与学习的智能助手&#xff0c;Agent Zero展现了它作为个性化使用工具的非凡潜力。在本篇文章中&#xff0c;…

第43节:Vision Transformer (ViT)视觉领域的革命性架构

1. ViT的诞生背景与核心思想 Vision Transformer (ViT) 是2020年由Google Research团队提出的一种革命性计算机视觉架构,它将自然语言处理(NLP)领域中大获成功的Transformer模型引入到计算机视觉任务中。这一创新彻底改变了传统卷积神经网络(CNN)在视觉任务中的主导地位,为图…