• 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. int* p2 = new int;
    13. cout << div() << endl;
    14. cout << "delete p1" << endl;
    15. delete p1;
    16. cout << "delete p2" << endl;
    17. delete p2;
    18. }
    19. int main()
    20. {
    21. try{Func();}
    22. catch (exception& e)
    23. {cout << e.what() << endl;}
    24. return 0;
    25. }

    main函数中调用了func函数, func函数里面调用了div函数, func函数中没有捕捉异常, 但是在main函数里面却捕捉了异常, 所以出现异常的话就会导致func函数中的部分代码没有被执行, 进而导致内存泄漏:

    异常抛出正常内存释放: 

    有异常抛出, 内存泄漏:  

    为了解决这个问题就可以异常重新抛出, 在func函数里面添加捕获异常的代码, 然后在catch里面对资源进行释放最后重新将异常进行抛出, 最后交给main函数中的catch进行处理, 比如说下面的代码: 

    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. int* p2 = new int;
    13. try
    14. {
    15. cout << div() << endl;
    16. }
    17. catch (...)
    18. {
    19. cout << "delete p1" << endl;
    20. delete p1;
    21. cout << "delete p2" << endl;
    22. delete p2;
    23. throw;
    24. }
    25. cout << "delete p1" << endl;
    26. delete p1;
    27. cout << "delete p2" << endl;
    28. delete p2;
    29. }
    30. int main()
    31. {
    32. try { Func(); }
    33. catch (exception& e)
    34. {
    35. cout << e.what() << endl;
    36. }
    37. return 0;
    38. }

    无论除零是否有异常都会正常释放资源: 

    但是这么写就完成正确了吗? 肯定没有, 因为new本身也是会抛异常的, 当内存不足却又使用new申请空间的话就会导致开辟空间失败从而抛出异常, 那new抛异常会导致什么结果呢?

    首先p1抛出异常会有什么问题吗?

    没有, p1抛出异常会直接跳转到main函数里面进行捕捉并且p2还没有开辟, p1没有开辟成功, 从而不会导致任何的内存泄漏.

    那要是p2开辟失败了呢?

    这时候也是会直接跳转到main函数里面进行捕捉, 但是p1已经开辟空间了, 如果p2开辟空间失败他会导致p1的申请的资源没有被正常释放, 所以为了安全起见我们给p2也添加一个try上去并且try块里面还得含有后面的代码, 因为一旦内存申请失败后面的调用函数也无需执行了.

    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. try
    13. {
    14. int* p2 = new int;
    15. try
    16. {
    17. cout << div() << endl;
    18. }
    19. catch (...){
    20. cout << "delete p1" << endl;
    21. delete p1;
    22. cout << "delete p2" << endl;
    23. delete p2;
    24. throw;
    25. }
    26. cout << "delete p1" << endl;
    27. delete p1;
    28. cout << "delete p2" << endl;
    29. delete p2;
    30. }
    31. catch (...)
    32. {
    33. cout << "delete p1" << endl;
    34. delete p1;
    35. throw;
    36. }
    37. }
    38. int main()
    39. {
    40. try { Func(); }
    41. catch (exception& e)
    42. {
    43. cout << e.what() << endl;
    44. }
    45. return 0;
    46. }

    我们这里只new了两个对象, 那如果有三个有四个甚至更多的话又该如何来嵌套try catch呢? 所以当出现连续抛出异常的情况时, 我们之前学习的try catch语句就很难进行应对, 那么为了解决这个问题就有了智能指针


    智能指针的使用及原理

    RAII

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

    注意: 智能指针是RAII的一种实现方式.


    智能指针实现 

    首先智能指针是一个类, 并且这个类要处理各种各样的数据, 所以这个类就要是模板类, 比如:

    1. #pragma once
    2. template<class T>
    3. class SmartPoint
    4. {
    5. public:
    6. // RAII
    7. SmartPoint(T* ptr = nullptr)
    8. :_ptr(ptr)
    9. {}
    10. ~SmartPoint()
    11. {
    12. cout << "~SmartPoint" << endl;
    13. delete _ptr;
    14. }
    15. private:
    16. T* _ptr;
    17. };

    构造函数拿T类型的指针来初始化内部的_ptr就行, 析构函数在内部使用delete释放指针指向的空间即可.

    1. #include
    2. using namespace std;
    3. #include"SmartPoint.h"
    4. int div()
    5. {
    6. int a, b;
    7. cin >> a >> b;
    8. if (b == 0)
    9. throw invalid_argument("除0错误");
    10. return a / b;
    11. }
    12. void Func()
    13. {
    14. SmartPoint<int> sp1 = new int;
    15. SmartPoint<int> sp2 = new int;
    16. cout << div() << endl;
    17. //有了智能指针就不需要显示释放资源
    18. //cout << "delete p1" << endl;
    19. //delete p1;
    20. //cout << "delete p2" << endl;
    21. //delete p2;
    22. }
    23. int main()
    24. {
    25. try { Func(); }
    26. catch (exception& e)
    27. {
    28. cout << e.what() << endl;
    29. }
    30. return 0;
    31. }

    可以看到出现了除0错误这里也可以将两个申请的空间进行释放, 原理就是智能指针对象的生命周期属于Func函数, 当除0错误抛出异常的时候会从当前函数递归式的匹配catch直到main函数, 在匹配catch的时候虽然函数不会向下执行,但是函数栈帧会随之销毁, 其中类对象的生命周期也就跟着结束, 就会自动调用析构函数来释放空间, 就解决了之前的问题. 而且这样做不需要显式地释放资源, 而且对象所需的资源在其生命期内始终保持有效.


    智能指针的原理

    上述的SmartPtr还不能将其称为智能指针, 因为它还不具有指针的行为. 指针可以解引用, 也可以通过->去访问所指空间中的内容, 因此模板类中还需要将重载* 、->, 才可让其像指针一样去使用.

    1. template<class T>
    2. class SmartPoint
    3. {
    4. public:
    5. // RAII
    6. SmartPoint(T* ptr = nullptr)
    7. :_ptr(ptr)
    8. {}
    9. ~SmartPoint()
    10. {
    11. cout << "~SmartPoint" << endl;
    12. delete _ptr;
    13. }
    14. // 像指针一样
    15. T& oprator* ()
    16. {
    17. return *_ptr;
    18. }
    19. T* operator->()
    20. {
    21. return _ptr;
    22. }
    23. private:
    24. T* _ptr;
    25. };
    1. #include
    2. using namespace std;
    3. #include"SmartPoint.h"
    4. struct Date
    5. {
    6. int _year;
    7. int _month;
    8. int _day;
    9. };
    10. int main()
    11. {
    12. SmartPoint<int> sp1(new int);
    13. *sp1 = 10;
    14. cout << *sp1 << endl;
    15. SmartPoint sparray(new Date);
    16. // 需要注意的是这里应该是sparray.operator->()->_year = 2018;
    17. // 本来应该是sparray->->_year这里语法上为了可读性,省略了一个->
    18. sparray->_year = 2023;
    19. sparray->_month = 12;
    20. sparray->_day = 16;
    21. cout << "year:" << sparray->_year<< endl;
    22. cout << "month:" << sparray->_month << endl;
    23. cout << "day:" << sparray->_day << endl;
    24. return 0;
    25. }

    总结一下智能指针的原理:

    1. RAII特性.
    2. 重载operator*和opertaor->, 具有像指针一样的行为.


    C++库中的智能指针

    在这之前我们首先来看看下面这段代码:

    1. void func2()
    2. {
    3. SmartPoint<int>sp1(new int(10));
    4. SmartPoint<int>sp2(sp1);
    5. }
    6. int main()
    7. {
    8. func2();
    9. return 0;
    10. }

    原因很简单我们自己实现的类里面没有拷贝构造函数, 所以默认的拷贝构造函数以浅拷贝的方式进行构造, 所以对象的生命周期结束时就会调用delete将同一份空间析构两次, 所以就会报错.

    那么为了解决这个问题我们就得自己来实现一个拷贝构造函数, 可是这里的拷贝构造并不能是深拷贝, 因为我们这个类的目的是让它和指针一样对资源进行管理, 而并不是一个容器对资源进行存储. 所以这里就不能采用深拷贝的形式来进行拷贝构造, 那么库中是如何来解决这个问题的呢?

    先看看最早的auto_ptr如何解决这个问题: 

    std::auto_ptr

    std::auto_ptr文档

     C++98版本的库中就提供了auto_ptr的智能指针. auto_ptr的实现原理是 管理权转移的思想.

    简化模拟实现一份test::auto_ptr来了解它的原理 :

    1. namespace test
    2. {
    3. // C++98
    4. // 管理权转移,最后一个拷贝对象管理资源,被拷贝对象都被置空
    5. template<class T>
    6. class auto_ptr
    7. {
    8. public:
    9. // RAII
    10. auto_ptr(T* ptr)
    11. :_ptr(ptr)
    12. {}
    13. ~auto_ptr()
    14. {
    15. if (_ptr)
    16. {
    17. cout << "delete->" << _ptr << endl;
    18. delete _ptr;
    19. _ptr = nullptr;
    20. }
    21. }
    22. // ap2(ap1)
    23. auto_ptr(auto_ptr& ap)
    24. :_ptr(ap._ptr)
    25. {
    26. //管理权转移
    27. ap._ptr = nullptr;
    28. }
    29. auto_ptr& operator=(auto_ptr& ap)
    30. {
    31. // 检测是否为自己给自己赋值
    32. if (this != &ap)
    33. {
    34. // 释放当前对象中资源
    35. if (_ptr)
    36. delete _ptr;
    37. // 转移ap中资源到当前对象中
    38. _ptr = ap._ptr;
    39. ap._ptr = nullptr;
    40. }
    41. return *this;
    42. }
    43. // 像指针一样
    44. T& operator*()
    45. {
    46. return *_ptr;
    47. }
    48. T* operator->()
    49. {
    50. return _ptr;
    51. }
    52. private:
    53. T* _ptr;
    54. };
    55. }

    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;
    8. cout << *sp1 << endl;//解引用空指针
    9. return 0;
    10. }

    结论: auto_ptr是一个失败设计, 很多场景明确要求不能使用auto_ptr, 原因就是auto_ptr使用的方式太不合常理了.

    后来为了解决auto_ptr难用的问题, 就有了unique_ptrshare_ptr/weak_ptr .


    std::unique_ptr

    unique_ptr文档

    1. // C++11
    2. template<class T>
    3. class unique_ptr
    4. {
    5. public:
    6. // RAII
    7. unique_ptr(T* ptr)
    8. :_ptr(ptr)
    9. {}
    10. ~unique_ptr()
    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. // C++11
    25. unique_ptr(const unique_ptr& up) = delete;
    26. unique_ptr& operator=(const unique_ptr& up) = delete;
    27. private:
    28. // C++98
    29. // 1、只声明不实现
    30. // 2、限定为私有
    31. //unique_ptr(const unique_ptr& up);
    32. //unique_ptr& operator=(const unique_ptr& up);
    33. private:
    34. T* _ptr;
    35. };

    unique_ptr解决拷贝构造问题的思路简单粗暴, 直接禁止拷贝构造(拷贝构造设为私有或者C++11中设为delete).

    1. int main()
    2. {
    3. test::unique_ptr<int> sp1(new int);
    4. test::unique_ptr<int> sp2(sp1);
    5. test::unique_ptr<int> sp3(new int);
    6. sp3 = sp1;
    7. return 0;
    8. }


    std::shared_ptr

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

    std::shared_ptr文档

    shared_ptr的原理: 通过引用计数的方式来实现多个shared_ptr对象之间共享资源.

    1. shared_ptr在其内部, 给每个资源都维护了着一份计数, 用来记录该份资源被几个对象共享
    2. 在对象被销毁时(也就是析构函数调用), 就说明自己不使用该资源了, 对象的引用计数减一
    3. 如果引用计数是0, 就说明自己是最后一个使用该资源的对象, 必须释放该资源;
    4. 如果不是0, 就说明除了自己还有其他对象在使用该份资源, 不能释放该资源, 否则其他对象就成野指针了。 

     一个智能指针指向两块空间, 一块用于存储数据, 另一块记录当前空间被几个智能指针所指向, 当前只有对象sp1指向这个空间所以当前的计数就为1:

    当我们再创建一个对象sp2并指向这个空间时图片就变成了:

    当sp1对象生命周期结束, 或者sp1指向其他内容时, 就变成了: 

    当引用计数为0时, 空间被释放: 

    1. int main()
    2. {
    3. std::shared_ptr sp1(new string("xxxxxxxxxx"));
    4. std::shared_ptr sp2 = sp1;
    5. std::shared_ptr sp3;
    6. sp3 = sp1;
    7. std::shared_ptr sp4(new string("xxxxxxxxxxxxxxxxx"));
    8. sp3 = sp4;
    9. cout << *sp1 << endl;
    10. cout << *sp2 << endl;
    11. cout << *sp3 << endl;
    12. cout << *sp4 << endl;
    13. return 0;
    14. }

    可以看到使用share_ptr既不会出现拷贝完原指针置空(auto_ptr)的情况, 也不会出现禁止拷贝(unique_ptr)的情况, 而且这个智能指针跟普通的指针一样指向的是同一块区域的内容.

    模拟实现一下shared_ptr:

    首先有个问题: 引用计数的空间如何来分配?

    可以是个普通的整型变量放到对象里面吗?

    不可以, 因为当计数变量的值发生改变时, 所有指向该空间对象的内部计数变量都得改变, 此时只改变一个对象的引用计数对其它对象内存储的引用计数毫无影响.

    那么可以使用静态成员变量来实现吗?

    看上去可以, 因为不管类实例化出来了多少个对象, 这个静态变量只有一个, 并且所有对象都会共享这个静态变量, 那么这时只要一个对象对这个静态变量进行修改的话, 其他对象都会跟着一起修改, 这是不是达到了我们的目的呢?

    其实没有, 因为静态变量虽然一个对象修改所有对象都会共享, 但是这个对象指的是这个类所有实例化出来的对象, 假如又有一个智能指针指向一块新开辟的资源需要管理, 那么这个智能指针的引用计数需要初始化为1, 这就影响了之前的引用计数, 虽然是同一个类实例化出来的对象, 但是管理的资源可能会不同, 引用计数混在一起就乱套了.

    正确的方法是在类里面添加一个整型的指针变量, 让指针指向new开辟的一块空间:

    1. template<class T>
    2. class shared_ptr
    3. {
    4. public:
    5. // RAII
    6. shared_ptr(T* ptr = nullptr)
    7. :_ptr(ptr)
    8. ,_pcount(new int(1))
    9. {}
    10. ~shared_ptr()
    11. {
    12. if (--(*_pcount) == 0)
    13. {
    14. cout << "delete->" << _ptr << endl;
    15. delete _ptr;
    16. delete _pcount;
    17. }
    18. }
    19. // 像指针一样
    20. T& operator*()
    21. {
    22. return *_ptr;
    23. }
    24. T* operator->()
    25. {
    26. return _ptr;
    27. }
    28. private:
    29. T* _ptr;
    30. int* _pcount;
    31. };

    构造函数中将引用计数初始化为1, 调用一次析构函数引用计数就--一次, 当引用计数为0时就释放被管理的资源和为引用计数开辟的那块资源. 

    拷贝构造函数:

    1. // sp2(sp1)
    2. shared_ptr(const shared_ptr& sp)
    3. :_ptr(sp._ptr)
    4. ,_pcount(sp._pcount)
    5. {
    6. (*_pcount)++;
    7. }

     拷贝构造函数比较简单, 把_ptr和_pcount拷贝过来然后引用计数++即可.

    赋值重载就需要注意了: 

    1. //sp2 = sp1
    2. shared_ptr& operator=(const shared_ptr& sp)
    3. {
    4. //要考虑是不是自己给自己赋值
    5. if (_ptr != sp._ptr)
    6. {
    7. if (--*(_pcount) == 0)
    8. {
    9. delete _ptr;
    10. delete _pcount;
    11. }
    12. _ptr = sp._ptr;
    13. _pcount = sp._pcount;
    14. (*_pcount)++;
    15. }
    16. return *this;
    17. }

    首先要判断要拷贝的被拷贝的对象指向的是不是同一块空间:

    1. 如果是同一块空间(也就是自己给自己赋值)就没有必要进行拷贝, 或者说不能进行拷贝, 因为按照这个逻辑会先把引用计数--判断其是否为0决定原来的空间要不要释放, 如果当前引用计数不为1还好, 只是重复拷贝的问题, 但是如果引用计数为1, 先--引用计数然后把空间释放了, 再去拷贝这段已经被释放了的空间, 显然是不对的, 所以要先判断是否是自己给自己赋值.

    自己给自己赋值不要用this != &sp来判断了, 因为这里的自己赋值实际指的是底层管理的那段空间是不是一个空间, 所以用_ptr != sp._ptr来判断.

    2. 如果不是同一块空间, 先要把原来的引用计数--, 并判断是否为0, 为0就需要把原来的空间释放掉, 处理完原来的空间再去拷贝新的空间.


    std::shared_ptr的循环引用

    首先我们创建一个名为Listnode的类, 类里面含有两个listnode的指针和一个int的变量用来存储数据, 然后创建一个析构函数用来作为标记, 那么这里的代码就如下:

    1. struct Listnode
    2. {
    3. Listnode* next;
    4. Listnode* prev;
    5. int val;
    6. };
    7. void test_shared_ptr()
    8. {
    9. test::shared_ptr ptr1 = new Listnode;
    10. test::shared_ptr ptr2 = new Listnode;
    11. //ptr1->next = ptr2;
    12. //ptr2->prev = ptr1;
    13. }

    可以看到我希望用shared_ptr去管理创建的Listnode, 但是ptr1->next = ptr2 和ptr2->prev = ptr1的赋值肯定是不能兼容的, 所以改变一下Listnode的成员变量:

    1. struct Listnode
    2. {
    3. test::shared_ptr next;
    4. test::shared_ptr prev;
    5. int val;
    6. //顺便添加一个析构, 等会以便于观察
    7. ~Listnode()
    8. {
    9. cout << "~Listnode()" << endl;
    10. }
    11. };
    12. void test_shared_ptr()
    13. {
    14. test::shared_ptr n1 = new Listnode;
    15. test::shared_ptr n2 = new Listnode;
    16. //循环引用
    17. n1->next = n2;
    18. n2->prev = n1;
    19. }

    调用 test_shared_ptr():

    1. int main()
    2. {
    3. test_shared_ptr();
    4. return 0;
    5. }

     

    发现没有调用析构, 也就是发生了内存泄漏. 

    现在将下面一行代码注释掉, 看看是否能正常析构:

    1. void test_shared_ptr()
    2. {
    3. test::shared_ptr n1 = new Listnode;
    4. test::shared_ptr n2 = new Listnode;
    5. //循环引用
    6. n1->next = n2;
    7. //n2->prev = n1;
    8. }
    9. int main()
    10. {
    11. test_shared_ptr();
    12. return 0;
    13. }

    可以正常析构, 为什么呢? 

     这是一开始n1和n2都只是默认初始化时的状态:

     n1->next = n2之后, 指针指向变成了这样:

    这种情况下n1和n2可以正常, 按照构造函数相反的方向, n2的析构函数先调用, 引用计数--为1, 不发生任何改变, n1的析构函数再调用, 引用计数--为0, 调用delete _ptr也就是调用Listnode的析构函数, 打印~ListNode()后, prev先析构, 正常析构即可, next析构引用计数--为0, 释放原来n2指向的那片Listnode空间, 这部分空间内的两个shared_ptr都未初始化, 正常析构没有问题, 至此析构完成.

    再来重新分析这段代码的析构:

    1. void test_shared_ptr()
    2. {
    3. test::shared_ptr n1 = new Listnode;
    4. test::shared_ptr n2 = new Listnode;
    5. //循环引用
    6. n1->next = n2;
    7. n2->prev = n1;
    8. }

    同样的, n2先析构(*_pcount)--为1, n1再析构(*_pcount)--为1, 此时析构已经调用完成了, 引用计数都没有减到0, 不会对资源进行释放, 就发生了内存泄漏, 这也就是shared_ptr发生的循环引用问题.

    对于std:: shared_ptr也同样有这个问题, 而且库里的shared_ptr的构造函数加了explicit, 不能隐式类型转换了:

    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. void test_shared_ptr2()
    12. {
    13. std::shared_ptr n1(new Listnode);
    14. std::shared_ptr n2(new Listnode);
    15. //循环引用
    16. n1->next = n2;
    17. n2->prev = n1;
    18. }
    19. int main()
    20. {
    21. test_shared_ptr();
    22. return 0;
    23. }


    std::weak_ptr 

    上面的问题最根本在于我们默许prev和next指针参与资源的管理, 因此为了解决循环引用的问题,weak_ptr就出现了. 库里面的解决方式是把Listnode里的next和prev换成weak_ptr :

     可以看到weak_ptr是支持用shared_ptr去构造的.

    1. struct Listnode
    2. {
    3. int val;
    4. std::weak_ptr next;
    5. std::weak_ptr prev;
    6. ~Listnode()
    7. {
    8. cout << "~Listnode()" << endl;
    9. }
    10. };

    weak_ptr是一个有资源指向能力但没有管理权的指针, 所以无论创建多少个weak_ptr, 对应shared_ptr的引用计数也不会增加.

     weak_ptr特点

    1. 不是传统的智能指针, 不支持RAII

    2. weak_ptr中只有一个普通指针作为成员变量, 用来指向开辟的内存空间, 它不会去管理引用计数, 但是能去查看引用计数.

    3. 能像指针一样使用

     虽然weak_ptr不能去管理引用计数, 但是它需要能查看引用计数, 因为假如有一个shared_ptr和weak_ptr指向同一块空间, shared_ptr释放了但是weak_ptr并不知道, 为了解决这个问题, weak_ptr也有use_count接口可以查看引用计数, 其次它还有一个expired接口检查它是否"过期".

    1. void test_shared_ptr2()
    2. {
    3. std::shared_ptr n1(new Listnode);
    4. std::shared_ptr n2(new Listnode);
    5. cout << n1.use_count()<
    6. cout << n2.use_count() << endl;
    7. //循环引用
    8. n1->next = n2;
    9. n2->prev = n1;
    10. //weak_ptr接口
    11. cout << n1->next.use_count() << endl;
    12. cout << n2->prev.use_count() << endl;
    13. cout << n1->next.expired() << endl;;
    14. cout << n2->prev.expired() << endl;;
    15. }

     简单实现一个weak_ptr:

    1. template<class T>
    2. class weak_ptr
    3. {
    4. public:
    5. weak_ptr()
    6. :_ptr(nullptr)
    7. {}
    8. weak_ptr(const shared_ptr& sp)
    9. :_ptr(sp.get())
    10. {}
    11. weak_ptr& operator=(const shared_ptr& wp)
    12. {
    13. _ptr = wp.get();
    14. return *this;
    15. }
    16. //像指针一样使用
    17. T* operator->()
    18. {
    19. return _ptr;
    20. }
    21. T& operator*()
    22. {
    23. return *_ptr;
    24. }
    25. private:
    26. T* _ptr;
    27. };

    这里用shared_ptr去构造weak_ptr需要调用shared_ptr的get接口去得到管理的那部分空间的地址, 在shared_ptr里添加一个get:


    定制删除器 

    智能指针不光可以管理一个变量, 也可以管理一个数组。

    对于一个变量而言,可以使用delete _ptr,但对于一个数组而言,回收资源就需要用delete[] _ptr,可是我们之前实现的shared_ptr释放用的是delete, 所以去管理一个数组空间时析构会崩溃(std::shared_ptr也会崩溃):

    1. void test_shared_ptr3()
    2. {
    3. test::shared_ptr n(new string[10]);
    4. //std::shared_ptr n(new string[10]);
    5. }

    所以这里我们需要一个定制删除器来实现对不同资源的不同释放方式 :

     在构造shared_ptr时可以传递第二个 对象参数, 也就是定制删除器, 来实现自定义的删除方式.

    1. 我们可以选择传递一个仿函数对象: 

    1. template<class T>
    2. struct ListnodeDL
    3. {
    4. void operator()(T* ptr)
    5. {
    6. delete[] ptr;
    7. }
    8. };
    9. void test_shared_ptr3()
    10. {
    11. std::shared_ptr p(new Listnode[10], ListnodeDL());
    12. }
    13. int main()
    14. {
    15. test_shared_ptr3();
    16. return 0;
    17. }

    2. 还可以选择传递一个lambda对象, 更加方便:

    1. void test_shared_ptr3()
    2. {
    3. std::shared_ptr p(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });
    4. }

     3. 还可以去管理一个文件指针:

    1. void test_shared_ptr3()
    2. {
    3. std::shared_ptr p3(fopen("test.cpp", "r"), [](FILE* ptr) {fclose(ptr); });
    4. }

     了解完定制删除器后, 现在可以为之前模拟实现的shared_ptr添加上删除器:

     

    可以看到新添加了一个包装器成员_del, 因为我们要在释放资源时中去调用删除器自己实现的删除功能, 它是一个返回值类型为void参数为T*类型的函数包装器.

    注意: 这里_del要给一个缺省值, 或者在单参数的构造里初始化列表初始化, 因为之前的delete注释掉了, 对于普通场景的资源释放也需要处理.

    完整代码: 

    1. template<class T>
    2. class shared_ptr
    3. {
    4. public:
    5. // RAII
    6. shared_ptr(T* ptr = nullptr)
    7. :_ptr(ptr)
    8. ,_pcount(new int(1))
    9. {}
    10. template<class D>
    11. shared_ptr(T* ptr, D del)
    12. :_ptr(ptr)
    13. ,_pcount(new int(1))
    14. ,_del(del)
    15. {}
    16. ~shared_ptr()
    17. {
    18. if (--(*_pcount) == 0)
    19. {
    20. //cout << "delete->" << _ptr << endl;
    21. //delete _ptr; 有了定制删除器, 就不能直接用delete了
    22. _del(_ptr);
    23. delete _pcount;
    24. }
    25. }
    26. // sp2(sp1)
    27. shared_ptr(const shared_ptr& sp)
    28. :_ptr(sp._ptr)
    29. ,_pcount(sp._pcount)
    30. {
    31. (*_pcount)++;
    32. }
    33. //sp2 = sp1
    34. shared_ptr& operator=(const shared_ptr& sp)
    35. {
    36. if (_ptr != sp._ptr)
    37. {
    38. (*_pcount)--;
    39. if (*_pcount == 0)
    40. {
    41. //delete _ptr;
    42. _del(_ptr);
    43. delete _pcount;
    44. }
    45. _ptr = sp._ptr;
    46. _pcount = sp._pcount;
    47. (*_pcount)++;
    48. }
    49. return *this;
    50. }
    51. T* get() const
    52. {
    53. return _ptr;
    54. }
    55. // 像指针一样
    56. T& operator*()
    57. {
    58. return *_ptr;
    59. }
    60. T* operator->()
    61. {
    62. return _ptr;
    63. }
    64. private:
    65. T* _ptr;
    66. int* _pcount;
    67. function<void(T*)>_del = [](T* ptr) {delete ptr; };
    68. };

    测试一下: 

    1. void test_shared_ptr3()
    2. {
    3. test::shared_ptr p1(new Listnode[10], ListnodeDL());
    4. test::shared_ptr p2(new Listnode[10], [](Listnode* ptr) {delete[] ptr; });
    5. test::shared_ptr p3(fopen("test.txt","r"), [](FILE* ptr) {fclose(ptr); });
    6. test::shared_ptr p4(new Listnode);
    7. }
    8. int main()
    9. {
    10. test_shared_ptr3();
    11. return 0;
    12. }

    传alloc参数 

    此外, 库中还有一种构造可以传Allocator, 这是为了防止内存碎片化, 因为每一个智能指针都维护一个引用计数, 大量使用智能指针就会有大量的内存碎片


     C++智能指针发展历史

    C++ 98的auto_ptr: 管理权的转移->不好的设计, 对象悬空. (不建议使用)

    boost的scoped_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

    boost的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

    C++11的unique_ptr: 防止拷贝->简单粗暴, 对于不需要拷贝的场景很好.

    C++11的shared_ptr: 引用计数, 最后一个释放的对象释放资源-> 复杂一些, 但支持拷贝, 很好->问题, 循环引用.

    而C++11的unique_ptr和shared_ptr, 分别对应boost库中的scoped_ptr和shared_ptr. 

    什么是boost?

    一项改变要正式进入标准是很严谨的事情, 所以标准委员会库工作组织成立了一个boost的第三方库作为标准库的后备:

  • 相关阅读:
    阅读llama源码笔记_1
    MySQL锁
    ArcGIS中批量mxd高版本转低版本
    软件工程
    Go-Windows环境的快速搭建
    TCP 中 Flags 标志位 ACK、SYN 与 seq、ack
    脚手架工程使用ElementUI
    SAP 采购订单行项目屏幕增强(BADI)
    全局大喇叭--广播机制
    Android KR3399 原生系统 wlan0与eth0共存调试
  • 原文地址:https://blog.csdn.net/ZZY5707/article/details/134899066