RAII - std智能指针

std 智能指针

智能指针的选择标准

在类中使用智能指针作为成员变量。需要根据实际情况(主要是看所有权)

原始指针:

  • 所有权:该资源对象不属于我,

  • 使用前提:资源对象被释放前,我理应必然已经被释放。否则会存在风险。

unique_ptr:

  • 所有权:该对象仅仅属于我。

  • 被管理的资源对象的生命周期,取决于他所属的唯一一个引用的寿命。

shared_ptr:

  • 所有权:

    1. 该资源由多个对象共享

    2. 或者 虽然该对象仅仅属于我,但有使用 weak_ptr 的需要。

      可用于函数传参

  • 被管理的资源对象的生命周期,取决于所有引用中,最长寿的那一个。

weak_ptr:

  • 所有权:该资源对象不属于我

  • 该资源对象释放后,我仍可能不被释放时。

智能指针全家桶

相较于裸指针,智能指针对裸指针封装的初衷,是无需手动释放内存。

auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,已经被C++11抛弃------支持复制运算符重载。

用于管理指针的对象,当对象过期时其析构函数将使用delete 来释放内存。

  • 复制或者赋值都会改变资源的所有权
  • 在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
  • 不支持对象数组的内存管理

unique_ptr

使用 unique_ptr 最主要的特点:对资源是独占的,防止多个智能指针指向同一个对象。

  • 更安全

    • 不支持复制运算符重载,不允许复制。
    • 支持移动语义 move,完美转发 forward
    • 因为独占,所以压根就不存在shared_ptr循环引用的问题
  • 更好的支持数组。

    std::unique_ptr<T[]>:为数组提供自动内存管理的智能指针,可用于管理动态分配的数组的内存释放。

  • 通过在析构函数中释放资源来管理对象的生命周期,来自动管理资源。

shared_ptr

资源&控制块的生命周期

被管理的资源对象的生命周期:取决于所有 shared_ptr(强引用)中最长寿的那一个。

控制块的生命周期:

  • 所有的强引用和弱引用全都析构后,控制块才会删除。
  • 因为哪怕是资源析构掉,也要满足wp查询是否资源还存在的需求。
make_shared

make_shared内存布局更加紧凑,避免内存碎片化。

若使用shared_ptr<Person> (new X) :控制块的地址与被控制资源的地址 分离。

耗费资源高

耗费资源高,不能代替 unique_ptr

  • 需要维护一个 atomic 的引用计数器,效率低,需要额外的一块管理内存,访问实际对象需要二级指针

  • 而且 deleter 使用了类型擦除技术。

线程安全

