STL一共给我们提供了四种智能指针:auto_ptr、unique_ptr、shared_ptr和weak_ptr,auto_ptr是C++98提供的解决方案,C+11已将其摒弃,并提出了unique_ptr作为auto_ptr替代方案。虽然auto_ptr已被摒弃,但在实际项目中仍可使用,但建议使用较新的unique_ptr,因为unique_ptr比auto_ptr更加安全,后文会详细叙述。shared_ptr和weak_ptr则是C+11从准标准库Boost中引入的两种智能指针。此外,Boost库还提出了boost::scoped_ptr、boost::scoped_array、boost::intrusive_ptr 等智能指针,虽然尚未得到C++标准采纳,但是实际开发工作中可以使用。
#1.unique_ptr
unique_ptr由C++11引入,旨在替代不安全的auto_ptr。unique_ptr是一种定义在\< memory>中的智能指针。它持有对对象的独有权——两个unique_ptr不能指向一个对象,即unique_ptr不共享它的所管理的对象。它无法复制到其他unique_ptr,无法通过值传递到函数,也无法用于需要副本的任何标准模板库 (STL)算法。只能移动 unique_ptr,即对资源管理权限可以实现转移。这意味着,内存资源所有权可以转移到另一个unique_ptr,并且原始 unique_ptr 不再拥有此资源。实际使用中,建议将对象限制为由一个所有者所有,因为多个所有权会使程序逻辑变得复杂。因此,当需要智能指针用于纯 C++ 对象时,可使用 unique_ptr,而当构造 unique_ptr 时,可使用 make_unique Helper 函数。
下图演示了两个 unique_ptr 实例之间的所有权转换。
unique_ptr与原始指针一样有效,并可用于 STL 容器。将 unique_ptr 实例添加到 STL 容器运行效率很高,因为通过 unique_ptr 的移动构造函数,不再需要进行复制操作。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权,unique_ptr还可能没有对象,这种情况被称为empty。
unique_ptr的基本操作有:
1 | //智能指针的创建 |
2.auto_ptr
auto_ptr 同样是STL中智能指针家族的成员之一,由C++98引入,定义在头文件\
auto_ptr从C++98使用至今,为何从C++11开始,引入unique_ptr来替代auto_ptr呢?原因主要有如下几点:
(1)基于安全考虑
先来看下面的赋值语句:
1 | auto_ptr< string> ps (new string ("I reigned lonely as a cloud.”); |
上述赋值语句将完成什么工作呢?如果ps和vocation是常规指针,则两个指针将指向同一个string对象。这是不能接受的,因为程序将试图删除同一个对象两次,一次是ps过期时,另一次是vocation过期时。要避免这种问题,方法有多种:
(1)定义陚值运算符,使之执行深复制。这样两个指针将指向不同的对象,其中的一个对象是另一个对象的副本,缺点是浪费空间,所以智能指针都未采用此方案。
(2)建立所有权(ownership)概念。对于特定的对象,只能有一个智能指针可拥有,这样只有拥有对象的智能指针的析构函数会删除该对象。然后让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr 的策略,但unique_ptr的策略更严格。
(3)创建智能更高的指针,跟踪引用特定对象的智能指针数。这称为引用计数。例如,赋值时,计数将加1,而指针过期时,计数将减1,。当减为0时才调用delete。这是shared_ptr采用的策略。
当然,同样的策略也适用于复制构造函数,即auto_ptr\
下面举个例子来说明。
1 | # include <iostream> |
运行下发现程序崩溃了,原因在上面注释已经说的很清楚,films[2]已经是空指针了,下面输出访问空指针当然会崩溃了。但这里如果把auto_ptr换成shared_ptr或unique_ptr后,程序就不会崩溃,原因如下:
使用shared_ptr时运行正常,因为shared_ptr采用引用计数,pwin和films[2]都指向同一块内存,在释放空间时因为事先要判断引用计数值的大小因此不会出现多次删除一个对象的错误。
使用unique_ptr时编译出错,与auto_ptr一样,unique_ptr也采用所有权模型,但在使用unique_ptr时,程序不会等到运行阶段崩溃,而在编译期因下述代码行出现错误:
1 | unique_ptr<string> pwin; |
指导你发现潜在的内存错误。这就是为何要摒弃auto_ptr的原因,一句话总结就是:避免因潜在的内存问题导致程序崩溃。
从上面可见,unique_ptr比auto_ptr更加安全,因为auto_ptr有拷贝语义,拷贝后原象变得无效,再次访问原对象时会导致程序崩溃;unique_ptr则禁止了拷贝语义,但提供了移动语义,即可以使用std::move()进行控制权限的转移,如下代码所示:
1 | unique_ptr<string> upt(new string("lvlv")); |
这里要注意,在使用std::move将unique_ptr的控制权限转移后,不能够再通过unique_ptr来访问和控制资源了,否则同样会出现程序崩溃。我们可以在使用unique_ptr访问资源前,使用成员函数get()进行判空操作。
1 | unique_ptr<string> upt1=std::move(upt); //控制权限转移 |
(2)unique_ptr不仅安全,而且灵活
如果unique_ptr 是个临时右值,编译器允许拷贝语义。参考如下代码:
1 | unique_ptr<string> demo(const char * s) |
demo()返回一个临时unique_ptr,然后ps接管了临时对象unique_ptr所管理的资源,而返回时临时的 unique_ptr 被销毁,也就是说没有机会使用 unique_ptr 来访问无效的数据,换句话来说,这种赋值是不会出现任何问题的,即没有理由禁止这种赋值。实际上,编译器确实允许这种赋值。相对于auto_ptr任何情况下都允许拷贝语义,这正是unique_ptr更加灵活聪明的地方。
(3)扩展auto_ptr不能完成的功能
(3.1)unique_ptr可放在容器中,弥补了auto_ptr不能作为容器元素的缺点。
1 | //方式一: |
(3.2)管理动态数组,因为unique_ptr有unique_ptr<X[]>重载版本,销毁动态对象时调用delete[]。
1 | unique_ptr<int[]> p (new int[3]{1,2,3}); |
(3.3)自定义资源删除操作(Deleter)。unique_ptr默认的资源删除操作是delete/delete[],若需要,可以进行自定义:
1 | void end_connection(connection *p) { disconnect(*p); } //资源清理函数 |
综上所述,基于unique_ptr的安全性和扩充的功能,unique_ptr成功的将auto_ptr取而代之。
3.shared_ptr
3.1shared_ptr简介
shared_ptr 是一个标准的共享所有权的智能指针,允许多个指针指向同一个对象,定义在 memory 文件中,命名空间为 std。shared_ptr最初实现于Boost库中,后由C++11引入到C++ STL。shared_ptr利用引用计数的方式实现了对所管理的对象的所有权的分享,即允许多个shared_ptr共同管理同一个对象。像shared_ptr这种智能指针,《Effective C++》称之为“引用计数型智能指针”(reference-counting smart pointer,RCSP)。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的),在使用引用计数的机制上提供了可以共享所有权的智能指针,当然这需要额外的开销:
(1)shared_ptr 对象除了包括一个所拥有对象的指针外,还必须包括一个引用计数代理对象的指针;
(2)时间上的开销主要在初始化和拷贝操作上, *和->操作符重载的开销跟auto_ptr是一样;
(3)开销并不是我们不使用shared_ptr的理由,,永远不要进行不成熟的优化,直到性能分析器告诉你这一点。
3.2通过辅助类模拟实现shared_ptr
(1)基础对象类
首先,我们来定义一个基础对象类Point类,为了方便后面我们验证智能指针是否有效,我们为Point类创建如下接口:
1 | class Point |
(2)辅助类
在创建智能指针类之前,我们先创建一个辅助类。这个类的所有成员皆为私有类型,因为它不被普通用户所使用。为了只为智能指针使用,还需要把智能指针类声明为辅助类的友元。这个辅助类含有两个数据成员:计数count与基础对象指针。也即辅助类用以封装使用计数与基础对象指针。
1 | class RefPtr |
(3)为基础对象类实现智能指针类
引用计数是实现智能指针的一种通用方法。智能指针将一个计数器与类指向的对象相关联,引用计数跟踪共有多少个类对象共享同一指针。它的具体做法如下:
(3.1)当创建智能指针类的新对象时,初始化指针,并将引用计数设置为1;
(3.2)当能智能指针类对象作为另一个对象的副本时,拷贝构造函数复制副本的指向辅助类对象的指针,并增加辅助类对象对基础类对象的引用计数(加1);
(3.3)使用赋值操作符对一个智能指针类对象进行赋值时,处理复杂一点:先使左操作数的引用计数减1(为何减1:因为指针已经指向别的地方),如果减1后引用计数为0,则释放指针所指对象内存。然后增加右操作数所指对象的引用计数(为何增加:因为此时做操作数指向对象即右操作数指向对象)。
(3.4)完成析构函数:调用析构函数时,析构函数先使引用计数减1,如果减至0则delete对象。
做好前面的准备后,我们可以为基础对象类Point书写一个智能指针类了。根据引用计数实现关键点,我们可以写出如下智能指针类:
1 | class SmartPtr |
(4)智能指针类的使用与测试
至此,我们的智能指针类就完成了,我们可以来看看如何使用。
1 | int main() |
//定义三个智能指针类对象,对象都指向基础类对象pa
//使用花括号控制三个智能指针的生命周期,观察计数的变化
{
SmartPtr sptr1(pa);//此时计数count=1
cout <<”sptr1:”<
{
SmartPtr sptr2(sptr1); //调用拷贝构造函数,此时计数为count=2
cout<<”sptr2:” <
{
SmartPtr sptr3=sptr1; //调用赋值操作符,此时计数为conut=3
cout<<”sptr3:”<<(sptr3).getX()<<”,”<<(sptr3).getY()<<endl;
}
//此时count=2
}
//此时count=1;
}
//此时count=0;pa对象被delete掉
cout << pa->getX ()<< endl;
system(“pause”);
return 0;
1
2
}
运行结果:
1 | sptr1:10,20 |
如期,在离开大括号后,共享基础对象的指针从3->2->1->0变换,最后计数为0时,pa对象被delete,此时使用getX()已经获取不到原来的值。
(5)对智能指针的改进
目前这个智能指针只能用于管理Point类的基础对象,如果此时定义了个矩阵的基础对象类,那不是还得重新写一个属于矩阵类的智能指针类吗?但是矩阵类的智能指针类设计思想和Point类一样啊,就不能借用吗?答案当然是能,那就是使用模板技术。为了使我们的智能指针适用于更多的基础对象类,我们有必要把智能指针类通过模板来实现。这里贴上上面的智能指针类的模板版本:
1 | //模板类作为友元时要先有声明 |
//构造函数的参数为基础对象的指针
RefPtr(T *ptr) :p(ptr), count(1) { }
//析构函数
~RefPtr() { delete p; }
//引用计数
int count;
//基础对象指针
T *p;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
};
//智能指针类
template <typename T> class SmartPtr
{
public:
SmartPtr(T *ptr) :rp(new RefPtr<T>(ptr)) { } //构造函数
SmartPtr(const SmartPtr<T> &sp) :rp(sp.rp) { ++rp->count; } //复制构造函数
SmartPtr& operator=(const SmartPtr<T>& rhs) //重载赋值操作符
{
++rhs.rp->count; //首先将右操作数引用计数加1,
if (--rp->count == 0) //然后将引用计数减1,可以应对自赋值
delete rp;
rp = rhs.rp;
return *this;
}
T & operator () //重载操作符
{
return (rp->p);
}
T operator ->() //重载->操作符
{
return rp->p;
}
~SmartPtr() //析构函数
{
if (–rp->count == 0) //当引用计数减为0时,删除辅助类对象指针,从而删除基础对象
delete rp;
else
{
cout << “还有” << rp->count << “个指针指向基础对象” << endl;
}
}
1
2
3
4
private:
RefPtr<T> *rp; //辅助类对象指针
};
现在使用智能指针类模板来共享其它类型的基础对象,以int为例:
1 | int main() |
测试结果如下:
1 | sptr1:10 |
4.weak_ptr
4.1weak_ptr简介
weak_ptr被设计为与shared_ptr共同工作,可以从一个shared_ptr或者另一个weak_ptr对象构造而来。weak_ptr是为了配合shared_ptr而引入的一种智能指针,它更像是shared_ptr的一个助手而不是智能指针,因为它不具有普通指针的行为,没有重载operator*和->,因此取名为weak,表明其是功能较弱的智能指针。它的最大作用在于协助shared_ptr工作,可获得资源的观测权,像旁观者那样观测资源的使用情况。观察者意味着weak_ptr只对shared_ptr 进行引用,而不改变其引用计数,当被观察的shared_ptr失效后,相应的weak_ptr也相应失效。
4.2用法
使用weak_ptr的成员函数use_count()可以观测资源的引用计数,另一个成员函数expired()的功能等价于use_count()==0,但更快,表示被观测的资源(也就是shared_ptr管理的资源)已经不复存在。weak_ptr可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr管理的对象, 从而操作资源。但当expired()==true的时候,lock()函数将返回一个存储空指针的shared_ptr。总结来说,weak_ptr的基本用法总结如下:
1 | weak_ptr<T> w; //创建空weak_ptr,可以指向类型为T的对象。 |
下面是一个简单的使用示例:
1 | # include < assert.h> |
程序输出:
1 | int:100 |
从上面可以看到,尽管以shared_ptr来构造weak_ptr,但是weak_ptr内部的引用计数并没有什么变化。
4.3weak_ptr的作用
现在要说的问题是,weak_ptr到底有什么作用呢?从上面那个例子看来,似乎没有任何作用。其实weak_ptr可用于打破循环引用。引用计数是一种便利的内存管理机制,但它有一个很大的缺点,那就是不能管理循环引用的对象。一个简单的例子如下:
1 | # include <iostream> |
void doSomthing()
{
if(_wife.lock())
{
}
}
~Man()
{
std::cout << “kill man\n”;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
};
class Woman
{
private:
//std::weak_ptr<Man> _husband;
std::shared_ptr<Man> _husband;
public:
void setHusband(std::shared_ptr<Man> man)
{
_husband = man;
}
~Woman()
{
std::cout <<"kill woman\n";
}
};
int main(int argc, char** argv)
{
std::shared_ptr<Man> m(new Man());
std::shared_ptr<Woman> w(new Woman());
if(m && w)
{
m->setWife(w);
w->setHusband(m);
}
return 0;
}
在Man类内部会引用一个Woman,Woman类内部也引用一个Man。当一个man和一个woman是夫妻的时候,他们直接就存在了相互引用问题。man内部有个用于管理wife生命期的shared_ptr变量,也就是说wife必定是在husband去世之后才能去世。同样的,woman内部也有一个管理husband生命期的shared_ptr变量,也就是说husband必须在wife去世之后才能去世。这就是循环引用存在的问题:husband的生命期由wife的生命期决定,wife的生命期由husband的生命期决定,最后两人都死不掉,违反了自然规律,导致了内存泄漏。
一般来讲,解除这种循环引用有下面三种可行的方法:
(1)当只剩下最后一个引用的时候需要手动打破循环引用释放对象。
(2)当parent的生存期超过children的生存期的时候,children改为使用一个普通指针指向parent。
(3)使用弱引用的智能指针打破这种循环引用。
虽然这三种方法都可行,但方法1和方法2都需要程序员手动控制,麻烦且容易出错。这里主要介绍一下第三种方法,使用弱引用的智能指针std:weak_ptr来打破循环引用。
weak_ptr对象引用资源时不会增加引用计数,但是它能够通过lock()方法来判断它所管理的资源是否被释放。做法就是上面的代码注释的地方取消注释,取消Woman类或者Man类的任意一个即可,也可同时取消注释,全部换成弱引用weak_ptr。
另外很自然地一个问题是:既然weak_ptr不增加资源的引用计数,那么在使用weak_ptr对象的时候,资源被突然释放了怎么办呢?不用担心,因为不能直接通过weak_ptr来访问资源。那么如何通过weak_ptr来间接访问资源呢?答案是在需要访问资源的时候weak_ptr为你生成一个shared_ptr,shared_ptr能够保证在shared_ptr没有被释放之前,其所管理的资源是不会被释放的。创建shared_ptr的方法就是lock()成员函数。
注意: shared_ptr实现了operator bool() const方法来判断一个管理的资源是否被释放。
5.如何选择智能指针
上文简单地介绍了C++标准模板库STL中四种智能指针,当然,除了STL中的智能指针,C++准标准库Boost中的智能指针,比如boost::scoped_ptr、boost::shared_array、boost:: intrusive_ptr也可以在实际编程实践中拿来使用,但这里不做进一步的介绍,有兴趣的读者可以参考:C++ 智能指针详解。
在了解STL中的四种智能指针后,大家可能会想另一个问题:在实际应用中,应使用哪种智能指针呢?
下面给出几个使用指南。
(1)如果程序要使用多个指向同一个对象的指针,应选择shared_ptr。这样的情况包括:
(1.1)有一个指针数组,并使用一些辅助指针来标示特定的元素,如最大的元素和最小的元素;
(1.2)两个对象都包含指向第三个对象的指针;
(1.3)STL容器包含指针。很多STL算法都支持复制和赋值操作,这些操作可用于shared_ptr,但不能用于unique_ptr(编译器发出warning)和auto_ptr(行为不确定)。如果你的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
(2)如果程序不需要多个指向同一个对象的指针,则可使用unique_ptr。如果函数使用new分配内存,并返还指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权转让给接受返回值的unique_ptr,而该智能指针将负责调用delete。可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋值给另一个的算法(如sort())。例如,可在程序中使用类似于下面的代码段。
1 | unique_ptr<int> make_int(int n) |
其中push_back调用没有问题,因为它返回一个临时unique_ptr,该unique_ptr被赋给vp中的一个unique_ptr。另外,如果按值而不是按引用给show()传递对象,for_each()将非法,因为这将导致使用一个来自vp的非临时unique_ptr初始化pi,而这是不允许的。前面说过,编译器将发现错误使用unique_ptr的企图。
在unique_ptr为右值时,可将其赋给shared_ptr,这与将一个unique_ptr赋给另一个unique_ptr需要满足的条件相同,即unique_ptr必须是一个临时的对象。与前面一样,在下面的代码中,make_int()的返回类型为unique_ptr\< int>:
1 | unique_ptr<int> pup(make_int(rand() % 1000)); // ok |
模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptr。shared_ptr将接管原来归unique_ptr所有的对象。
在满足unique_ptr要求的条件时,也可使用auto_ptr,但unique_ptr是更好的选择。如果你的编译器没有unique_ptr,可考虑使用Boost库提供的scoped_ptr,它与unique_ptr类似。
参考文献
[1]Stanley B.Lippman著,王刚,杨巨峰译.C++ Primer(第五版).2013:400-422
[2]Scott Meyers著,侯捷译.Effective C++中文版(第三版).2011:61-77
[3]C++智能指针简单剖析
[4]shared_ptr基于引用计数智能指针实现
[5] C++中智能指针的设计和使用
[6]C++11智能指针之unique_ptr
[7]Boost智能指针——weak_ptr
[8]std::shared_ptr 和 std::weak_ptr的用法以及引用计数的循环引用问题
作者:Dablelv
来源:CSDN
原文:https://blog.csdn.net/k346k346/article/details/81478223
版权声明:本文为博主原创文章,转载请附上博文链接!