智能指针我也是看过了很多文章,也自己尝试了自己去设计和实现,但是一直有一个问题存疑在我心中——那就是我们为什么要使用智能指针。我既然能够很好地使用new和delete去管理我的堆上内存,也留意了拷贝构造函数的浅拷贝问题,似乎也就没有智能指针的用武之地了。如果有同样想法的同学,应该好好看看我这篇博文。
最近在复习设计模式,用C++去写一些设计模式的代码,然后就想到了一些问题:设计模式肯定是要用到多态的,而C++的多态是用指针或引用来实现的(基本上是用指针),但是如果用的是原生指针,那对象的生命周期该如何管理?很显然,我们不能再用以前手动new / delete的方式去维护对象的生命周期,因为我们无法得知对象应该在什么时候要被析构掉。在各种设计模式中,一个对象不再是个独立的个体,他可能会和许多的对象产生关系,所以就很难维护一个个体的生命周期。
然后这几天在看陈硕的《Linux多线程服务端编程:使用muduo C++网络库》,里面的第一章就着重讲了多线程中对象的生命周期管理。设计模式的对象生命周期管理还算简单,但是这个问题一旦放到多线程中,就变成了一个更加复杂且麻烦的事情。多线程的不确定性大大提高了对象生命周期的维护难度,使用原生指针很容易就会造成空悬指针的问题(浅拷贝造成的问题也是空悬指针)。比起内存泄漏,这是个更加严重的问题,使用空悬指针就跟使用了野指针一样,直接让程序crash掉。
所以我们希望有一种代理,能够帮我们管理指针所指向的对象的生命周期,同时使用的时候也像普通指针一样去使用。于是乎就有了智能指针这一伟大发明。
智能指针因为其优秀的设计和不俗的性能,被纳入了C++11的标准库中,也是每个C++程序员必须掌握的技能之一。
我们通常所说的智能指针,其实就是以下几种封装好的模板类:
auto_ptr (C++98的方案,C++11已弃用)unique_ptrscoped_ptrshared_ptrweak_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”。
综合特点与机制,可以推测出智能指针的设计大概是这样的:
定义一个模板类来封装对象的指针;构造函数中完成资源的分配及初始化;重载->运算符和*运算符以达到原生指针的效果;析构函数中完成资源的清理,正确释放对象指针。所以智能指针本质上就是一个模板类,他封装了T类型的对象指针,有构造函数,有拷贝构造函数,有析构函数,能重载各种运算符。所以利用好我们以前学的知识,我们自己也能封装这些智能指针。
不同的智能指针有不同的资源管理方式,我们要依情况来选择不同的智能指针。大体流程就是在智能指针构造的时候,“托付”一个堆上对象指针给他管理;然后利用对象在离开作用域的时候会调用析构函数这一机制,做到有效地管理对象生命周期,我可以在智能指针的析构函数中delete掉他管理的堆上对象指针嘛。下面我们来讨论shared_ptr和weak_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对象相互引用造成的死锁问题,这个是比较典型的问题,具体看一下代码:
class B; class A { public: shared_ptr<B> pb_; ~A() { cout << "~A()" << endl; } }; class B { public: shared_ptr<A> pa_; ~B() { cout << ~B()" << endl; } }; void fun() { shared_ptr<B> pb(new B()); shared_ptr<A> pa(new A()); cout << pb.use_count() << endl; //1 cout << pa.use_count() << endl; //1 pb->pa_ = pa; pa->pb_ = pb; cout << pb.use_count() << endl; //2 cout << pa.use_count() << endl; //2 } int main() { fun(); return 0; }可以看到,在离开了fun()函数之后,两个shared_ptr对象的use_count()并不会下降到0,这就造成了内存泄漏的问题,没有达到使用智能指针的目的。所以这就有了weak_ptr出场的机会,我们要用到weak_ptr去解决类似这样的问题。
对比shared_ptr的强引用,weak_ptr就如同他的字面意思一样,是一个“弱”指针。用回上面的比喻,把这种弱引用比喻成“棉线”也是十分贴切的。weak_ptr指向一个shared_ptr管理的对象,并且不会增加shared_ptr的引用计数。进行该对象的内存管理的是那个强引用的shared_ptr, weak_ptr只是提供了对管理对象的一个访问手段。 他只可以从一个shared_ptr或另一个weak_ptr对象构造,并且可以通过成员函数“提升为”一个shared_ptr。
weak_ptr的使用方法如下:
weak_ptr<Object> wp; //空构造 weak_ptr<Object> wp(sp); //用shared_ptr构造weak_ptr wp = p; //p可以是shared_ptr或者weak_ptr,赋值后他们指向同一个堆上对象 wp.reset(); //将wp置空 wp.use_count(); //返回wp共享对象的shared_ptr的引用计数 wp.expired(); //wp.use_count()为1返回true,否则返回false wp.lock(); //将weak_ptr“提升为”shared_ptrweak_ptr的使用方法一共就这么多,他是配合shared_ptr使用的。在使用weak_ptr的时候,我们更多的是希望知道这个堆上对象是否“还活着”。如果他还活着,那么lock()成员函数就能够将weak_ptr成功“提升为”shared_ptr,否则将“提升为”一个空的shared_ptr。
我们可以用“弱回调”的方式对堆上对象进行访问,所谓“弱回调”就是:“如果对象还活着,我们就调用他的成员函数,否则忽略他”。“弱回调”操作如下:
auto sp = wp.lock(); if(sp) //因为shared_ptr对bool操作符进行了重载,所以可以这样判断 { //堆上对象还活着,可以对他进行操作 } else { //堆上对象已经被析构了 }那么上面这个死锁问题就很好解决了,只要把其中一个类中的shared_ptr改成weak_ptr就能够解决了。然后要使用weak_ptr所指向的对象的成员函数的话,就采用“弱回调”的方式就好了。
那么谁该持有shared_ptr,谁又该持有weak_ptr呢?很明显,owner应该持有child的shared_ptr,因为只有owner才能决定堆上对象的生死,而child应该持有指向owner的weak_ptr,child只是访问堆上对象,无权决定其生死。放到上面的例子就要看A类和B类谁是owner谁是child了,在其他类的设计上面也应该遵守这个从属原则。
陈硕的这本书中用的就是观察者模式来说明对象的生命周期管理以及线程安全问题。这里我们撇开线程安全问题不谈,这个问题比较复杂,感兴趣的可以去看陈硕的《Linux多线程服务端编程:使用muduo C++网络库》。在这里主要还是想谈谈智能指针在设计模式上的应用,以及对象生命周期管理的一些考量。
简单介绍一下观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。具体的内容可以去看《大话设计模式》这本书,也可以百度一下,这里就不展开了。
先来简单定义一下观察者基类:
class Observable; class Observer { public: virtual ~Observer(); void observe(Observable *); //观察某个对象 virtual void update() = 0; //更新观察者状态 // ... private: Observable *m_suject; }; Observer::~Observer() { m_suject->detch(this); //没有办法delete m_suject,因为可能有其他对象还观察着他呢 } void Observer::observe(Observable *s) { m_suject->attach(this); m_suject = s; }然后再来定义被观察类,在陈硕的书上是Observable,在菜鸟教程是Subject。然后我想吐槽一下,在《大话设计模式》这边书里,观察者是Subject,被观察对象是Observer,感好像意义反了,Observer的翻译就是观察者。
class Observable { public: void attach(Observer *); //注册观察者 void detch(Observer *); //移除观察者 void notifyAll(); //提醒所有观察者 private: vector<Observer*> m_observers; }; void Observable::attach(Observer *ob) { vector<Observer*>::iterator iter = find(m_observers.begin(), m_observers.end(), ob); //#include <algorithm> if(iter == m_observers.end()){ //没有才添加 m_observers.push_back(ob); } } void Observable::detch(Observer *ob) { vector<Observer*>::iterator iter = find(m_observers.begin(), m_observers.end(), ob); if(iter != m_observers.end()){ m_observers.erase(iter); } } void Observable::notifyAll() { for(Observer *ob : m_observers){ ob->update(); } }以上是使用原生指针的最简单实现。很明显,使用原生指针会有对象生命周期管理的隐患,不管在什么时候什么地方析构什么对象都不合适。 所以我们应该用智能指针去改造他,在使用原生指针的地方,统统换成shared_ptr。
class Observable; class Observer { public: virtual ~Observer(); void observe(shared_ptr<Observable> s); virtual void update() = 0; // ... private: shared_ptr<Observable> m_suject; }; class Observable { public: void attach(shared_ptr<Observer>); void detch(shared_ptr<Observer>); void notifyAll(); private: vector<shared_ptr<Observer>> m_observers; };这个2.0版本已经挺好的了,但是还是有隐患。不过这个隐患主要还是发生在多线程上,因为这里有一个竞态条件(race condition)。感兴趣的话还是去看看陈硕的那本书,想了解竞态条件的种种可以去看《Unix高级环境编程》(简称APUE)这本书。
在这里还是用上面提到的从属原则来解释一下:很明显,控制Observer对象生死的应该是Observer本身,被观察对象Observable只是访问了观察者的update()成员函数,无权左右其生死,所以Observable类里面的shared_ptr<Observer>应该换成weak_ptr<Observer>。
虽然这样的设计确实在一定程度上解决了竞态条件,但是这个解释不足以成为解决竞态条件的依据。具体为什么用了weak_ptr就解决了竞态条件,还是得去看看陈硕那本书,这里不展开细谈。
所以3.0版本的代码应该是这样的:
class Observable { public: void attach(weak_ptr<Observer>); void detch(weak_ptr<Observer>); void notifyAll(); private: vector<weak_ptr<Observer>> m_observers; }; void Observable::notifyAll() { vector<weak_ptr<Observer>>::iterator iter = m_observers.begin(); while(iter != m_observers.end()){ auto sp = iter->lock(); //弱回调 if(sp){ sp->update(); //对象还存在 ++iter; } else{ m_observers.erase(iter); //对象已被销毁 } } }按照从属原则的逻辑去思考,好像被观察对象的生命周期也不应该由观察者来控制,那是不是观察者类里边的m_subject也应该改为weak_ptr呢?我认为的是:观察对象是要被观察才存在的,所以要说观察者控制了观察对象的生命也是合理的。3.0的代码设计已经足够好了,就不继续延展了。
顺带一提,如果嫌auto关键字会带来阅读的麻烦,又嫌写weak_ptr<Observer>太长太麻烦,可以使用typedef关键字给这个类型重命名一下:
typedef shared_ptr<Observable> ObservableSPtr; typedef weak_ptr<Observer> ObserverWPtr;智能指针真的是C++工程师的福音,感兴趣的同学可以去看看他的源码,或者自己去实现一下。没有垃圾回收机制的语言在对象生命周期管理方面真的不行,也因此带来了多线程编程上的不便,就连写个设计模式,都会感到十分无奈。
在掌握了shared_ptr / weak_ptr之后,两者配合使用,基本上能应付绝大部分情况了。包括多线程的对象生存周期管理,也是用到这两个智能指针打配合来解决,比较难的是线程的安全问题。这些都是后话了,在充分理解了智能指针的RAII机制之后,再去理解怎么解决线程的安全问题也不迟。