C++智能指针以及我们为什么要使用智能指针

2023-10-31

前言

智能指针我也是看过了很多文章,也自己尝试了自己去设计和实现,但是一直有一个问题存疑在我心中——那就是我们为什么要使用智能指针。我既然能够很好地使用new和delete去管理我的堆上内存,也留意了拷贝构造函数的浅拷贝问题,似乎也就没有智能指针的用武之地了。如果有同样想法的同学,应该好好看看我这篇博文。

对象生命周期的管理问题

最近在复习设计模式,用C++去写一些设计模式的代码,然后就想到了一些问题:设计模式肯定是要用到多态的,而C++的多态是用指针或引用来实现的(基本上是用指针),但是如果用的是原生指针,那对象的生命周期该如何管理?很显然,我们不能再用以前手动new / delete的方式去维护对象的生命周期,因为我们无法得知对象应该在什么时候要被析构掉。在各种设计模式中,一个对象不再是个独立的个体,他可能会和许多的对象产生关系,所以就很难维护一个个体的生命周期。

然后这几天在看陈硕的《Linux多线程服务端编程:使用muduo C++网络库》,里面的第一章就着重讲了多线程中对象的生命周期管理。设计模式的对象生命周期管理还算简单,但是这个问题一旦放到多线程中,就变成了一个更加复杂且麻烦的事情。多线程的不确定性大大提高了对象生命周期的维护难度,使用原生指针很容易就会造成空悬指针的问题(浅拷贝造成的问题也是空悬指针)。比起内存泄漏,这是个更加严重的问题,使用空悬指针就跟使用了野指针一样,直接让程序crash掉。

所以我们希望有一种代理,能够帮我们管理指针所指向的对象的生命周期,同时使用的时候也像普通指针一样去使用。于是乎就有了智能指针这一伟大发明。

智能指针

智能指针因为其优秀的设计和不俗的性能,被纳入了C++11的标准库中,也是每个C++程序员必须掌握的技能之一。

我们通常所说的智能指针,其实就是以下几种封装好的模板类:

  • auto_ptr (C++98的方案,C++11已弃用)
  • unique_ptr
  • scoped_ptr
  • shared_ptr
  • weak_ptr

除了第一个,后面的都是C++11引入的,使用时需要#include <memory>。这里主要介绍shared_ptr和weak_ptr,他们两个搭配使用就能解决大部分情况的对象生命周期管理问题。其他指针的详细内容可以去翻阅文档。

然后智能指针有以下三种特点:

  • 具有RAII机制
  • 能像原生指针一样使用
  • 能够有效管理对象的生命周期

RAII机制(Resource Acquisition Is Initialization),资源获取即初始化。 这是智能指针的核心思想,也是C++程序设计的重要思想。这里引用陈硕书中的一句话:

初学C++的教条是“new和delete要配对,new了之后要记者delete”;如果使用RAII,要改成“每一个明确的资源配置动作(例如new)都应该在单一语句用执行,并在该语句中立刻将配置获得的资源交给handle对象(如shared_ptr),程序中一般不出现delete”。

综合特点与机制,可以推测出智能指针的设计大概是这样的:

  1. 定义一个模板类来封装对象的指针;
  2. 构造函数中完成资源的分配及初始化;
  3. 重载->运算符和*运算符以达到原生指针的效果;
  4. 析构函数中完成资源的清理,正确释放对象指针。

所以智能指针本质上就是一个模板类,他封装了T类型的对象指针,有构造函数,有拷贝构造函数,有析构函数,能重载各种运算符。所以利用好我们以前学的知识,我们自己也能封装这些智能指针。

不同的智能指针有不同的资源管理方式,我们要依情况来选择不同的智能指针。大体流程就是在智能指针构造的时候,“托付”一个堆上对象指针给他管理;然后利用对象在离开作用域的时候会调用析构函数这一机制,做到有效地管理对象生命周期,我可以在智能指针的析构函数中delete掉他管理的堆上对象指针嘛。下面我们来讨论shared_ptr和weak_ptr是怎么实现的。

shared_ptr

shared_ptr实现了共享拥有的概念,利用“引用计数”来控制堆上对象的生命周期。 原理也很简单,在初始化的时候引用计数设为1,每当被拷贝或者赋值的时候引用计数+1,析构的时候引用计数-1,直到引用计数被减到0,那么就可以delete掉对象的指针了。他的构造方式主要有以下三种:

