• C++ -------- 智能指针


    目录

    智能指针 

    1.为什么需要智能指针

     2.内存泄漏

    (1)什么是内存泄漏,内存泄漏的危害

    (2)内存泄漏分类(了解)

    (3)如何避免内存泄漏

    智能指针的使用及原理

    1.RAII

    (1)基本概念

    (2)代码模拟

    (3)为什么要解决智能指针对象的拷贝问题

    2.std::auto_ptr

    (1)C++98库中提供了auto_ptr ,原理是实现管理权转移

    (2)模拟实现auto_ptr

    3.std::unique_ptr

    (1)C++11更新智能指针

    (2)unique_ptr模拟实现

    4.std::shared_ptr

    (1)C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

    (2)share_ptr原理

    (3)简单模拟实现share_ptr

    (4)为什么引用计数要放在堆区

    5.线程安全问题

    (1)线程不安全测试,这里我们自己实现的share_ptr未加锁

    (2)代码测试总结

    (3)模拟线程安全的代码 , 引用计数加锁

     6.std::share_ptr的循环引用

    (1)测试代码

    (2)循环引用分析

    (3)循环引用的解决方式 weak_ptr 

    (4)weak_ptr模拟实现

    定制删除器

    1.关于new 和 delete 的补充

     2.定制删除器的用法

    (1)错误用法

    (2)正确用法

    (3)模拟实现

    (4)小结

    C++11和boost中智能指针的关系


                    

     

    智能指针 

    1.为什么需要智能指针

    • 为了解决内存泄漏的问题,C++中提出了智能指针

                                    

    • 测试代码
    1. int div()
    2. {
    3. int a, b;
    4. cin >> a >> b;
    5. if (b == 0)
    6. throw invalid_argument("除0错误");
    7. return a / b;
    8. }
    9. void func()
    10. {
    11. int* p1 = new int;
    12. cout << div() << endl; //这里可能会抛异常
    13. delete p1;
    14. }
    15. int main()
    16. {
    17. try
    18. {
    19. func();
    20. }
    21. catch (const exception& e)
    22. {
    23. cout << e.what() << endl;
    24. }
    25. //Do something ...
    26. return 0;
    27. }

                             

    • ①在异常的讲解中提到过,如果程序的中间抛异常且在主函数中捕获可能导致内存泄漏的问题
    • ②new出来的空间,没有进行释放,存在内存泄漏的问题。
    • ③异常安全问题。如果在 new和delete 之间如果存在抛异常,那么还是有内存泄漏。这种问题就叫异常安全。
    • ④异常安全的问题一种解决方式 -- 捕获重新抛出,但是这种方式并没有从根源上解决问题
                         
                       
                                    

     2.内存泄漏

    (1)什么是内存泄漏,内存泄漏的危害

    • 什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
    • 内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

    ​                

    (2)内存泄漏分类(了解)

    ①堆内存泄漏(Heap leak)

    • 堆内存指的是程序执行中依据须要分配通过malloc / calloc / realloc / new等从堆中分配的一块内存,
    • 用完后必须通过调用相应的 free或者delete 删掉。假设程序的设计错误导致这部分内存没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap Leak。

    ②系统资源泄漏

    • 指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。

                                                     

    (3)如何避免内存泄漏

    • ①工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记着匹配的去释放。ps:这个理想状态。但是如果碰上异常时,就算注意释放了,还是可能会出问题。需要下一条智能指针来管理才有保证。
    • ②采用RAII思想或者智能指针来管理资源。
    • ③有些公司内部规范使用内部实现的私有内存管理库。这套库自带内存泄漏检测的功能选项。
    • ④出问题了使用内存泄漏工具检测。( 不过很多工具都不够靠谱,或者收费昂贵 )

                                                     

    总结:  内存泄漏非常常见,解决方案分为两种

    • 1、事前预防型。如智能指针等。
    • 2、事后查错型。如泄漏检测工具。

                     

                          

                              

    智能指针的使用及原理

            

    1.RAII

    (1)基本概念

    ①RAII(Resource Acquisition Is Initialization)是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
    ②在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内始终保持有效,最后在对象析构的时候释放资源。借此, 我们实际上把管理一份资源的责任托管给了一个对象 。这种做法有两大好处
    • 不需要显式地释放资源。
    • 采用这种方式,对象所需的资源在其生命期内始终保持有效。

                                             

     (2)代码模拟

    实现智能指针时需要考虑以下三个方面的问题:

    • 在对象构造时获取资源,在对象析构的时候释放资源,利用对象的生命周期来控制程序资源,即RAII特性。
    • *->运算符进行重载,使得该对象具有像指针一样的行为。
    • 智能指针对象的拷贝问题。
    1. // RAII
    2. // 用起来像指针一样
    3. template<class T>
    4. class SmartPtr
    5. {
    6. public:
    7. SmartPtr(T* ptr)
    8. :_ptr(ptr)
    9. {}
    10. ~SmartPtr()
    11. {
    12. cout << "delete:" << _ptr << endl;
    13. delete _ptr;
    14. }
    15. // 像指针一样使用
    16. T& operator*()
    17. {
    18. return *_ptr;
    19. }
    20. T* operator->()
    21. {
    22. return _ptr;
    23. }
    24. private:
    25. T* _ptr;
    26. };

                    

    (3)为什么要解决智能指针对象的拷贝问题

    对于当前实现的SmartPtr类,如果用一个SmartPtr对象来拷贝构造另一个SmartPtr对象,或是将一个SmartPtr对象赋值给另一个SmartPtr对象,都会导致程序崩溃

    1. int main()
    2. {
    3. SmartPtr<int> sp1(new int);
    4. SmartPtr<int> sp2(sp1); //拷贝构造
    5. SmartPtr<int> sp3(new int);
    6. SmartPtr<int> sp4(new int);
    7. sp3 = sp4; //拷贝赋值
    8. return 0;
    9. }
    • 编译器默认生成的拷贝构造函数对内置类型完成值拷贝(浅拷贝),因此用sp1拷贝构造sp2后,相当于这sp1和sp2管理了同一块内存空间,当sp1和sp2析构时就会导致这块空间被释放两次。
    • 编译器默认生成的拷贝赋值函数对内置类型也是完成值拷贝(浅拷贝),因此将sp4赋值给sp3后,相当于sp3和sp4管理的都是原来sp3管理的空间,当sp3和sp4析构时就会导致这块空间被释放两次,并且还会导致sp4原来管理的空间没有得到释放。
    • 需要注意的是,智能指针就是要模拟原生指针的行为,当我们将一个指针赋值给另一个指针时,目的就是让这两个指针指向同一块内存空间,所以这里本就应该进行浅拷贝,但单纯的浅拷贝又会导致空间被多次释放,因此根据解决智能指针拷贝问题方式的不同,从而衍生出了不同版本的智能指针。
       

                            

    2.std::auto_ptr

    (1)C++98库中提供了auto_ptr ,原理是实现管理权转移

    1. int main()
    2. {
    3. std::auto_ptr<int> sp1(new int);
    4. std::auto_ptr<int> sp2(sp1); //管理权的转移
    5. //最大的问题在于 sp1悬空
    6. *sp2 = 10;
    7. cout << *sp2 << endl; //10
    8. cout << *sp1 << endl; //error
    9. return 0;
    10. }
    • 保证一个资源在任何时刻都只有一个对象在对其进行管理,这时同一个资源就不会被多次释放了 
    • auto_ptr的问题:当对象拷贝或者赋值后,前面的对象就悬空了
    • C++98中设计的auto_ptr问题是非常明显的,所以实际中很多公司明确规定了不能使用auto_ptr

                     

    (2)模拟实现auto_ptr

    • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
    • 对*和->运算符进行重载,使auto_ptr对象具有指针一样的行为。
    • 在拷贝构造函数中,用传入对象管理的资源来构造当前对象,并将传入对象管理资源的指针置空。
    • 在拷贝赋值函数中,先将当前对象管理的资源释放,然后再接管传入对象管理的资源,最后将传入对象管理资源的指针置空。
    1. template<class T>
    2. class auto_ptr
    3. {
    4. public:
    5. auto_ptr(T* ptr)
    6. :_ptr(ptr)
    7. {}
    8. auto_ptr(auto_ptr& sp)
    9. :_ptr(sp._ptr)
    10. {
    11. // 管理权转移
    12. sp._ptr = nullptr;
    13. }
    14. auto_ptr& operator=(auto_ptr& sp)
    15. {
    16. if (this != &ap)
    17. {
    18. delete _ptr; //释放自己管理的资源
    19. _ptr = sp._ptr; //接管sp对象的资源
    20. sp._ptr = nullptr; //管理权转移后sp被置空
    21. }
    22. return *this;
    23. }
    24. ~auto_ptr()
    25. {
    26. if (_ptr)
    27. {
    28. cout << "delete:" << _ptr << endl;
    29. delete _ptr;
    30. }
    31. }
    32. // 像指针一样使用
    33. T& operator*()
    34. {
    35. return *_ptr;
    36. }
    37. T* operator->()
    38. {
    39. return _ptr;
    40. }
    41. private:
    42. T* _ptr;
    43. };

            

                            

    3.std::unique_ptr

    (1)C++11更新智能指针

    •  boost  ->  scoped_ptr / shared_ptr / weak_ptr
    • C++11库才更新智能指针实现
    • C++11将boost库中智能指针精华部分吸收了过来
    •  C++11 -> unique_ptr / shared_ptr / weak_ptr

                                     

    (2)unique_ptr模拟实现

    • 原理:简单粗暴 -- 防拷贝
    • 在构造函数中获取资源,在析构函数中释放资源,利用对象的生命周期来控制资源。
    • *->运算符进行重载,使unique_ptr对象具有指针一样的行为。
    • 用C++98的方式将拷贝构造函数和拷贝赋值函数声明为私有,或者用C++11的方式在这两个函数后面加上 =delete,防止外部调用。
    1. namespace XM
    2. {
    3. template<class T>
    4. class unique_ptr
    5. {
    6. public:
    7. unique_ptr(T* ptr = nullptr)
    8. : _ptr(ptr)
    9. {}
    10. ~unique_ptr()
    11. {
    12. if (_ptr)
    13. delete _ptr;
    14. }
    15. T& operator*() { return *_ptr; }
    16. T* operator->() { return _ptr; }
    17. private:
    18. // C++98防拷贝的方式:只声明不实现+声明成私有
    19. unique_ptr(const unique_ptr& sp);
    20. unique_ptr& operator=(const unique_ptr& sp);
    21. // C++11防拷贝的方式:delete
    22. unique_ptr(const unique_ptr& sp) = delete;
    23. unique_ptr& operator=(const unique_ptr& sp) = delete;
    24. private:
    25. T* _ptr;
    26. };
    27. }

            

                                    

    4.std::shared_ptr

    (1)C++11中开始提供更靠谱的并且支持拷贝的shared_ptr

    1. class Date
    2. {
    3. public:
    4. Date() { cout << "Date()" << endl;}
    5. ~Date(){ cout << "~Date()" << endl;}
    6. int _year = 0;
    7. int _month = 0;
    8. int _day = 0;
    9. };
    10. int main()
    11. {
    12. // shared_ptr通过引用计数支持智能指针对象的拷贝
    13. std::shared_ptr sp(new Date);
    14. std::shared_ptr copy(sp);
    15. cout << "ref count:" << sp.use_count() << endl;
    16. cout << "ref count:" << copy.use_count() << endl;
    17. return 0;
    18. }

                    

     (2)share_ptr原理

    • shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源
    • shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享
    • 对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
    • 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源
    • 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了

                     

    (3)简单模拟实现share_ptr

    • 在shared_ptr类中增加一个成员变量count,表示智能指针对象管理的资源对应的引用计数。
    • 在构造函数中获取资源,并将该资源对应的引用计数设置为1,表示当前只有一个对象在管理这个资源。
    • 在拷贝构造函数中,与传入对象一起管理它管理的资源,同时将该资源对应的引用计数++。
    • 在拷贝赋值函数中,先将当前对象管理的资源对应的引用计数--(如果减为0则需要释放),然后再与传入对象一起管理它管理的资源,同时需要将该资源对应的引用计数++。
    • 在析构函数中,将管理资源对应的引用计数--,如果减为0则需要将该资源释放。
    • 对*和->运算符进行重载,使shared_ptr对象具有指针一样的行为。
    1. namespace XM
    2. {
    3. template<class T>
    4. class shared_ptr
    5. {
    6. public:
    7. shared_ptr(T* ptr)
    8. :_ptr(ptr)
    9. , _pRefCount(new int(1))
    10. {}
    11. shared_ptr(const shared_ptr& sp)
    12. :_ptr(sp._ptr)
    13. , _pRefCount(sp._pRefCount)
    14. {
    15. ++(*_pRefCount);
    16. }
    17. shared_ptr& operator=(const shared_ptr& sp)
    18. {
    19. if (_ptr != sp._ptr)
    20. {
    21. if (--(*_pRefCount) == 0)
    22. {
    23. delete _ptr;
    24. delete _pRefCount;
    25. }
    26. _ptr = sp._ptr;
    27. _pRefCount = sp._pRefCount;
    28. ++(*_pRefCount);
    29. }
    30. return *this;
    31. }
    32. ~shared_ptr()
    33. {
    34. if (--(*_pRefCount) == 0 && _ptr)
    35. {
    36. cout << "delete:" << _ptr << endl;
    37. delete _ptr;
    38. delete _pRefCount;
    39. //_ptr = nullptr;
    40. //_pRefCount = nullptr;
    41. }
    42. }
    43. int use_count() const
    44. {
    45. return *_pRefCount;
    46. }
    47. // 像指针一样使用
    48. T& operator*()
    49. {
    50. return *_ptr;
    51. }
    52. T* operator->()
    53. {
    54. return _ptr;
    55. }
    56. private:
    57. T* _ptr;
    58. int* _pRefCount;
    59. };
    60. }

    (4)为什么引用计数要放在堆区

    • ①首先,shared_ptr中的引用计数count不能单纯的定义成一个int类型的成员变量,因为这就意味着每个shared_ptr对象都有一个自己的count成员变量,而当多个对象要管理同一个资源时,这几个对象应该用到的是同一个引用计数。
    • ②其次,shared_ptr中的引用计数count也不能定义成一个静态的成员变量,因为静态成员变量是所有类型对象共享的,这会导致管理相同资源的对象和管理不同资源的对象用到的都是同一个引用计数。
    • ③而如果将shared_ptr中的引用计数count定义成一个指针,当一个资源第一次被管理时就在堆区开辟一块空间用于存储其对应的引用计数,如果有其他对象也想要管理这个资源,那么除了将这个资源给它之外,还需要把这个引用计数也给它。
    • ④这时管理同一个资源的多个对象访问到的就是同一个引用计数,而管理不同资源的对象访问到的就是不同的引用计数了,相当于将各个资源与其对应的引用计数进行了绑定。
    • ⑤但同时需要注意,由于引用计数的内存空间也是在堆上开辟的,因此当一个资源对应的引用计数减为0时,除了需要将该资源释放,还需要将该资源对应的引用计数的内存空间进行释放。
       

                     

    5.线程安全问题

    (1)线程不安全测试,这里我们自己实现的share_ptr未加锁

    1. #include
    2. #include
    3. #include
    4. #include
    5. using namespace std;
    6. struct Date
    7. {
    8. int _year = 0;
    9. int _month = 0;
    10. int _day = 0;
    11. };
    12. namespace XM
    13. {
    14. template<class T>
    15. class shared_ptr
    16. {
    17. public:
    18. shared_ptr(T* ptr)
    19. :_ptr(ptr)
    20. , _pRefCount(new int(1))
    21. {}
    22. shared_ptr(const shared_ptr& sp)
    23. :_ptr(sp._ptr)
    24. , _pRefCount(sp._pRefCount)
    25. {
    26. AddRef();
    27. }
    28. shared_ptr& operator=(const shared_ptr& sp)
    29. {
    30. if (_ptr != sp._ptr)
    31. {
    32. Release();
    33. _ptr = sp._ptr;
    34. _pRefCount = sp._pRefCount;
    35. AddRef();
    36. }
    37. return *this;
    38. }
    39. ~shared_ptr()
    40. {
    41. Release();
    42. }
    43. T* get() const
    44. {
    45. return _ptr;
    46. }
    47. int use_count()
    48. {
    49. return *_pRefCount;
    50. }
    51. T& operator*()
    52. {
    53. return *_ptr;
    54. }
    55. T* operator->()
    56. {
    57. return _ptr;
    58. }
    59. private:
    60. void Release()
    61. {
    62. if (--(*_pRefCount) == 0 && _ptr)
    63. {
    64. delete _ptr;
    65. delete _pRefCount;
    66. }
    67. }
    68. void AddRef() //增加计数
    69. {
    70. ++(*_pRefCount);
    71. }
    72. private:
    73. T* _ptr;
    74. int* _pRefCount;
    75. };
    76. }
    77. void SharePtrFunc(XM::shared_ptr& sp, size_t n,mutex& mtx)
    78. {
    79. cout << sp.get() << endl;
    80. for (size_t i = 0; i < n; ++i)
    81. {
    82. // 这里智能指针拷贝会++计数,智能指针析构会--计数,自己模拟实现是不安全的
    83. XM::shared_ptr copy(sp);
    84. {
    85. unique_lock lk(mtx);
    86. copy->_year++;
    87. copy->_month++;
    88. copy->_day++;
    89. }
    90. }
    91. }
    92. int main()
    93. {
    94. XM::shared_ptr p(new Date);
    95. cout << p.get() << endl;
    96. const size_t n = 10000;
    97. mutex mtx;
    98. thread t1(SharePtrFunc, std::ref(p), n,std::ref(mtx));
    99. thread t2(SharePtrFunc, std::ref(p), n,std::ref(mtx));
    100. t1.join();
    101. t2.join();
    102. cout << p->_year << endl;
    103. cout << p->_month << endl;
    104. cout << p->_day << endl;
    105. cout << p.use_count() << endl;
    106. return 0;
    107. }

                     

    • ①通过实验结果可知,如果share_ptr不加锁在多线程的情况下是不安全的,在pRefCount ++,- - 时  可能出现错误
    • ②智能指针对象中引用计数是多个智能指针对象共享的,两个线程中智能指针的引用计数同时++或--,这个操作不是原子的,引用计数原来是1,++了两次,可能还是2.这样引用计数就错乱了。会导致资源未释放或者程序崩溃的问题。所以只能指针中引用计数++、--是需要加锁的,也就是说引用计数的操作是线程安全的。
    • ③智能指针管理的对象存放在堆上,两个线程中同时去访问,会导致线程安全问题
    • ④这里智能指针访问管理的资源,不是线程安全的;对Date的成员 ++ , 所以我们看看这些值两个线程++了2n次,但是最终看到的结果,并一定是加了2n ; 为了保证线程安全还要手动加锁

    (2)代码测试总结

    ①加锁保证数据安全,局部域,锁只保护这一个局部域,出了局部域就解锁了

    1. int main()
    2. {
    3. { //局部域
    4. unique_lock lk(mtx);
    5. copy->_year++;
    6. copy->_month++;
    7. copy->_day++;
    8. }
    9. //Do something ... 不需要加锁
    10. }

                     

    ②shared_ptr智能指针是线程安全的吗?

    •  是的,引用计数的加减是加锁保护的。但是指向的资源不是线程安全的,需要自己管
    • 指向堆上资源的线程安全问题是访问的人处理的,智能指针不管,也管不了; 引用计数的线程安全问题,是智能指针要处理的

                     

    ③模拟线程不安全的情况尽量控制在某一个方面不安全,不要多个方面都是线程不安全的,这样线程出现错误的概率大大增加,很难模拟出想要的结果。

            

    (3)模拟线程安全的代码 , 引用计数加锁

    要解决引用计数的线程安全问题,本质就是要让对引用计数的自增和自减操作变成一个原子操作,因此可以对引用计数的操作进行加锁保护,也可以用原子类atomic对引用计数进行封装,这里以加锁为例

    • 在shared_ptr类中新增互斥锁成员变量,为了让管理同一个资源的多个线程访问到的是同一个互斥锁,管理不同资源的线程访问到的是不同的互斥锁,因此互斥锁也需要在堆区创建
    • 在调用拷贝构造函数和拷贝赋值函数时,除了需要将对应的资源和引用计数交给当前对象管理之外,还需要将对应的互斥锁也交给当前对象。
    • 当一个资源对应的引用计数减为0时,除了需要将对应的资源和引用计数进行释放,由于互斥锁也是在堆区创建的,因此还需要将对应的互斥锁进行释放。
    • 为了简化代码逻辑,可以将拷贝构造函数和拷贝赋值函数中引用计数的自增操作提取出来,封装成AddRef函数,将拷贝赋值函数和析构函数中引用计数的自减操作提取出来,封装成Release函数,这样就只需要对AddRef和Release函数进行加锁保护即可。
    1. namespace XM
    2. {
    3. template<class T>
    4. class shared_ptr
    5. {
    6. public:
    7. shared_ptr(T* ptr)
    8. :_ptr(ptr)
    9. , _pRefCount(new int(1))
    10. ,_pmtx(new mutex)
    11. {}
    12. shared_ptr(const shared_ptr& sp)
    13. :_ptr(sp._ptr)
    14. , _pRefCount(sp._pRefCount)
    15. ,_pmtx(sp._pmtx)
    16. {
    17. AddRef();
    18. }
    19. shared_ptr& operator=(const shared_ptr& sp)
    20. {
    21. //if (this != &sp) 这样判断不太好,防止自己给自己赋值应该判断指针的值是否相同
    22. if (_ptr != sp._ptr)
    23. {
    24. Release();
    25. _ptr = sp._ptr;
    26. _pRefCount = sp._pRefCount;
    27. _pmtx = sp._pmtx;
    28. AddRef();
    29. }
    30. return *this;
    31. }
    32. ~shared_ptr()
    33. {
    34. Release();
    35. }
    36. T* get() const
    37. {
    38. return _ptr;
    39. }
    40. int use_count()
    41. {
    42. return *_pRefCount;
    43. }
    44. T& operator*()
    45. {
    46. return *_ptr;
    47. }
    48. T* operator->()
    49. {
    50. return _ptr;
    51. }
    52. private:
    53. void Release() //释放资源
    54. {
    55. _pmtx->lock();
    56. bool flag = false;
    57. if (--(*_pRefCount) == 0 && _ptr)
    58. {
    59. delete _ptr;
    60. delete _pRefCount;
    61. flag = true; //锁不能在这里释放,因为后面要解锁
    62. }
    63. _pmtx->unlock();
    64. if (flag == true)
    65. {
    66. delete _pmtx;
    67. }
    68. }
    69. void AddRef() //增加计数
    70. {
    71. _pmtx->lock();
    72. ++(*_pRefCount);
    73. _pmtx->unlock();
    74. }
    75. private:
    76. T* _ptr;
    77. int* _pRefCount;
    78. mutex* _pmtx;
    79. };
    80. }

     

    ②小结

    • 在Release函数中,当引用计数被减为0时需要释放互斥锁资源,但不能在临界区中释放互斥锁,因为后面还需要进行解锁操作,因此代码中借助了一个flag变量,通过flag变量来判断解锁后释放需要释放互斥锁资源。
    • shared_ptr只需要保证引用计数的线程安全问题,而不需要保证管理的资源的线程安全问题,就像原生指针管理一块内存空间一样,原生指针只需要指向这块空间,而这块空间的线程安全问题应该由这块空间的操作者来保证

     6.std::share_ptr的循环引用

    (1)测试代码

    1. struct ListNode
    2. {
    3. int _val;
    4. std::shared_ptr _next;
    5. std::shared_ptr _prev;
    6. ~ListNode()
    7. {
    8. cout << "~ListNode()" << endl;
    9. }
    10. };
    11. int main()
    12. {
    13. std::shared_ptr n1(new ListNode);
    14. std::shared_ptr n2(new ListNode);
    15. cout << n1.use_count() << endl;
    16. cout << n2.use_count() << endl;
    17. // 循环引用
    18. n1->_next = n2;
    19. n2->_prev = n1;
    20. cout << n1.use_count() << endl;
    21. cout << n2.use_count() << endl;
    22. return 0;
    23. }

    • shared_ptr的循环引用问题在一些特定的场景下才会产生。比如定义结点类,并在结点类的析构函数中打印一句提示语句,便于判断结点是否正确释放。最终的结果是未打印提示语句,即结点未释放
    • 但如果去掉连接结点时的两句代码中的任意一句,那么这两个结点就都能够正确释放,根本原因就是因为这两句连接结点的代码导致了循环引用

    (2)循环引用分析

    •  node1和node2两个智能指针对象指向两个节点,引用计数变成1,我们不需要手动delete。
    • node1的_next指向node2,node2的_prev指向node1,引用计数变成2。
    •  node1和node2析构,引用计数减到1,但是_next还指向下一个节点。但是_prev还指向上一个节点。
    • 也就是说_next析构了,node2就释放了。
    • 也就是说_prev析构了,node1就释放了。
    • 但是_next属于node的成员,node1释放了,_next才会析构,而node1由_prev管理,_prev属于node2成员,所以这就叫循环引用,谁也不会释放

     

    (3)循环引用的解决方式 weak_ptr 

    ①weak_ptr介绍

    • weak_ptr是C++11中引入的智能指针,weak_ptr不是用来管理资源的释放的,它主要是用来解决shared_ptr的循环引用问题的。
    • weak_ptr支持用shared_ptr对象来构造weak_ptr对象,构造出来的weak_ptr对象与shared_ptr对象管理同一个资源,但不会增加这块资源对应的引用计数。

                     

    将ListNode中的next和prev成员的类型换成weak_ptr就不会导致循环引用问题了,此时当node1和node2生命周期结束时两个资源对应的引用计数就都会被减为0,进而释放这两个结点的资源。

    1. struct ListNode
    2. {
    3. std::weak_ptr _next;
    4. std::weak_ptr _prev;
    5. int _val;
    6. ~ListNode()
    7. {
    8. cout << "~ListNode()" << endl;
    9. }
    10. };
    11. int main()
    12. {
    13. std::shared_ptr node1(new ListNode);
    14. std::shared_ptr node2(new ListNode);
    15. cout << node1.use_count() << endl;
    16. cout << node2.use_count() << endl;
    17. node1->_next = node2;
    18. node2->_prev = node1;
    19. //...
    20. cout << node1.use_count() << endl;
    21. cout << node2.use_count() << endl;
    22. return 0;
    23. }

                             

    (4)weak_ptr模拟实现

    • 提供一个无参的构造函数,比如刚才new ListNode时就会调用weak_ptr的无参的构造函数。
    • 支持用shared_ptr对象拷贝构造weak_ptr对象,构造时获取shared_ptr对象管理的资源。
    • 支持用shared_ptr对象拷贝赋值给weak_ptr对象,赋值时获取shared_ptr对象管理的资源。
    • 对* 和 ->运算符进行重载,使weak_ptr对象具有指针一样的行为。
    1. namespace cl
    2. {
    3. template<class T>
    4. class weak_ptr
    5. {
    6. public:
    7. weak_ptr()
    8. :_ptr(nullptr)
    9. {}
    10. weak_ptr(const shared_ptr& sp)
    11. :_ptr(sp.get())
    12. {}
    13. weak_ptr& operator=(const shared_ptr& sp)
    14. {
    15. _ptr = sp.get();
    16. return *this;
    17. }
    18. //可以像指针一样使用
    19. T& operator*()
    20. {
    21. return *_ptr;
    22. }
    23. T* operator->()
    24. {
    25. return _ptr;
    26. }
    27. private:
    28. T* _ptr; //管理的资源
    29. };
    30. }

                    

                      

                                  

    定制删除器

                    

    1.关于new 和 delete 的补充

    • 如果A的析构函数没有显示写,这里不会报错也不会有内存泄漏,原因:  new底层是用malloc开辟空间,delete底层是free,free不管你开辟多少空间,开多少释放多少空间。
    • 如果A的析构函数显示写,这里就会出问题,原因 :  new的时候如果有析构函数的情况下,假设一个对象是4字节,10个对象是40个字节,它不会只开40个字节,它还要在头部多开4个字节去存对象的个数,delete的时候,delete[]没有指明delete几个对象,它去头部取那4个字节,发现是10就调用10次析构函数

                            

                     

     2.定制删除器的用法

    (1)错误用法

    • 当智能指针对象的生命周期结束时,所有的智能指针默认都是以 delete 的方式将资源释放,这是不太合适的,因为智能指针并不是只管理以 new 方式申请到的内存空间,智能指针管理的也可能是以 new[ ] 的方式申请到的空间,或管理的是一个文件指针
    • 这时当智能指针对象的生命周期结束时,再以 delete 的方式释放管理的资源就会导致程序崩溃,因为以 new[ ] 的方式申请到的内存空间必须以 delete[ ] 的方式进行释放,而文件指针必须通过调用 fclose 函数进行释放。
    1. struct ListNode
    2. {
    3. ListNode* _next;
    4. ListNode* _prev;
    5. int _val;
    6. ~ListNode()
    7. {
    8. cout << "~ListNode()" << endl;
    9. }
    10. };
    11. int main()
    12. {
    13. std::shared_ptr sp1(new ListNode[10]); //error
    14. std::shared_ptr sp2(fopen("test.cpp", "r")); //error
    15. return 0;
    16. }

                    

    (2)正确用法

    1. template <class U, class D>
    2. shared_ptr (U* p, D del);

     ①参数

    • p:需要让智能指针管理的资源。
    • del:删除器,这个删除器是一个可调用对象,比如函数指针、仿函数、lambda表达式以及被包装器包装后的可调用对象。

    ②当shared_ptr对象的生命周期结束时就会调用传入的删除器完成资源的释放,调用该删除器时会将shared_ptr管理的资源作为参数进行传入。

                    

    ③因此当智能指针管理的资源不是以 new 的方式申请到的内存空间时,就需要在构造智能指针对象时传入定制的删除器

    1. template<class T>
    2. struct DelArr
    3. {
    4. void operator()(const T* ptr)
    5. {
    6. cout << "delete[]: " << ptr << endl;
    7. delete[] ptr;
    8. }
    9. };
    10. int main()
    11. {
    12. std::shared_ptr sp1(new ListNode[10], DelArr()); //仿函数
    13. std::shared_ptr sp2(fopen("test.cpp", "r"), [](FILE* ptr){
    14. cout << "fclose: " << ptr << endl;
    15. fclose(ptr);
    16. }); //lamba表达式
    17. return 0;
    18. }

                     

    (3)模拟实现

    • C++标准库中实现shared_ptr时是分成了很多个类的,因此C++标准库中可以将删除器的类型设置为构造函数的模板参数,然后将删除器的类型在各个类之间进行传递。
    • 但我们是直接用一个类来模拟实现shared_ptr的,因此不能将删除器的类型设置为构造函数的模板参数。因为删除器不是在构造函数中调用的,而是需要在Release函数中进行调用,因此势必需要用一个成员变量将删除器保存下来,而在定义这个成员变量时就需要指定删除器的类型,因此这里模拟实现的时候不能将删除器的类型设置为构造函数的模板参数。
    • 要在当前模拟实现的shared_ptr的基础上支持定制删除器,就只能给shared_ptr类再增加一个模板参数,在构造shared_ptr对象时就需要指定删除器的类型。然后增加一个支持传入删除器的构造函数,在构造对象时将删除器保存下来,在需要释放资源的时候调用该删除器进行释放即可。最好在设置一个默认的删除器,如果用户定义shared_ptr对象时不传入删除器,则默认以delete的方式释放资源。
    1. namespace XM
    2. {
    3. //默认的删除器
    4. template<class T>
    5. struct Delete
    6. {
    7. void operator()(const T* ptr)
    8. {
    9. delete ptr;
    10. }
    11. };
    12. template<class T, class D = Delete>
    13. class shared_ptr
    14. {
    15. private:
    16. void Release()
    17. {
    18. _pmtx->lock();
    19. bool flag = false;
    20. if (--(*_pRefCount) == 0 && _ptr) //将管理的资源对应的引用计数--
    21. {
    22. _del(_ptr); //使用定制删除器释放资源
    23. delete _pRefCount;
    24. flag = true;
    25. }
    26. _pmtx->unlock();
    27. if (flag == true)
    28. {
    29. delete _pmtx;
    30. }
    31. }
    32. //...
    33. public:
    34. shared_ptr(T* ptr, D del)
    35. : _ptr(ptr)
    36. , _pRefCount(new int(1))
    37. , _pmtx(new mutex)
    38. , _del(del)
    39. {}
    40. //...
    41. private:
    42. T* _ptr; //管理的资源
    43. int* _pRefCount; //管理的资源对应的引用计数
    44. mutex* _pmtx; //管理的资源对应的互斥锁
    45. D _del; //管理的资源对应的删除器
    46. };
    47. }

                                    

    我们模拟出来的删除器但使用起来没有C++标准库中的那么方便

    • 如果传入的删除器是一个仿函数,那么需要在构造shared_ptr对象时指明仿函数的类型。
    • 如果传入的删除器是一个lambda表达式就更麻烦了,因为lambda表达式的类型不太容易获取。这里可以将lambda表达式的类型指明为一个包装器类型,让编译器传参时自行进行推演,也可以先用auto接收lambda表达式,然后再用decltype来声明删除器的类型。
    1. template<class T>
    2. struct DelArr
    3. {
    4. void operator()(const T* ptr)
    5. {
    6. cout << "delete[]: " << ptr << endl;
    7. delete[] ptr;
    8. }
    9. };
    10. int main()
    11. {
    12. //仿函数示例
    13. XM::shared_ptr> sp1(new ListNode[10], DelArr());
    14. //lambda示例1
    15. XM::shared_ptrvoid(FILE*)>> sp2(fopen("test.cpp", "r"), [](FILE* ptr){
    16. cout << "fclose: " << ptr << endl;
    17. fclose(ptr);
    18. });
    19. //lambda示例2
    20. auto f = [](FILE* ptr){
    21. cout << "fclose: " << ptr << endl;
    22. fclose(ptr);
    23. };
    24. XM::shared_ptrdecltype(f)> sp3(fopen("test.cpp", "r"), f);
    25. return 0;
    26. }

                             

    (4)小结

    • 定制删除器,实际在平时的工作中使用有价值
    • 定制删除器的意义 : 默认情况,智能指针底层都是delete资源 ,那么如果你的资源不是new出来的呢?比如:new[]、malloc、fopen ,定制删除器 -- 传入可调用对象,自定义释放资源

                            

    C++11和boost中智能指针的关系

    • C++98中产生了第一个智能指针auto_ptr。
    • C++boost给出了更实用的scoped_ptr、shared_ptr和weak_ptr。
    • C++TR1,引入了boost中的shared_ptr等。不过注意的是TR1并不是标准版。
    • C++11,引入了boost中的unique_ptr、shared_ptr和weak_ptr。需要注意的是,unique_ptr对应的就是boost中的scoped_ptr,并且这些智能指针的实现原理是参考boost中实现的。
    • 说明:boost库是为C++语言标准库提供扩展的一些C++程序库的总称,boost库社区建立的初衷之一就是为C++的标准化工作提供可供参考的实现,比如在送审C++标准库TR1中,就有十个boost库成为标准库的候选方案。
       
  • 相关阅读:
    【git】常用命令
    Adobe Acrobat Pro DC 2023:提升工作效率,激发创意灵感 mac/win版
    【word】图表引用
    Prometheus+Ansible+Consul实现服务发现
    必看!2023年最新MSP开源应用程序指南电子书大揭秘
    数据结构——实现通讯录(附源码)
    JavaScript 变量提升的作用
    编程面试_动态规划
    VueDraggable 依赖怎么安装
    企业电子招投标系统源码之电子招投标系统建设的重点和未来趋势
  • 原文地址:https://blog.csdn.net/m0_52169086/article/details/127079838