• C++——智能指针


    智能指针

    智能指针是什么?
    由于指针很危险,使用不当很容易造成内存泄露,所以自动垃圾回收就显得十分重要,C++虽然并没有向java、python那样完全支持自动垃圾回收,但是C++11 增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收,不再需要手动delete。

    智能指针的实现原理:
    利用代理模式,把裸指针包装起来,在构造函数里初始化,在析构函数里释放。这样当对象使用完了时,C++ 就会自动调用析构函数,完成内存释放、资源回收等清理工作。

    智能指针本质是对象
    智能指针虽然名字叫指针,用起来也很像,但它实际上是一个对象
    所以不要企图对它调用 delete,它会自动管理初始化时的指针,在离开作用域时析构释放内存。

    unique_ptr

    unique_ptr 核心特点
    unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,引用计数只能为 1,一旦该 unique_ptr 指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。

    为了实现unique_ptr 的独占特性,unique_ptr 应用了 C++ 的转移语义(move),同时禁止了拷贝赋值,所以,在向另一个 unique_ptr 赋值的时候,要特别留意,必须用 std::move() 函数显式地声明所有权转移

    赋值操作之后,指针的所有权就被转走了,原来的 unique_ptr 变成了空指针,新的 unique_ptr 接替了管理权,保证所有权的唯一性:

    auto ptr1 = make_unique<int>(42);    // 工厂函数创建智能指针
    auto ptr2 = std::move(ptr1);         // 使用move()转移所有权
    
    • 1
    • 2

    shared_ptr

    shared_ptr 与 unique_ptr 的最大不同点:
    shared_ptr 指针指向的堆内存可以同其它 shared_ptr 共享

    auto ptr1 = make_shared<int>(42);    // 工厂函数创建智能指针
    auto ptr2 = ptr1;                  // 直接拷贝赋值,不需要使用move()
    assert(ptr1 && ptr2);              // 此时两个智能指针均有效
    assert(ptr1 == ptr2);             // shared_ptr可以直接比较
    
    // 两个智能指针均不唯一,且引用计数为2
    assert(!ptr1.unique() && ptr1.use_count() == 2); 
    assert(!ptr2.unique() && ptr2.use_count() == 2); 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    shared_ptr 的注意事项
    1、虽然 shared_ptr 非常“智能”,但天下没有免费的午餐,它也是有代价的,引用计数的存储和管理都是成本,这方面是 shared_ptr 不如 unique_ptr 的地方。

    2、如果不考虑应用场合,过度使用 shared_ptr 就会降低运行效率。不过,你也不需要太担心,shared_ptr 内部有很好的优化,在非极端情况下,它的开销都很小。

    3、另外一个要注意的地方是 shared_ptr 的销毁动作。
    因为我们把指针交给了 shared_ptr 去自动管理,但在运行阶段,引用计数的变动是很复杂的,很难知道它真正释放资源的时机,无法像 Java、Go 那样明确掌控、调整垃圾回收机制。所以要特别小心对象的析构函数,不要有非常复杂、严重阻塞的操作。一旦 shared_ptr 在某个不确定时间点析构释放资源,就会阻塞整个进程或者线程,“整个世界都会静止不动”( Go 也是)。排查起来费了很多功夫,真的是“血泪教训”。

    引用计数

    引用计数:shared_ptr 支持安全共享的原理在于内部使用了“引用计数”
    即:引用计数最开始的时候是 1,表示只有一个持有者。如果发生拷贝赋值,引用计数就增加,而发生析构销毁的时候,引用计数就减少。只有当引用计数减少到 0,即没有任何人使用这个指针的时候,它才会真正调用 delete 释放内存。

    引用计数存在循环引用的问题:
    shared_ptr 的引用计数也导致了一个新的问题,就是“循环引用”,这在把 shared_ptr 作为类成员的时候最容易出现,典型的例子就是链表节点,看下面的例子:

    auto n1 = make_shared<Node>();   // 工厂函数创建智能指针
    auto n2 = make_shared<Node>();   // 工厂函数创建智能指针
    
    n1->next = n2;                 // 两个节点互指,形成了循环引用
    n2->next = n1;
    
    assert(n1.use_count() == 2);    // 引用计数为2
    assert(n2.use_count() == 2);    // 无法减到0,无法销毁,导致内存泄漏
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里,两个节点指针刚创建时,引用计数是 1,但指针互指(即拷贝赋值)之后,引用计数都变成了 2。

    这个时候,shared_ptr 就“犯傻”了,意识不到这是一个循环引用,多算了一次计数,后果就是引用计数无法减到 0,无法调用析构函数执行 delete,最终导致内存泄漏。

    想要从根本上杜绝循环引用,光靠 shared_ptr 是不行了,必须要用到它的“小帮手”:weak_ptr。

    shared_ptr的线程安全问题

    1、引用计数的加减操作是否线程安全?
    安全,因为是原子操作
    2、shared_ptr指向的对象是否线程安全?
    不安全
    当智能指针发生拷贝的时候,会先拷贝智能指针,再拷贝对象,这两个操作并不是原子的,所以不安全。
    比如在计数-1的时候,其内部的指向被其他线程修改了。引用计数的异常会导致某个管理的对象被提前析构,后续在使用到该数据的时候会导致内存泄露。

    make_shared

    前面看到,创建智能指针有两种方式,使用shared_ptr直接创建智能指针和利用make_shared创建,区别是什么呢?

    先看使用shared_ptr直接创建智能指针:

    auto px = shared_ptr(new int(100));
    
    • 1

    上面代码会执行下面两个过程:

    • new int申请内存,并把指针传给px
    • 为shared_ptr 的控制块另外申请一块内存,用来存放shared_ptr的控制信息,比如shared_ptr引用计数,weak_ptr引用计数。

    所以new方式创建的shared_ptr,内存地址空间和引用计数地址空间不在一块。
    而make_shared申请的在一起。

    上面的两步会存在下面两个问题:

    • 当 new int 申请内存成功,但引用计数内存申请失败时,很可能造成内存泄漏。
    • 内存分配是一个消耗性能的过程,分两次分配内存,意味着性能会下降

    为了解决直接使用shared_ptr创建智能指针带来的问题,C++11标准库引入了make_shared

    auto p = make_shared(100);
    
    • 1

    make_shared只会申请一次内存,这块内存会大于int所占用的内存,多出的部分被用于智能指针引用计数。这样就避免了直接使用shared_ptr带来的问题。

    不过make_shared并不是完美的:
    前面只说到强引用计数,其实智能指针还有弱引用计数。当强引用计数为0时,释放引用的对象内存,当弱引用计数为0时,释放引用计数所占用的内存。
    由于弱引用计数的存在,make_shared创建的智能指针引用的对象,可能无法得到及时的释放,只有当强/弱引用都为0时,才能释放make_shared申请的一整块内存。

    weak_ptr(解决循环引用)

    weak_ptr 顾名思义,功能很“弱”。它专门为打破循环引用而设计

    因为weak_ptr只观察指针,不会增加引用计数(即weak_ptr 是弱引用),但在需要的时候,可以调用成员函数 lock(),获取 shared_ptr(强引用)。刚才shared_ptr造成的循环引用的例子里,只要改用 weak_ptr,循环引用的烦恼就会消失、

    不严谨的weak_ptr 和shared_ptr的区别:

    • shared_ptr是强引用,无论如何都需要持有共享对象的时候就用它。
    • weak_ptr是弱引用,不一定要持有对象,只是“偶尔”想去看看对象在不在,不在也可以接受。

    weak_ptr怎么用呢?
    weak_ptr与shared_ptr配合着使用,用weak_ptr先确保shared_ptr是持有指针的,然后再放心使用

    总结

    智能指针是代理模式的具体应用,它使用 RAII 技术代理了裸指针,能够自动释放内存,无需程序员干预。

    如果指针是“独占”使用,就应该选择 unique_ptr,它为裸指针添加了很多限制,更加安全。

    如果指针是“共享”使用,就应该选择 shared_ptr,它的功能非常完善,用法几乎与原始指针一样。

    应当使用工厂函数 make_unique()、make_shared() 来创建智能指针,强制初始化,而且还能使用 auto 来简化声明。

    shared_ptr 有少量的管理成本,也会引发一些难以排查的错误,所以不要过度使用。

  • 相关阅读:
    App上架小米应用商店
    基于 Docker 搭建 ownCloud 个人云盘
    风力发电机监测 震动监测 故障监测
    细说MySQL数据类型
    报错:Gradle build failed.See the Console for details.(已解决)
    http代理IP它有哪些应用场景?如何提升访问速度?
    借助调试工具理解BLE协议_1.蓝牙简介和BLE工作流程
    计算机毕业设计ssm+vue基本微信小程序的一起考研学习系统
    基于机器视觉的二维码识别检测 - opencv 二维码 识别检测 机器视觉 计算机竞赛
    Could not find org.jetbrains.kotlin:kotlin-stdlib-jre7:1.5.21.
  • 原文地址:https://blog.csdn.net/qq_40337086/article/details/126025232