C++——智能指针简介[From Net]
一、什么是对象所有权?
在接触智能指针之前首先要理解对象的所有权是什么,在这之前我们总是用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
的unique_ptr
,而像make_unique(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
。 先来看一段代码:
1 | #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;
所以调用它的时候是会出错的。 再来看一段代码:
1 | #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
)
1 | #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
,分享所有权。 看下面这段代码:
1 | #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()
函数。 来看下面这段代码:
1 | #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
稍后再谈。
如果程序要使用多个指向同一个对象的指针,应选择
1
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
赋值给它们。- 智能指针的内存泄漏:千万不要以为智能指针就不会导致内存泄漏了。现在我们来看一段代码:
1 | #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
的问题,使得程序员能够高效而安全地管理堆内存