• 是时候来点现代c++了 c++11之超级重要之smart pointer详细解析


    我们为什么需要smart pointer?

    众所周知 新手写的c++代码是很恐怖 压根就不能用 其中最大的原因就在于新手写的代码可能存在大量的内存泄漏 那么为什么新手无法很好的去掌握内存的东西呢 就是因为原生的c++并不像java那样存在垃圾回收的机制 申请在堆区的资源都需要自己去回收 然而最痛苦的一件事情在于 指针的生命周期结束时 你会不小心就没去回收他在堆区的资源 因为堆区资源的生命周期是很难把握的 有可能你析构了 直接导致野指针访问异常 那么为了解决这个问题 c++就推出了智能指针 其中最重要的三种指针就是shared_ptr unique_ptr weak_ptr 接下来让我们来讲讲如何将智能指针的生命周期和堆区资源的生命周期绑定起来吧

    其实也非常简单 本质就是当这片堆区资源的引用计数变为0的时候就释放这片内存

    smart pointer基本概念之引用计数

    先来说说引用计数 这个东西是stl保证了肯定是线程安全的 所以即使你在多个线程内同时去增加或者同时减少引用计数也并不会让引用计数的值出现非你预期的结果

    智能指针是和引用计数绑定在一起的 当你创建智能指针指向一片资源时 引用计数就加一 当智能指针析构时 引用计数就减一 当引用计数变为0时 堆区资源被析构

    smart pointer之shared_ptr

    让我们来看看下一段代码

    1. int main()
    2. {
    3. std::shared_ptr i(new std::string("its good"));
    4. std::shared_ptr j(new std::string("its bad"));
    5. std::vector> smartPointer_vec;
    6. for(int k=0;k<5;k++)
    7. smartPointer_vec.emplace_back(i);
    8. for (int k = 0; k < 4; k++)
    9. smartPointer_vec.emplace_back(j);
    10. for (auto &i : smartPointer_vec)
    11. {
    12. std::cout<c_str();
    13. std::cout << i.use_count() << " ";
    14. std::cout << j.use_count() << std::endl;
    15. i = nullptr;
    16. }
    17. std::cout << i->c_str();
    18. std::cout << i.use_count() <<" ";
    19. std::cout << j.use_count() << std::endl;
    20. }

    聪明人看输出 你就能完全明白 当引用计数为0的时候就会析构 其他不多说了 

    重要讲解:首先使用share_ptr去指向new出来的数据是性能低效的 最本质的原因在于 他会进行两次内存分配 第一次是对象堆区资源的申请 然后才是引用计数堆区资源的申请 而使用make_shared可以只进行一次内存分配 所以他更快 并且更安全 并且c++标准委员会也推荐你这么做 关于make_shared等下讲解

     自定义deleter(也就是自定义删除器)

    先说我们为什么需要自定义删除器 因为在某些情况下 我们希望当智能指针指向的堆区资源释放的时候进行一些自定义操作 也就是说你可以玩一些很花的操作 但是也是那句话 stl并不会执行任何安全检查 崩了需要自己负责 并且总所周知 new []这种形式的堆区资源需要我们使用delete[]来释放 这就是最大的问题 shared_ptr默认是使用delete的 也就是说 当你使用shared_ptr去指向new []时如果不自定义删除器 必然会造成内存泄漏 如下图所示的一段代码就是经典的内存泄漏

    正确的写法如下

     

     即自定义一个删除器 当然你也可以玩一些移动操作 也就是花哨的操作 当然花哨操作就很多了 我只演示其中一种 如下图所示

    运行结果截图如下:

    Tips:当你非常清楚你在干什么的时候再玩 功力不够 不要乱玩

    shared_ptr之make_shared

    上文我们说过 使用智能指针指向new出来的资源有一个问题就是他会进行两次内存分配 而标准委员会推荐创建shared_ptr的方式是使用make_shared 让我们来看看make_shared是如何进行堆区资源申请的 一个最简单的例子如下

    1. int main()
    2. {
    3. std::shared_ptr<int>p1(new int(5));
    4. //下面这种方式比上面这种方式性能更快 并且更加安全
    5. std::shared_ptr<int>p2 = make_shared<int>(5);
    6. }

    当你使用make_shared的时候 又想去使用智能指针指向一个数组的时候 一个推荐的做法如下 

    1. int main()
    2. {
    3. std::shared_ptrint>>p1(new std::vector<int>());
    4. //下面这种方式比上面这种方式性能更快 并且更加安全
    5. std::shared_ptrint>>p2 = make_sharedint>>();
    6. }

    智能指针存在的问题之循环引用

    那么现在我们来看看shared_ptr存在的一些问题 其中比较著名的一个问题就是循环引用 什么叫循环引用呢 本人的观点是当你的智能指针指向的A堆区资源里又有智能指针去指向B堆区资源 而B堆区资源又存在一个智能指针来指向A堆区资源 而你能拿到的指针对半是全局或者是栈区的智能指针 你无法干预到堆区的智能指针的释放 下面来看一个最简单的例子造成的循环引用  代码如下图所示

    1. class SmartPointerTest
    2. {
    3. public:
    4. std::shared_ptr LoopRef{};
    5. int p[1000]{};
    6. };
    7. int main()
    8. {
    9. std::shared_ptrp1(new SmartPointerTest());
    10. std::shared_ptrp2(new SmartPointerTest());
    11. p1->LoopRef = p2;
    12. p2->LoopRef = p1;
    13. }

    可以明显看到 我们创建了两个智能指针p1和p2 而p1指向的堆区资源里又有智能指针指向p2的堆区资源 同理p2 而当main函数结束的时候 p1 p2指针被释放 但是 这个时候 因为两片堆区资源的引用计数都没被置为0 所以不会释放 那么这片堆区内存也就永远的泄漏了 这是所有循环引用的原型 无论任何再复杂的循环引用都是建立在这个最基本的循环引用之上的

    解决循环引用之weak_ptr

    我们现在希望有一个方法来解决循环引用的问题 并且我们也想去随时拿到资源 那么我们该如何做呢 标准委员会也考虑到了这个问题 于是他提供了weak_ptr 当他指向一片堆区资源的时候 并不会让这片堆区资源的引用计数加一 而是作为这片资源的观察者 当需要这片资源的时候 随时使用lock()函数来获得一个shared_ptr来进行使用 下面让我们来看看如何使用weak_ptr 基于上面的例子

    1. class SmartPointerTest
    2. {
    3. public:
    4. std::weak_ptr LoopRef{};
    5. int p[1000]{};
    6. };
    7. int main()
    8. {
    9. std::shared_ptrp1(new SmartPointerTest());
    10. std::shared_ptrp2(new SmartPointerTest());
    11. p1->LoopRef = p2;
    12. p2->LoopRef = p1;
    13. //当你想使用资源的时候 用下面的操作进行
    14. std::cout << p1->LoopRef.lock()->p << std::endl;
    15. }

    输出结果如下:

     

    Tips:当然weak_ptr的作用远远不止如此 他存在的意义仅仅是你想共享资源但是你并不想增加引用计数 解决循环引用只是顺便解决的 优秀的程序员总是能知道在什么情况下使用何种指针来达到性能最优 lock()函数 顾名思义是要去给引用计数上锁的 频繁上锁带来的性能问题不用多说了吧

     如果weak_ptr指向的资源已经被析构 那么他会抛出bad_weak_ptr的异常 请注意捕获异常

     智能指针问题之无法创建指向自己的智能指针(本质当创建自己的智能指针时会创建两个所属组)

    什么叫无法创建指向自己的智能指针呢 看如下这段代码 

    1. class SmartPointerTest
    2. {
    3. public:
    4. std::weak_ptr LoopRef{};
    5. int p[1000]{};
    6. std::vector> spt_vec;
    7. void MemberFuncTest()
    8. {
    9. spt_vec.push_back(std::shared_ptr(this));
    10. }
    11. int operator[](int i)
    12. {
    13. return p[i];
    14. }
    15. };
    16. int main()
    17. {
    18. std::shared_ptrp1(new SmartPointerTest());
    19. p1->MemberFuncTest();
    20. std::cout<use_count()<
    21. system("pause");
    22. }

    我们预期的结果是把指向自己的智能指针传入 并且引用计数为2 但是运行结果如下:

     

    并且程序会崩溃 为什么呢 因为你重复释放了 这就是我说的 你会创建两个组 而不是单纯的增加引用计数 其本质还是滥用普通指针和智能指针引起的麻烦

     解决方法如下

    代码如下 我们可以继承于std::enable_shared_from_this来解决

    1. class SmartPointerTest :std::enable_shared_from_this
    2. {
    3. public:
    4. std::weak_ptr LoopRef{};
    5. int p[1000]{};
    6. std::vector> spt_vec;
    7. void MemberFuncTest()
    8. {
    9. spt_vec.push_back(std::shared_ptr(shared_from_this()));
    10. }
    11. int operator[](int i)
    12. {
    13. return p[i];
    14. }
    15. };
    16. int main()
    17. {
    18. std::shared_ptrp1(new SmartPointerTest());
    19. p1->MemberFuncTest();
    20. std::cout<use_count()<
    21. system("pause");
    22. }

    当你这样继承自enable_shared_from_this的时候 你就可以将自身的智能指针传入 而不是创建一个新的组 避免了重复释放 非常的方便

    关于unique_ptr我们将会在下一篇文章进行详细讲解 其实也很简单就是他堆区资源的引用计数永远只可能是一 也就是说他的资源只可能被一个指针指向 附带而来的有一些小细节和普通的shared_ptr不同 我们也就留在下一章再说了

    Tips: 优秀的c++程序员总是能在合适的场景选择最合适的指针来最优化性能 各种移动模板玩的飞起 c++修行还长 个人还需努力

  • 相关阅读:
    回归与聚类算法系列④:岭回归
    11种增加访问者在网站上平均停留时间的技巧
    【ML】cheatsheet
    【数据结构】二叉树遍历的实现(超详细解析,小白必看系列)
    chatgpt生成【2023高考作文】全国甲卷-人.技术.时间
    【C3AE】《C3AE:Exploring the Limits of Compact Model for Age Estimation》
    lua学习笔记
    以太坊的终局:去信任的信任
    windows下安装配置CGAL
    深眸科技自研AI视觉分拣系统,实现物流行业无序分拣场景智慧应用
  • 原文地址:https://blog.csdn.net/qq_16401691/article/details/126425900