Bootstrap

【C++】35.智能指针(2)


5. shared_ptr和weak_ptr

5.1 shared_ptr循环引用问题

  • shared_ptr大多数情况下管理资源非常合适,支持RAII,也支持拷贝。但是在循环引用的场景下会导致资源没得到释放内存泄漏,所以我们要认识循环引用的场景和资源没释放的原因,并且学会使用weak_ptr解决这种问题。
  • 如下图所述场景,n1和n2析构后,管理两个节点的引用计数减到1
  1. 右边的节点什么时候释放呢,左边节点中的_next管着呢,_next析构后,右边的节点就释放了。
  2. _next什么时候析构呢,_next是左边节点的的成员,左边节点释放,_next就析构了。
  3. 左边节点什么时候释放呢,左边节点由右边节点中的_prev管着呢,_prev析构后,左边的节点就释放了。
  4. _prev什么时候析构呢,_prev是右边节点的成员,右边节点释放,_prev就析构了。
  • 至此逻辑上成功形成回旋镖似的循环引用,谁都不会释放就形成了循环引用,导致内存泄漏
  • ListNode结构体中的_next_prev改成weak_ptrweak_ptr绑定到shared_ptr时不会增加它的引用计数,_next_prev不参与资源释放管理逻辑,就成功打破了循环引用,解决了这里的问题

2a494ece2d2b9e2f35947831dfa8016d

2636088b88ef349dd6f5fbdabdbda622

struct ListNode
{
    int _data;  // 节点数据

    // 方案1:使用shared_ptr(会导致循环引用)
    std::shared_ptr<ListNode> _next;  // 指向下一个节点的智能指针
    std::shared_ptr<ListNode> _prev;  // 指向前一个节点的智能指针
    
    // 方案2:使用weak_ptr(解决循环引用)
    /*std::weak_ptr<ListNode> _next;  // 弱引用指向下一节点
    std::weak_ptr<ListNode> _prev;   // 弱引用指向前一节点*/
    // weak_ptr特点:
    // 1. 当n1->_next = n2绑定shared_ptr时不增加n2的引用计数
    // 2. 不参与资源释放的管理
    // 3. 成功避免循环引用问题

    ~ListNode()
    {
        cout << "~ListNode()" << endl;  // 析构时输出提示
    }
};

int main()
{
    // 演示循环引用导致的内存泄露问题
    std::shared_ptr<ListNode> n1(new ListNode);  // 创建第一个节点
    std::shared_ptr<ListNode> n2(new ListNode);  // 创建第二个节点

    cout << n1.use_count() << endl;  // 输出1(初始引用计数)
    cout << n2.use_count() << endl;  // 输出1(初始引用计数)

    // 建立双向链接,使用shared_ptr会导致循环引用
    n1->_next = n2;  // n2的引用计数加1
    n2->_prev = n1;  // n1的引用计数加1

    cout << n1.use_count() << endl;  // 输出2(因为n2->_prev也指向n1)
    cout << n2.use_count() << endl;  // 输出2(因为n1->_next也指向n2)

    // 注意:weak_ptr的限制
    // 1. weak_ptr不支持直接管理资源
    // 2. 不支持RAII(资源获取即初始化)
    // 3. weak_ptr只能绑定到shared_ptr上
    //std::weak_ptr<ListNode> wp(new ListNode);  // 错误!weak_ptr不能直接管理资源

    // 程序结束时:
    // 1. 由于循环引用,n1和n2的引用计数永远不会降到0
    // 2. 导致内存泄露
    // 3. 析构函数不会被调用
    return 0;
}

5.2 weak_ptr

  • weak_ptr 不支持RAII,也不支持访问资源,所以我们看文档发现weak_ptr构造时不支持绑定到资源,只支持绑定到shared_ptr,绑定到shared_ptr时,不增加shared_ptr的引用计数,那么就可以解决上述的循环引用问题。
  • weak_ptr也没有重载operator*和operator->等,因为他不参与资源管理,那么如果他绑定的shared_ptr已经释放了资源,那么他去访问资源就是很危险的。weak_ptr支持expired检查指向的资源是否过期,use_count也可获取shared_ptr的引用计数,weak_ptr想访问资源时,可以调用lock返回一个管理资源的shared_ptr,如果资源已经被释放,返回的shared_ptr是一个空对象,如果资源没有释放,则通过返回的shared_ptr访问资源是安全的。
