C++11 : 智能指针

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 
    #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;
    }
    

2. RALL和智能指针的设计思路

  • 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;  // 程序正常结束
    }
    

3. C++标准库智能指针的使用

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

  • 其他特性

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

4. 智能指针的原理

  • auto_ptr 的思路是拷贝时转移资源管理权给被拷贝对象,这种思路是不被认可的,也不建议使用。unique_ptr 的思路是不支持拷贝。

  • 大家重点要看看 shared_ptr 是如何设计的,尤其是引用计数的设计,主要这里一份资源就需要一个引用计数,所以引用计数采用静态成员的方式是无法实现的,要使用堆上动态开辟的方式,构造智能指针对象时来一份资源,就要 new 一个引用计数出来。多个 shared_ptr 指向资源时就 ++ 引用计数,shared_ptr 对象析构时就 -- 引用计数,引用计数减到 0 时代表当前析构的 shared_ptr 是最后一个管理资源的对象,则析构资源。

  • C++11 : 智能指针_第1张图片

  • 代码示例:(模拟实现智能指针)

    // 命名空间 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;
    }
    

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 不再参与资源释放管理,从而成功打破循环引用,解决该问题。

  • C++11 : 智能指针_第2张图片

  • 代码示例:

    #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;
    }
    

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 
    #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;
    }
    

6. 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对象被销毁
    }
    

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. 检测工具:定期使用内存泄漏检测工具(尤其是项目上线前),但需注意工具的可靠性。

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

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

你可能感兴趣的:(c++,开发语言)