前言
c++中对象一般有着严格的生命期,全局对象启动时被初始化赋值,程序结束就死掉,局部对象和其程序块共存亡,局部static第一次使用被初始化赋值,程序结束就被干掉。
除此以外,c++还拥有动态分配内存的对象(毕竟继承了c),它的存在需要显式创建显式销毁。他们的管理经常会出现问题,因为人们经常始乱终弃开了个头就没有尾了,所以这里介绍标准库中为了管理动态分配对象而定义的三个智能指针类型,从而确保它的释放。
一、shared_prt类
和另外两个智能指针一样,shared_ptr定义在memory头文件中,他们用于管理动态对象就是基于它们的自动释放指向对象的特性(析构)。智能指针的实现需要定下类型,因为它是一个模板类,创建智能指针对象时,需要提供额外的类型信息。
shared_ptr<int> pi;
shared_ptr<string> ps;
shared_ptr<vector<int>> pVec;
shared_ptr是一个类,所以上面构造int、string、vector类型的智能指针时,都是默认初始化,也就是说,它现在保存着一个空指针,同时这个类是一个类指针,指针的行为它都能进行:以pi为条件判断,*ps解引用获取指向对象,ps->mem等。
可以知道的是,c++用的都是new来申请空间,然后有一个相应类型的指针来使用该内存空间,所以上面的算是完成了指针部分,接下来还需要一个申请空间的东西,标准库提供了make_shared函数来负责分配对象内存并初始化。
shared_ptr<int> pi = make_shared<int> (42);
shared_ptr<string> ps = make_shared<string> (5, "l");
shared_ptr<int> pi = make_shared<int> ();
如上,可以看到make_shared的使用,定好类型,根据类型的构造函数进行对象初始化,这种方式对于类类型来说,构造函数尤其重要,如果使用自定义的类,必须好好规划构造函数。
指向相同对象的shared_ptr
shared_ptr是模板类,它支持拷贝和赋值的操作,可以用它的对象去初始化另一个对象或者赋值初始化给另一个对象,这样这两个指针就指向了相同的对象。shared_ptr有一个引用计数的功能,会对它指向对象的使用进行计数,这个使用就是有多少个shared_ptr引用它,当一个shared_ptr对象p1被拷贝用来初始化p2对象,这样p1和p2有着共同指向的对象,计数器递增;相反的操作会使得计数器递减,当计数器为0,管理对象就会被销毁。
注:c++ primer中标记了记录多少个指针共享对象的方式不一定是计数器也可能是其他数据结构,这个由标准库的具体实现来决定。我们需要关注的是,它能记录多少个shared_ptr指向相同对象并在关键时刻释放。
现在我们知道指向一个对象的可以是多个shared_ptr,当我们上面说的计数器为0,也就是没有shared_ptr还存在使用该对象的可能了,该对象才会被shared_ptr类的析构函数进行清除,并且释放内存。有时候当shared_ptr存放在容器中,并重排了容器导致shared_ptr无用,这时候的内存就会因为最后一个shared_ptr的存在而不进行空间释放,所以需要使用erase对不再需要的shared_ptr元素进行删除。
使用new配合shared_ptr
一个智能指针不初始化就是一个空指针,上面已示,另外也可以使用new返回的指针来对智能指针进行初始化,还可以从其他内置指针接管对象的所有权,就是所谓的移动语义。
智能指针的构造函数是explicit的,所以必须进行直接初始化
shared_ptr<int> pi = new int(1024);
shared_ptr<int> pi(new int(1024) );
各种需要对shared_ptr进行隐式转换的地方都需要显式绑定shared_ptr,比如函数返回值。当我们把内置指针指向的内存空间交给shared_ptr进行管理,就不能再用内置指针访问shared_ptr指向的内存空间了,因为我们不知道对象什么时候会被销毁,这是个危险的行为。
shared_ptr的get操作
智能指针类型存在返回内置指针的get函数,该内置指针指向智能指针管理的对象,当我们使用智能指针对象来绑定get返回的指针,编译器不会报错,但这样会引起未定义行为。这个行为是开放智能指针指向对象的访问权限,但销毁对象、释放内存还是由智能指针负责的,所以不需要在代码中对获得get返回空间进行delete释放。
对于一些为c和c++两种语言进行设计的类,通常要求用户显式地释放使用资源,这时候可以使用智能指针来确保资源的正确释放。
智能指针的一些规范:
不适用相同内置指针初始化多个智能指针; 不对get()返回的指针进行delete操作; 不把get操作返回的内置指针初始化另一个智能指针; get操作得到的指针在最初源头智能指针被销毁后,就会成为悬空指针; 对于智能指针管理的资源不是new得到的内存,记得配备一个删除器。
二、unique_ptr类
unique_ptr作为智能指针的一员,和shared_ptr具有相同的指针操作,但不同的是,同一时刻,对于给定对象,只能有一个unique_ptr指向它,并且它只能采用直接初始化的方式进行初始化而不像前者那样有make_shared函数进行构造。
因为同一时刻只能有一个unique_ptr指向给定对象,所以它不存在拷贝和赋值操作,但它可以转移指针所有权,使用release和reset都可以实现这个操作。
unique_ptr<int> p1(new int(1024) );
unique_ptr<int> p2(p1.release() );
unique_ptr<int> p3(new int(10));
p3.reset(p2.release());
release放弃对象的控制权,返回指向给定对象的指针,将当前unique_ptr置空;reset存在可选指针参数,当u.reset()调用,可以把u指向对象释放,如果以u.reset(ptr)形式调用,如果u为空,将u指向q这个内置指针指向对象,如果非空,则将u指向对象释放然后再指向q对应对象。
对于不能拷贝或者赋值还有个例外,就是一个将要被销毁的unique_ptr可以被拷贝或赋值,比如函数返回值。
三、weak_ptr类
weak_ptr有点特别,它是一个可以共享shared_ptr指向对象的智能指针,而且它绑定了shared_ptr指向对象后不会改变后者的引用计数,而且当指向对象的shared_ptr都被销毁,对象同样会被释放,不会因为weak_ptr的绑定而有所妥协。
前面说了,weak_ptr是绑定shared_ptr指向对象的,所以它的初始化是以shared_ptr对象进行初始化的。如下:
auto ptr = make_shared<string>("balabala");
weak_ptr<string> weak_p(ptr);
对于weak_ptr的使用需要谨慎,因为不知道它所指向的对象还在不在,所以不能直接访问:
shared_ptr<string> sp = weak_p.lock();
使用lock函数返回一个shared_ptr对象,当这个sp存在,指向的底层对象就会一直存在。
基本就这样,早期的标准库还有一个auto_ptr的具有unique_ptr一部分特性的类,但好像被抛弃,用的时候还是用unique_ptr吧。
应用例子更新中
|