当前位置:   article > 正文

C++智能指针以及我们为什么要使用智能指针_为什么要结合使用智能指针和new

为什么要结合使用智能指针和new

前言

智能指针我也是看过了很多文章,也自己尝试了自己去设计和实现,但是一直有一个问题存疑在我心中——那就是我们为什么要使用智能指针。我既然能够很好地使用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 *){
    //回收资源时调用的函数 });
  • 1
  • 2
  • 3
  • 4

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

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

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

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

auto ptr = make_shared<Object>(args);
  • 1

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

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

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

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

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

reset()		//放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

shared_ptr所带来的问题

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

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

class B;
class A
{
   
public:
	shared_ptr<B> pb_;
	~A
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/781567
推荐阅读
相关标签
  

闽ICP备14008679号