shared_ptr<Object> ptr;
shared_ptr<Object> ptr(new Object);
shared_ptr<Object> ptr(new Object, [=](Object *){
    //回收资源时调用的函数 });

第一种空构造,没有指定shared_ptr管理的堆上对象的指针,所以引用计数为0,后期可以通过reset()成员函数来指定其管理的堆上对象的指针,reset()之后引用计数设为1。

第二种是比较常见的构造方式,构造函数里面可以放堆上对象的指针,也可以放其他的智能指针(如weak_ptr),具体使用参照文档。

第三种构造方式指定了shared_ptr在析构自己所保存的堆上对象的指针时(即引用计数为0时)所要调用的函数,这说明我们可以自定义特定对象的特定析构方式。同样的,reset()成员函数也可以指定析构时调用的指定函数。

除了以上三种构造方式外,我们还有一种比较常见的构造shared_ptr的方式:

auto ptr = make_shared<Object>(args);

这是最安全的一种方式,使用标准库里边的make_shared<>()模板函数。该函数会调用模板类的构造方法,实例化一个堆上对象,然后将保存了该对象指针的shared_ptr返回。参数是该类构造函数的参数,所以使用make_shared<>()就好像单纯地在构造该类对象一样。auto是C++11的一个关键字,可以在编译期间自动推算变量的类型,在这里就是shared_ptr<Object>类型。

shared_ptr的其他成员函数,在这里就简单地列举一下:

use_count()	//返回引用计数的个数

unique()	//返回是否是独占所有权(use_count是否为1)

swap()		//交换两个shared_ptr对象(即交换所拥有的对象,引用计数也随之交换)

reset()		//放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

shared_ptr所带来的问题

值得一提的是,shared_ptr是一种强引用,引用陈硕书中对强引用的描述就是:就好像对象上面绑了一根根的铁丝。对象身上的铁丝不全数卸干净,对象就无法得到释放,这个比喻还是很贴切的。因此shared_ptr也会带来一定的麻烦,比如他会意外地延长对象的寿命然后的空悬指针。对这些技术上的陷阱感兴趣的同学可以去读读陈硕的那本书。

还有一个问题就是两个shared_ptr对象相互引用造成的死锁问题,这个是比较典型的问题,具体看一下代码:

class B;
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C++智能指针以及我们为什么要使用智能指针 的相关文章

  • MVC 重定向到没有控制器的视图

    希望应该是一个简单的 我创建了一个通用错误视图 当整个站点的操作方法内发生异常时 我想显示该视图 我创建了一个部分页面 所有导航都位于其中 因此我不需要在此视图上使用控制器 那么如何从控制器内的操作方法重定向到它 像这样的东西 HttpPo
  • 更快的算法来计算有多少数字可以被范围内的特定整数整除

    int a b c d 0 cin gt gt a gt gt b gt gt c for int i a i lt b i if i c 0 d cout lt
  • 必须打开存储才能执行此操作 - System.IO.Packaging.Package

    我正在使用 System IO Packaing Package 类来压缩文件 我的应用程序的多个实例可以同时运行 并读取和保存文件 当处理小文件时 一切似乎都很好 但是当涉及大文件时 如果应用程序的两个实例同时保存 我会收到一个异常 消息
  • C# 中的协变和逆变

    首先我要说的是 我是一名正在学习 C 编程的 Java 开发人员 因此 我会将我所知道的与我正在学习的进行比较 我已经使用 C 泛型几个小时了 我已经能够在 C 中重现我在 Java 中知道的相同内容 除了几个使用协变和逆变的示例 我正在读
  • 在 GCC 和 Clang 下,使用 lambda 的简单 RAII 包装器的复制初始化意外失败

    我在创建一个简单的 RAII 包装器时遇到了一个意想不到的问题 更不用说下面代码的逻辑不完整性了 复制构造函数和赋值运算符未删除等 这意味着是一个SSCCE 令我印象深刻的是复制初始化我的包装器与临时 lambda 的结果会导致编译错误 而
  • 为什么假设 send 可能返回的数据少于在阻塞套接字上传输的请求数据?

    在流套接字上发送数据的标准方法始终是调用 send 并写入一大块数据 检查返回值以查看是否发送了所有数据 然后再次调用 send 直到整个消息被接受 例如 这是一个常见方案的简单示例 int send all int sock unsign
  • rand() 播种与 time() 问题

    我很难弄清楚如何使用 rand 并使用 Xcode 用 time 为其播种 我想生成 0 到 1 之间的随机十进制数 该代码为我提供了元素 1 和 2 看似随机的数字 但元素 0 始终在 0 077 左右 有什么想法吗 我的代码是 incl
  • F10键没被抓住

    I have a Windows Form and there overriden ProcessCmdKey However this works with all of the F Keys except for F10 I am tr
  • 如何使用泛型类型的 DataContractSerializer 编写自定义序列化器?

    我想编写一个自定义序列化器 用于将会话状态存储到Azure 缓存 预览版 这意味着这个自定义序列化器必须实现IDataCacheObjectSerializer 如果我错了 请告诉我 我需要编写这个自定义序列化程序的原因是我需要序列化一些包
  • Microsoft.Graph - 如何从具有不同用户名的共享邮箱发送?

    我目前正在将使用 SMTP 的服务代码移植到 Office 365 通过 SMTP 我可以使用 发件人 字段在来自共享收件箱的邮件上设置不同的用户名 同时保留共享电子邮箱地址 这似乎无法通过 Office 365 运行 其工艺流程为 客户填
  • 为什么重载方法在 ref 仅符合 CLS 方面有所不同

    公共语言规范对方法重载非常严格 仅允许根据其参数的数量和类型来重载方法 如果是泛型方法 则根据其泛型参数的数量进行重载 根据 csc 为什么此代码符合 CLS 无 CS3006 警告 using System assembly CLSCom
  • c# 如何生成锦标赛括号 HTML 表

    所以我已经被这个问题困扰了三个星期 但我一生都无法弄清楚 我想做的是使用表格获得这种输出 演示 http www esl world net masters season6 hanover sc2 playoffs rankings htt
  • 如何在 C# 中使用 XmlDsigC14NTransform 类

    我正在尝试使用规范化 xml 节点System Security Cryptography Xml XMLDsigC14nTransformC net Framework 2 0 的类 该实例需要三种不同的输入类型 NodeList Str
  • 是什么原因导致 Linq 错误:此方法无法转换为存储表达式?

    我有一堆具有相同 select 语句的 Linq to Entity 方法 所以我想我会很聪明 并将其分离到它自己的方法中以减少冗余 但是当我尝试运行代码时 我得到了以下内容错误 该方法不能转化为 商店表达式 这是我创建的方法 public
  • 无法通过 LINQ to Entities 使用某些功能?

    我正在尝试使用 LINQ 查询在项目上实现搜索功能 由于数据有时包含带有重音符号和其他符号的字符 因此我创建了一种方法来删除这些字符以进行搜索 这是我的代码 var addresses from a in db Addresses join
  • 为什么C语言中可以使用多个分号?

    在 C 中我可以执行以下操作 int main printf HELLO WORLD 它有效 这是为什么 我个人的想法 分号是一个 NO OPERATION 来自维基百科 指示符 拥有一大串分号与拥有一个分号并告诉 C 语句已结束具有相同的
  • 如何使用 ASP.NET Web 表单从代码隐藏中访问更新面板内的文本框、标签

    我在更新面板中定义了一些控件 它们绑定到中继器控件 我需要根据匿名字段隐藏和显示用户名和国家 地区 但问题是我无法以编程方式访问更新面板中定义的控件 我如何访问这些控件 我也在网上查找但找不到很多参考资料 下面是来自aspx页面和 cs页面
  • 程序退出后,TcpListener Socket 仍处于活动状态

    当我的程序退出时 我试图停止 TCP 侦听器 我不关心套接字或任何活动客户端套接字上当前活动的任何数据 套接字清理代码本质上是 try myServer Server Shutdown SocketShutdown Both catch E
  • 将文本从文本文件添加到 PDF 文件[重复]

    这个问题在这里已经有答案了 这是我的代码 using FileStream msReport new FileStream pdfPath FileMode Create step 1 using Document pdfDoc new D
  • 如何从函数返回矩阵(二维数组)? (C)

    我创建了一个生成宾果板的函数 我想返回宾果板 正如我没想到的那样 它不起作用 这是函数 int generateBoard int board N M i j fillNum Boolean exists True initilize se

随机推荐