int main()
{
    // 创建一个shared_ptr,管理string对象"111111"
    std::shared_ptr<string> sp1(new string("111111"));
    
    // sp2和sp1指向相同的对象,引用计数增加到2
    std::shared_ptr<string> sp2(sp1);
    
    // 创建weak_ptr,观察sp1指向的对象,不增加引用计数
    std::weak_ptr<string> wp = sp1;
    
    cout << wp.expired() << endl;    // 输出0(false),wp观察的对象还存在
    cout << wp.use_count() << endl;  // 输出2,表示被观察对象的引用计数
    
    // sp1指向新对象"222222",原对象的引用计数减1(变为1)
    sp1 = make_shared<string>("222222");
    cout << wp.expired() << endl;    // 输出0(false),因为sp2还在引用原对象
    cout << wp.use_count() << endl;  // 输出1,sp2仍在引用原对象
    
    // sp2也指向新对象"333333",原对象引用计数减为0并释放
    sp2 = make_shared<string>("333333");
    cout << wp.expired() << endl;    // 输出1(true),原对象已被释放
    cout << wp.use_count() << endl;  // 输出0,没有shared_ptr引用原对象
    
    // wp重新绑定到sp1当前指向的对象("222222")
    wp = sp1;
    
    // 通过weak_ptr获取一个shared_ptr
    //std::shared_ptr<string> sp3 = wp.lock();
    auto sp3 = wp.lock();  // lock()返回一个shared_ptr,引用计数加1
    
    cout << wp.expired() << endl;    // 输出0(false),对象存在
    cout << wp.use_count() << endl;  // 输出2,sp1和sp3都指向对象
    
    // 通过sp3修改对象,因为sp1和sp3指向同一对象,所以sp1看到的值也改变
    *sp3 += "###";
    cout << *sp1 << endl;  // 输出"222222###"
    
    return 0;
    // 程序结束时sp1、sp3析构,对象被释放
}

6. shared_ptr的线程安全问题

  • shared_ptr的引用计数对象在堆上,如果多个shared_ptr对象在多个线程中,进行shared_ptr的拷贝析构时会访问修改引用计数,就会存在线程安全问题,所以shared_ptr引用计数是需要加锁或者原子操作保证线程安全的。
  • shared_ptr指向的对象也是有线程安全的问题的,但是这个对象的线程安全问题不归shared_ptr管,它也管不了,应该有外层使用shared_ptr的人进行线程安全的控制。
  • 下面的程序会崩溃或者A资源没释放,bit::shared_ptr引用计数从int*改成atomic<int>*就可以保证引用计数的线程安全问题,或者使用互斥锁加锁也可以。
struct AA
{
    int _a1 = 0;  // 第一个计数器,初始化为0
    int _a2 = 0;  // 第二个计数器,初始化为0
    ~AA()
    {
        cout << "~AA()" << endl;  // 析构时输出提示
    }
};

