C++ 智能指针

本文主要介绍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拥有它所指向的对象。
  1. 某一时刻只能有一个unique_ptr指向给定的对象
  2. 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_ptrauto_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;
}

解析:

  1. 首先创建了两个shared_ptr对象node1node2分别指向了两个不同的空间。
  2. 所以此时他们的引用计数都是1。
  3. 然后执行node1->_next = node2;,于是node2的的引用计数加1,变成了2。
  4. 同理,执行node2->_prev = node1;node1的引用计数加1,也变成了2.
  5. 当函数执行完,对象生命周期结束时,node1node2析构,所以他们的引用计数减到1。但是此时_next还指向的node2_prev还指向的node1
  6. 也就是说_next析构了,node2就释放了。
  7. 也就是说_prev析构了,node1就释放了。
  8. 但是_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不会增加node1node2的引用计数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值