对于这个问题,通常的回答我觉得答非所问:即 sharedptr 安全又不安全:

  1. 引用计数(控制块中的计数)线程安全

    资源的生命周期是线程安全的(这就足够啦)。

  2. 访问被指向的资源不是线程安全的(请用atomic_shared_ptr

    答非所问,人家的本职工作就是解决生命周期的问题。

    这个附加要求是在此之前的所有指针的通病。要解决它就必须引入新的复杂度。

如果想要支持这个需求2

  1. 在设计资源本身时下功夫

  2. 新写一个智能指针 支持指针控制资源的多线程修改,解决这个访问资源的竞争问题

    atomic_shared_ptr

反正不要影响sharedptr的复杂度和运行时效率,重温一下 cpp语言的准则:你不需要为不需要的特性付出代价!!

The zero-overhead principle is a C++ design principle that states:

  1. You don't pay for what you don't use.
  2. What you do use is just as efficient as what you could reasonably write by hand.
循环引用

内存泄漏

  • 全部用 shared_ptr,可能出现循环引用之类的问题,导致内存泄露,
  • 依然需要使用不影响计数的原始指针或者 weak_ptr 来避免。
cpp 复制代码
#include <iostream>
#include <string>
#include <vector>
#include <memory>
 
using namespace std;
 
class Person {
  public:
    string m_sName;
    shared_ptr<Person> m_pMother;
    shared_ptr<Person> m_pFather;
    vector<shared_ptr<Person>> m_oKids;
    // vector<weak_ptr<Person>> m_oKids; //弱指针
 
    Person (const string& sName,
            shared_ptr<Person> pMother = nullptr,
            shared_ptr<Person> pFather = nullptr)
     : m_sName(sName), m_pMother(pMother), m_pFather(pFather) {
    }

    ~Person() {
      // 由于循环引用,shared_ptr<Person> pKid pMom pDad都不会调用析构函数
      cout << "删除 " << m_sName << endl;
    }
};
 
shared_ptr<Person> initFamily (const string& sName)
{
    shared_ptr<Person> pMom(new Person(sName + "的母亲"));
    shared_ptr<Person> pDad(new Person(sName + "的父亲"));
    shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
    pMom->m_oKids.push_back(pKid);
    pDad->m_oKids.push_back(pKid);
    return pKid;
}
 
int main()
{
    string sName = "张三";
    shared_ptr<Person> pPerson = initFamily(sName);
 
    cout << sName << "家存在" << endl;
    cout << "- " << sName << "被分享" << pPerson.use_count() << "次" << endl;
    cout << "- " << sName << "母亲第一个孩子的名字是:"
         << pPerson->m_pMother->m_oKids[0]->m_sName << endl;
 
    sName = "李四";
    pPerson = initFamily(sName);
    cout << sName << "家已存在" << endl;
}

运行结果

sh 复制代码
张三家存在
- 张三被分享3次
- 张三母亲第一个孩子的名字是:张三
李四家已存在

weak_ptr

意义
解决循环引用

共享指针shared_ptr指针 ------ 循环引用

如果两个对象使用shared_ptr指针相互引用,并且不存在对这些对象的其他引用,若要释放这些对象及其关联的资源,则共享指针shared_ptr不会释放数据,因为每个对象的引用计数仍为1。

解决:

  • 使用普通指针,需要手动管理相关资源的释放。

  • 使用 weak_ptr

  • unique_ptr

    • 对资源是独占的,防止多个智能指针指向同一个对象。

    • 从使用场景上就不存在循环引用的可能。但是,这个需求又确确实实存在。

生命周期

弱指针weak_ptr需要共享指针shared_ptr才能创建,是共享指针shared_ptr的辅助类。

不影响指向对象的生命周期

  • 每当拥有该对象的最后一个共享指针失去其所有权时,任何弱指针weak_ptr指向的资源都会自动变为空
  • (但影响shared_ptr的控制块的生命周期)

共享但不拥有对象 的意义

  • 若使用共享指针shared_ptr指针,则其将永远不会释放对象。

  • 若使用普通指针,则可能出现指针所引用的对象不再有效,这会带来访问已释放数据的风险。

  • weak_ptr 不影响引用对象的生命周期

    ------引用的生存期可以超过了所引用的对象的生命周期。

更小的开销

你不必为不需要的特性付出代价

The zero-overhead principle is a C++ design principle that states:

  1. You don't pay for what you don't use.
  2. What you do use is just as efficient as what you could reasonably write by hand.
使用

弱指针weak_ptr仅提供少量操作:创建,复制和赋值一个弱指针,将弱指针转换为共享指针,检查它是否指向对象。

创建

弱指针weak_ptr指针仅提供3个构造函数

  • default构造函数
  • copy构造函数
  • 共享指针shared_ptr的构造函数。
访问

lock()会从所包含的弱指针weak_ptr中产生一个共享指针shared_ptr。其shared_ptr对象的使用计数在共享指针的生命周期内会增加。

注意,要使用弱指针,需要在表达式中插入lock()函数:

cpp 复制代码
pPerson->mother->kids[0].lock()->name

而不是调用

cpp 复制代码
pPerson->mother->kids[0]->name

如果无法进行此修改(例如,由于该对象的最后所有者同时释放了该对象),lock()函数会生成一个空的shared_ptr,而直接调用运算符*或->将导致未定义的行为。

检查存在

检查弱指针指向的对象是否仍然存在,则可以使用以下几种方法:

  • 接口列表 expired()

如果弱指针weak_ptr不再共享对象(空的弱指针),则返回true。

此选项等效于检查use_count()是否等于0,但可能更快。

  • 调用 use_count()

返回关联对象的所有者(共享指针shared_ptr)的所有者数量,如果返回值为0,则不再有有效的对象。

但是请注意,通常只应出于调试目的调用use_count(),因为C++标准库明确指出:"use_count()不一定有效。"

  • 通过使用相应的共享指针shared_ptr构造函数将弱指针weak_ptr显式转换为共享指针shared_ptr

如果没有有效的引用对象,则此构造方法将引发bad_weak_ptr异常。

这个异常是从std::exception派生的类的异常,其中what()会返回" bad_weak_ptr"。

操作字典

下表为弱指针提供的所有操作。

操作 结果
weak_ptr<T> wp 默认构造函数;创建一个空的弱指针
weak_ptr<T> wp(sp) 创建一个弱指针,共享由sp拥有的指针的所有权
weak_ptr<T> wp(wp2) 创建一个弱指针,共享由wp2拥有的指针的所有权
wp.~weak_ptr() 析构函数;销毁弱指针,但对拥有的对象无效
wp = wp2 赋值(wp之后共享wp2的所有权,放弃先前拥有的对象的所有权)
wp = sp 用共享指针sp进行赋值(wp之后共享sp的所有权,放弃先前拥有的对象的所有权)
wp.swap(wp2) 交换wp和wp2的指针
swap(wp1,wp2) 交换wp1和wp2的指针
wp.reset() 放弃拥有对象的所有权(如果有的话),并重新初始化为空的弱指针
wp.use_count() 返回共享所有者的数量(拥有对象的shared_ptr数目);如果弱指针为空,则返回0
wp.expired() 返回wp是否为空(等同于wp.use_count() == 0,但可能更快)
wp.lock() 返回共享指针,该共享指针共享弱指针拥有的指针的所有权(如果没有共享指针,则为空共享指针)
wp.owner_before(wp2) 提供严格的弱排序和另一个弱指针
wp.owner_before(sp) 通过共享指针提供严格的弱排序

原先使用shared_ptr

cpp 复制代码
shared_ptr<Person> m_pMother;
shared_ptr<Person> m_pFather;
vector<shared_ptr<Person>> m_oKids;

使用weak_ptr

cpp 复制代码
shared_ptr<Person> m_pMother;
shared_ptr<Person> m_pFather;
vector<weak_ptr<Person>> m_oKids; //弱指针

shared_ptr<Person> pMom(new Person(sName + "的母亲"));
shared_ptr<Person> pKid(new Person(sName, pMom, pDad));
// 尽管是vector<weak_ptr<Person>>但可以push_back分享指针
pMom->m_oKids.push_back(pKid);

std 主类型和弱引用

延伸一下:shared_ptr 是主类型,weak_ptr是其弱引用。

cpp 复制代码
// 主类型 std::shared_ptr<T>
// 弱引用类型 std::weak_ptr<T>、T *

除此之外,std库中还有其它已经实现且比较常用的 主类型 与 弱引用类型

cpp 复制代码
// 主类型 std::string
// 弱引用类型 std::string_view、const char *

// 主类型 std::vector<T>
// 弱引用类型 std::span<T>、(T *, size_t)

// 主类型 std::unique_ptr<T>
// 弱引用类型 T *
相关推荐
yuyanjingtao30 分钟前
CCF-GESP 等级考试 2023年9月认证C++四级真题解析
c++·青少年编程·gesp·csp-j/s·编程等级考试
程序猿进阶41 分钟前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
闻缺陷则喜何志丹1 小时前
【C++动态规划 图论】3243. 新增道路查询后的最短距离 I|1567
c++·算法·动态规划·力扣·图论·最短路·路径
charlie1145141911 小时前
C++ STL CookBook
开发语言·c++·stl·c++20
小林熬夜学编程1 小时前
【Linux网络编程】第十四弹---构建功能丰富的HTTP服务器:从状态码处理到服务函数扩展
linux·运维·服务器·c语言·网络·c++·http
倔强的石头1061 小时前
【C++指南】类和对象(九):内部类
开发语言·c++
A懿轩A2 小时前
C/C++ 数据结构与算法【数组】 数组详细解析【日常学习,考研必备】带图+详细代码
c语言·数据结构·c++·学习·考研·算法·数组
机器视觉知识推荐、就业指导3 小时前
C++设计模式:享元模式 (附文字处理系统中的字符对象案例)
c++
半盏茶香3 小时前
在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)
c语言·开发语言·c++·算法
Ronin3054 小时前
11.vector的介绍及模拟实现
开发语言·c++