##一、什么是对象所有权?
在接触智能指针之前首先要理解对象的所有权是什么,在这之前我们总是用new和delete来进行内存的申请与释放,在这种堆内存分配的方式中,要遵守一个很基本的原则——谁创建谁销毁原则,简单地举个例子,类foo构造函数中中通过new申请了一个数组,那么就要在类foo的析构函数中delete这个数组,而对象的所有权指的就是谁负责delete这个对象的关系。
根据几种智能指针的用途来分,对象的所有权可以分为独占所有权、分享所有权和弱引用。现在来一一介绍它们:
独占所有权:假设Dad拥有Son的独占所有权,那么Son就必须由Dad来delete,再从字面上看,独占意味如果有另一个对象OldWang想要持有Son的话,就必须让Dad放弃对Son的所有权,此所谓独占,亦即不可分享。
分享所有权:假设Dad拥有PS4的分享所有权,那么由最后一个持有PS4的对象来对其进行delete,也就是说,如果这个时候Son也想要持有PS4,Dad不必放弃自己的所有权,而是把所有权分享给Son,而如果Dad被销毁(生命周期结束、被释放等),那PS4就在Son被销毁时被释放,反之如果Son先于Dad被销毁,那么PS4就由Dad来释放。
弱引用:假设AccountThief对Account有弱引用的话,那么AccountThief可以使用Account,但是AccountThief不负责释放Account,如果Account已经被拥有其所有权的对象(比如AccountOwner)释放后,AccountThief还想继续使用Account的时候就会取得一个nullptr(nullptr勉强可以当做NULL来看,前者是关键字,后者是宏定义)。
##二、什么是智能指针?
智能指针是行为类似于指针的模板类对象,可以对其进行解引用*,指向结构体成员->等操作但是不能进行指针算术运算比如自加++、自减--等,因为指针算术运算实现上是根据指针所指空间大小来进行内存位置上的偏移,而智能指针是类对象,这种运算是没有意义的。当然它具有智能的地方,智能指针最为方便的就是能自动管理堆内存,无需关心何时收回已分配的内存。这么一听是不是觉得它很强?但先别高兴的太早,也有一些需要注意的地方,所有事情都有两面性,了解智能指针后我们将会提到一些需要注意的地方。
然后根据以上介绍的所有权类型,一一对应地,我们有unique_ptr、shared_ptr、weak_ptr,还有被时代抛弃的auto_ptr(稍后也会谈谈它为什么被抛弃)。
##三、怎么使用智能指针?
### 1、怎么使用unique_ptr?
在使用unique_ptr的时候,首先要知道它的make函数make_unique(),make_unique()可以构造一个类对象并返回指向它的unique_ptr,例如make_unique<int>(),就会返回一个指向int的unique_ptr,而像make_unique<int>(1)一样带有参数就会返回一个指向值为1的int的unique_ptr,实际上就上面两个就相当于先通过int{}和int{1}创建对象并构造指向它们的unique_ptr。那么对于得到的unique_ptr,我们可以像使用指针一样去使用。
现在我们有Dad对象是指向Son的unique_ptr,如果要Dad放弃对这个对象的所有权,就需要调用Dad.release()来将所有权进行释放,这个函数将会返回指向Son的普通指针Son*,现在就可以用这个不属于任何人的普通指针来构造一个新的unique_ptr。如果这个sonPtr是假的(这个指针没有管理对象,也就是说Son是不存在的),那么调用release()的时候就会返回nullptr。
先来看一段代码:
#include <iostream>
#include <memory>
using namespace std; //不提倡
int main() {
unique_ptr<string> Dad{make_unique<string>("Son")};
unique_ptr<string> OldWang{Dad};
}
根本就过不了编译!试图把unique_ptr赋值给另一个unique_ptr在编译时就会出错!
说一句题外话:其具体实现方法是重载了unique_ptr的拷贝构造函数,unique_ptr的定义中有一句:
unique_ptr(const unique_ptr&) = delete;
所以调用它的时候是会出错的。
再来看一段代码:
#include <iostream>
#include <memory>
using namespace std; //不提倡
int main() {
unique_ptr<string> Dad{make_unique<string>("Son")};
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
unique_ptr<string> OldWang{Dad.release()};
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
cout << "OldWang owns " << (OldWang ? *OldWang : "nothing") << endl;
}
上面有一个用法,就是直接把unique_ptr转换成bool值来判断其是否为空。
那么这段程序的输出,不出所料的是:
Dad owns Son
Dad owns nothing
OldWang owns Son
那么如果这样呢?(差别仅仅是Dad为nullptr)
#include <iostream>
#include <memory>
using namespace std; //不提倡
int main() {
unique_ptr<string> Dad{nullptr};
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
unique_ptr<string> OldWang{Dad.release()};
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
cout << "OldWang owns " << (OldWang ? *OldWang : "nothing") << endl;
}
那么调用Dad.release()后就得到了一个空指针。
显然输出将会变成:
Dad owns nothing
Dad owns nothing
OldWang owns nothing
### 2、怎么使用shared_ptr?
接下来让我们看shared_ptr,了解了unique_ptr之后,shared_ptr的使用就显得不陌生了,具体使用时,同样需要用到make函数make_shared(),shared_ptr和unique_ptr的不同之处它在于是可以共享的,它可以赋值给其他shared_ptr,分享所有权。
看下面这段代码:
#include <iostream>
#include <memory>
using namespace std;
int main() {
shared_ptr<string> Dad{make_shared<string>("PS4")};
shared_ptr<string> Son{nullptr};
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
cout << "Son owns " << (Son ? *Son : "nothing") << endl;
cout << endl;
Son = Dad;
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
cout << "Son owns " << (Son ? *Son : "nothing") << endl;
cout << endl;
Dad = nullptr;
cout << "Dad owns " << (Dad ? *Dad : "nothing") << endl;
cout << "Son owns " << (Son ? *Son : "nothing") << endl;
}
这段代码的输出会是:
Dad owns PS4
Son owns nothing
Dad owns PS4
Son owns PS4
Dad owns nothing
Son owns PS4
可以看出,shared_ptr的用法和unique_ptr的用法比较相似,只不过可以共享对指向对象的所有权而已。
如果有多个shared_ptr指向某对象,则在最后一个指针过期时才释放该对象,这个功能在实现的时候运用到了引用计数,即跟踪引用特定对象的智能指针数,例如:赋值给新指针时,计数将+1,指针过期时,计数将-1,当计数为0时,该特定对象将会被delete。
### 3、怎么使用weak_ptr?
我们需要从shared_ptr构造一个weak_ptr,也就是说有weak_ptr就肯定有shared_ptr,如果要使用weak_ptr所指向的对象,我们需要调用lock()函数,这个函数会返回所指对象的shared_ptr(在对象被销毁后就会得到空的share_ptr)。
weak_ptr还有expired()函数来取得它所指向的对象是否还存在的bool值,expired意为期满的,所以若返回值为真,则所指对象已被销毁,反之所指对象仍存在。这个函数存在的意义在于如果仅仅是为了判断对象是否健在,那么不需要调用lock()函数。
来看下面这段代码:
#include <iostream>
#include <memory>
using namespace std; //不提倡
int main() {
shared_ptr<string> AccountOwner{make_shared<string>("Account")};
weak_ptr<string> AccountThief{AccountOwner};
cout << "AccountOwner owns "
<< (AccountOwner ? *AccountOwner : "nothing") << endl;
cout << "AccountThief can use "
<< (!AccountThief.expired() ? *AccountThief.lock() : "nothing") << endl;
cout << endl;
AccountOwner = nullptr;
cout << "AccountOwner owns "
<< (AccountOwner ? *AccountOwner : "nothing") << endl;
cout << "AccountThief can use "
<< (!AccountThief.expired() ? *AccountThief.lock() : "nothing") << endl;
}
这段代码的输出将会是:
AccountOwner owns Account
AccountThief can use Account
AccountOwner owns nothing
AccountThief can use nothing
weak_ptr的用法可以从这段代码中窥见一斑。
### 4、如何选取智能指针?
上面介绍了三种智能指针,那么什么时候使用哪种呢?先来看一下shared_ptr和unique_ptr的使用场景,weak_ptr稍后再谈。
- 如果程序要使用多个指向同一个对象的指针,应选择
shared_ptr。这样的情况包括:
- 有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大和最小(需要被赋值指向特定元素)。
- 两个对象都包含指向第三个对象的指针。
- STL容器包含指针。
- ……
- 如果程序不需要多个指向同一个对象的指针,则可使用
unique_ptr,还有如果某函数使用new分配了内存,并返回指向该内存的指针,那么返回一个unique_ptr会是一个很好的选择。
有兴趣可以了解一下单例模式,并且想想其中的instance需要用哪种智能指针?
##四、auto_ptr为什么被抛弃了?
首先来看一下auto_ptr的概念,auto_ptr是在C++11标准前使用的智能指针,虽然也建立了所有权的概念,但是定位不够明确,一个auto_ptr(例如oldPtr)能够赋值给其他auto_ptr(例如newPtr),但是oldPtr对指向对象(例如Object)的所有权将被newPtr剥夺,虽然这将避免oldPtr和newPtr各自调用一次Object的析构函数,但是再次使用oldPtr的时候会导致难以预料的结果,因为它不再指向有效的数据。这样看看,auto_ptr是不是同时具有了shared_ptr的赋值功能,以及unique_ptr对对象的独占所有权?所以使用不当会导致各种问题,有的时候还会难以发现。因此使用unique_ptr比使用auto_ptr更加安全,在编译时就可以避免非法的赋值操作,如果想要分享所有权,那很自然的就应该使用shared_ptr。
##五、智能指针注意事项
- 我们不能把并不指向堆内存的指针赋值给智能指针,试想
delete一个指向栈内存的指针会发生什么?智能指针还没有智能到自动分辨堆上对象和栈上对象。 unique_ptr和shared_ptr都有get()函数,能在不放弃所有权的情况下返回所指向对象的普通指针。unique_ptr和shared_ptr还有reset()函数,效果相当于把nullptr赋值给它们。- 智能指针的内存泄漏:千万不要以为智能指针就不会导致内存泄漏了。现在我们来看一段代码:
#include <memory>
using namespace std; //不提倡
class foo {
public:
shared_ptr<foo> father;
};
int main() {
shared_ptr<foo> a{make_shared<foo>()};
shared_ptr<foo> b{make_shared<foo>()};
a->father = b;
b->father = a;
}
这种情况下两个对象都分别被两个shared_ptr所指,一个是a和b->father,一个是b和a->father,当函数执行完以后,虽然a和b被析构,但是原来的a->father和b->father(对象自身含有的指针)仍然指着这两个对象,也就是说在这种情况下,一个对象的智能指针通过某种路径的一连串智能指针最终指向了自身,最终构成了一个环,这个对象就永远不会被自动释放了,这就是智能指针会造成的内存泄漏,那么为了避免这种情况,就轮到了我们的weak_ptr出场了。将上述类foo中的father替换成weak_ptr类型,就可以既实现相同的效果(当然取得对象的时候要用lock()),又避免了内存泄漏。
这种误用导致的问题,在链表中比较常见,比如双向链表、循环链表等等,又比如说需要记录父节点的树,在这些情况下,会出现某对象内指针指向的对象内的指针指向自己的情况,使用shared_ptr就会引发内存泄漏,这是要务必小心的。
- 不要把智能指针作为节省
delete语句的工具,它们本身就是用来表示所有权关系的,使用的时候还是要从所有权的角度进行分析。
##六、总结
智能指针给程序设计带来了极大的方便,使得**“别忘记delete”**不再那么困扰程序员,并且解决了某些场景下不知如何安放delete的问题,使得程序员能够高效而安全地管理堆内存,今后我亦会尝试使用智能指针,在此共勉。
智能指针详解
791

被折叠的 条评论
为什么被折叠?



