本文主要介绍C++中的四个智能指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr
。其中auto_ptr
已经被弃用,原因我们下面说。
为什么要使用智能指针?
首先看下面一段代码:
void func()
{
int *p=new int(1);
return 0;
}
在这段代码中,我们使用了new在堆上开辟一个空间,但是我们在函数结束的时候并没有用delete释放用new申请的空间,那么这就会导致内存泄漏的问题。当然我们也知道如何解决它——return前面添加一个delete即可。但是很多时候,我们可能会因为一些原因而忘记这么做或者是做了但是在不经意之间将这段代码给删除了,那么有没有一种办法可以让释放空间这件事,由编译器自动完成,而不需要程序员自己来回收。于是就出现了智能指针。
什么是智能指针?
所谓的智能指针其实和普通的指针一样,可以完成和普通指针一样的工作,只不过普通指针需要程序员自己释放开辟的空间,而智能指针在它过期时,其析构函数会自动使用delete来释放空间。
auto_ptr
(1)使用办法
第一种:
auto_ptr<int> p(new int(1));
第二种:
int *p1=new int(1);
auto_ptr<int> p2(p1);
注意: 如果将auto_ptr<int> p2(p1)
改为auto_ptr<int> p2=p1
那么就会出错,因为auto_ptr
的构造函数时explicit,,它阻止了一般指针隐式转换为auto_ptr
的构造。
(2)auto_ptr关于拷贝和赋值的问题(重点)
auto_ptr在拷贝构造和赋值运算符重载时需要进行特殊的操作。而这个做法就是对所有权进行完全转移(设计思想),在拷贝和赋值时,会剥夺原auto_ptr
对指针的所有权,赋予当前auto_ptr
对指针的所有权。也就是说,当前auto_ptr
会获得原auto_ptr
对指针的管理,并将原auto_ptr
置为空。由于会修改原对象,所以auto_ptr
的拷贝构造和赋值运算符重载函数的参数是引用而不是常引用。
结合下面的代码具体说明:
auto_ptr<int> p1(new int(1));
auto_ptr<int> p2(p1); //auto_ptr<int> p2=p1;
解析:
auto_ptr<int> p2(p1);
这句代码因为调用了拷贝构造函数,所以会发生所有权转移的情况,也就是说,现在只能由p2去访问这块空间,而p1会被置为空。
这正是auto_ptr
的缺陷所在,一旦发生拷贝或者复制,那么原对象就会成为空指针,如果一旦不小心使用了一个空指针,那么程序就会崩溃,所以它的安全性也就不好,于是便舍弃了auto_ptr
。
unique_ptr
(1)设计思想
为了解决auto_ptr
的问题,于是便有了unique_ptr
,那么unique_ptr
是如何解决的呢?其实很简单,既然在拷贝和赋值的时候会发生问题,那么我就不让你拷贝和赋值,也就是说,如果你使用unique_ptr
进行了拷贝和赋值,那么编译就会报错。这就是uniqie_ptr
的设计思想:防拷贝、防赋值。是不是简单粗暴,如果我解决不了问题,那么我就解决提出问题的人。
(2)使用unique_ptr
时,需要直接初始化
正是因为它的设计思想,所以就不能用拷贝和赋值来进行初始化,只能直接对它初始化。
unique_ptr<int> p1(new int(1)); //正确,直接初始化
unqiue_ptr<int> p2=new int(1); //错误,赋值
unqiue_ptr<int> p3(p2); //错误,拷贝
(3)一个unique_ptr拥有它所指向的对象。
- 某一时刻只能有一个
unique_ptr
指向给定的对象 unique_ptr
被销毁时,它所指向的对象也就会被销毁
(4)unique_ptr与所拥有的对象的关系
在unique_ptr
的生命周期内,可以改变智能指针所指向的对象。
(1)在创建对象时,通过构造函数指定
(2)通过reset方法可以重新指定
(3)通过release方法可以释放所有权
(4)通过移动语义转移所有权。转移之后,原来的unique_ptr
不再拥有。
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2=move(p1); //转移所有权
p2.release(); //释放所有权
shared_ptr
shared_ptr
解决了上面所说的问题,使得多个shared_ptr
可以指向同一 个对象。而它的设计思路是:引用计数
(1)解决办法
对被管理的资源进行计数,当一个shared_ptr
对象要共享这个资源的时候,该资源的引用计数加1,当该对象的生命周期结束时,引用计数减1,这样最后一个对象被释放之后,资源的引用计数减到0,此时就调用析构函数,释放内存空间。
(2)拷贝和赋值的实现原理
首先要说明的是shared_ptr
中包含两个指针:一个指向计数器,一个指向数据成员。
定义一个shared_ptr对象:shared_ptr<A> p1(new A);
其对应的数据结构如下:
如果进行赋值:shared_ptr<A> p2=p1;
那么数据结构就如下图所示:
同样如果是拷贝,那么也是如上图所示
(3)shared_ptr的缺陷(循环引用计数)
虽然shared_ptr
弥补了unqiue_ptr
和auto_ptr
的不足。但是在某些时候shared_ptr
会有一个很大的缺陷。而这个缺陷我们叫做循环引用计数问题
我将结合一段代码来为大家讲述什么是循环引用计数。
struct ListNode
{
int _data;
shared_ptr<ListNode> _prev;
shared_ptr<ListNode> _next;
~ListNode(){ cout << "~ListNode()" << endl; }
};
int main()
{
shared_ptr<ListNode> node1(new ListNode);
shared_ptr<ListNode> node2(new ListNode);
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
node1->_next = node2;
node2->_prev = node1;
cout << node1.use_count() << endl;
cout << node2.use_count() << endl;
return 0;
}
解析:
- 首先创建了两个
shared_ptr
对象node1
和node2
分别指向了两个不同的空间。 - 所以此时他们的引用计数都是1。
- 然后执行
node1->_next = node2;
,于是node2
的的引用计数加1,变成了2。 - 同理,执行
node2->_prev = node1;
,node1
的引用计数加1,也变成了2. - 当函数执行完,对象生命周期结束时,
node1
和node2
析构,所以他们的引用计数减到1。但是此时_next
还指向的node2
,_prev
还指向的node1
。 - 也就是说
_next
析构了,node2
就释放了。 - 也就是说
_prev
析构了,node1
就释放了。 - 但是
_next
属于node1
的成员,node1
释放了,_next
才会析构,而node1
想要释放,那么就要_prev
析构,而_prev
想要析构就要node2
释放,而node2
想要释放就要_next
析构。所以这就叫循环引用,谁也不会释放谁。
weak_ptr
那么如何解决上面所述的循环引用计数的问题呢。这就需要weak_ptr
什么是weak_ptr?
是一种不控制所指向对象生命周期的智能指针,它指向一个shared_ptr管理的对象。
注意:
weak_ptr绑定到shared_ptr时,不会改变对象的引用计数。
当shared_ptr被销毁时,指向的对象也被销毁。不论weak_ptr是否指向了它。
解决办法
在引用计数的场景下,把节点中的_prev和_next改成weak_ptr就可以了。
原理: node1->_next = node2;
和node2->_prev = node1;
时weak_ptr
的_next
和_prev
不会增加node1
和node2
的引用计数。