int main()
{
    // 创建一个shared_ptr管理AA对象
    bit::shared_ptr<AA> p(new AA);
    
    // 定义循环次数
    const size_t n = 100000;
    
    // 创建互斥锁用于同步
    mutex mtx;
    
    // 定义线程要执行的函数
    auto func = [&]() {
        for (size_t i = 0; i < n; ++i)
        {
            // 创建智能指针的副本,此时会原子地增加引用计数
            // 这个操作本身是线程安全的,不需要加锁
            bit::shared_ptr<AA> copy(p);
            
            {
                // 作用域加锁,保护对象成员的修改
                unique_lock<mutex> lk(mtx);
                copy->_a1++;  // 增加第一个计数器
                copy->_a2++;  // 增加第二个计数器
            }
            // 作用域结束,锁自动释放
            // copy析构,引用计数自动减1
        }
    };

    // 创建两个线程执行相同的任务
    thread t1(func);
    thread t2(func);

    // 等待两个线程完成
    t1.join();
    t2.join();

    // 输出最终结果
    cout << p->_a1 << endl;       // 应该输出200000 (100000 * 2)
    cout << p->_a2 << endl;       // 应该输出200000 (100000 * 2)
    cout << p.use_count() << endl;// 输出1,因为只有p一个引用了

    return 0;
    // 程序结束时p析构,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. 内存泄漏

8.1 什么是内存泄漏,内存泄漏的危害

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

内存泄漏的危害:普通程序运行一会就结束了出现内存泄漏问题也不大,进程正常结束,页表的映射关系解除,物理内存也可以释放。长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务、长时间运行的客户端等等,不断出现内存泄漏会导致可用内存不断变少,各种功能响应越来越慢,最终卡死。

int main()
{
    // 申请一个1G未释放,这个程序多次运行也没啥危害
    // 因为程序马上就结束,进程结束各种资源也就回收了
    char* ptr = new char[1024 * 1024 * 1024];
    cout << (void*)ptr << endl;
    return 0;
}

8.2 如何检测内存泄漏(了解)


8.3 如何避免内存泄漏

  • 工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
  • 尽量使用智能指针来管理资源,如果自己场景比较特殊,采用RAII思想自己造个轮子管理。
  • 定期使用内存泄漏工具检测,尤其是每次项目快上线前,不过有些工具不够靠谱,或者是收费。
  • 总结一下:内存泄漏非常常见,解决方案分为两种:
    • 1、事前预防型。如智能指针等。
    • 2、事后查错型。如泄漏检测工具。

9. 定制删除器

namespace bit
{
    template<class T>
    class shared_ptr
    {
    public:
        // 构造函数,传入指针,初始化引用计数为1
        // 默认参数nullptr,支持无参构造
        shared_ptr(T* ptr = nullptr)
            :_ptr(ptr)
            , _pcount(new int(1))
        {}

        // 支持自定义删除器的构造函数
        // 模板参数D表示删除器类型,可以接收各种可调用对象
        template<class D>
        shared_ptr(T* ptr, D del)
            : _ptr(ptr)
            , _pcount(new int(1))
            , _del(del)  // 存储删除器
        {}

        // 析构函数 
        // 引用计数减1,如果减到0释放资源
        ~shared_ptr()
        {
            if (--(*_pcount) == 0)
            {
                cout << "delete:" << _ptr << endl;
                //_del替代了直接delete,更加灵活
                _del(_ptr);    // 使用删除器释放资源
                delete _pcount; // 释放计数器
            }
        }

        // 重载操作符,让智能指针用起来像普通指针
        T& operator*() { return *_ptr; }  // 解引用
        T* operator->() { return _ptr; }  // 箭头运算符

        // 拷贝构造,共享管理同一资源
        shared_ptr(const shared_ptr<T>& sp)
            :_ptr(sp._ptr)
            , _pcount(sp._pcount)
        {
            ++(*_pcount);  // 引用计数加1
        }

        // 赋值运算符重载
        shared_ptr<T>& operator=(const shared_ptr<T>& sp)
        {
            //检查自赋值
            if (_ptr == sp._ptr)
                return *this;

            // 释放原来的资源
            if (--(*_pcount) == 0)
            {
                delete _ptr;
                delete _pcount;
            }

            // 共享新资源
            _ptr = sp._ptr;
            _pcount = sp._pcount;
            ++(*_pcount);

            return *this;
        }

        // 获取引用计数
        int use_count() const { return *_pcount; }
        
        // 获取原始指针
        T* get() const { return _ptr; }

    private:
        T* _ptr;         // 指向管理的资源
        int* _pcount;    // 引用计数
        // 默认删除器使用delete,可以被构造函数中传入的删除器替换
        function<void(T*)> _del = [](T* ptr) {delete ptr; };
    };
}

// 自定义数组删除器
template<class T>
struct DeleteArray
{
    void operator()(T* ptr)
    {
        delete[] ptr;  // 使用delete[]释放数组
    }
};

/*
使用示例:
1. 管理数组:
bit::shared_ptr<A> sp1(new A[10], DeleteArray<A>());

2. 管理malloc内存:
bit::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });

3. 管理文件指针:
bit::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
    cout << "fclose:" << ptr << endl;
    fclose(ptr);
});
*/

// 基本使用
int main()
{
    bit::shared_ptr<A> sp1(new A[10], DeleteArray<A>());
	bit::shared_ptr<A> sp2((A*)malloc(sizeof(A)), [](A* ptr) {free(ptr); });
	bit::shared_ptr<FILE> sp3(fopen("Test.cpp", "r"), [](FILE* ptr) {
		cout << "fclose:" << ptr << endl;
		fclose(ptr);
		});
    bit::shared_ptr<A> sp1(new A); // 使用默认删除器
    return 0;
}

// 错误示例:可能导致内存泄漏
int main()
{
    char* ptr = new char[1024 * 1024 * 1024]; // 分配1GB内存但没有释放
    return 0;
}
;