第一篇——C++11中的智能指针

第一篇——C++11中的智能指针



前言

1. C++动态内存管理通过一对运算符来完成:

  new在动态内存中为对象分配空间并返回一个指向该对象的指针,可以对对象初始化;
  delete接受一个动态对象的指针,销毁该对象,并释放与之相关的内存。

2. 释放内存

  智能指针可以在适当时机自动释放分配的内存,可以很好地避免“忘记释放内存而内存泄漏”。

3. C++智能指针底层是采用引用计数的方式实现:

  在申请堆内存空间时,会配备一个整型值(初始值为1),每当有新对象使用此堆内存时,该值+1;反之,每当使用此堆内存的对象被释放时,该值-1。当堆空间对应的整型值为0时,即表明不会再有任何对象使用它,该堆空间被释放。


一、shared_ptr智能指针

  1. 实际上,每种智能指针都是以类模板的方式实现的。
  2. shared_ptr<T> 定义位于< memory>头文件、std命名空间中,其中T表示指针指向的具体数据类型。
  3. 多个 shared_ptr 智能指针可以共同使用同一块堆内存,在实现上采用的是 引用计数 机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数-1),也不影响其他指向同一堆内存的 shared_ptr 指针,只有引用计数为0时,堆内存才会被自动释放。

4. 创建方法

(1) 空智能指针:

shared_ptr<int> p1;					//不传入任何参数
shared_ptr<int> p2(nullptr);		//传入空指针nullptr

空的 shared_ptr 指针,其引用计数初始为0,而不是1。

(2) 明确指向:

shared_ptr<int> p3(new int(10));	//指向一块有数字10这个int类型数据的堆内存空间

另一种写法相同:

shared_ptr<int> p3 = make_shared<int>(10);

(3) 构造函数:

拷贝构造:

shared_ptr<int> p4(p3);		//p4指向p3当前指向的对象
shared_ptr<int> p4 = p3;	//另一种写法

移动构造:

shared_ptr<int> p5(move(p4));		//p4所拥有的指向所有权转给p5
shared_ptr<int> p5 = move(p4);		//另一种写法

  拷贝构造不会导致新的分配,而是使p4也指向p3当前指向的同一个对象;同时,若p3指向空,则拷贝出的p4也指向空,其内存的引用计数为0;若p3不为空,则拷贝出的p4使其所指向的内存引用计数+1;p3、p4不仅指向同一块地址,而且它们共享对那个地址的所有权,只有当所有指向该对象的指针都被销毁或重置时,引用计数才会变成0,这时动态分配的对象才会被自动删除。
  移动构造函数会将p4所拥有的拥有权转移到p5,且使p4变成一个空的shared_ptr,p4由p3拷贝构造而来,而p4被移动后不会影响p3,p3仍然在指向原对象,此时,p3、p5指向该对象,p4指空,该对象的引用计数为2。

(4) 创建初始化:

  当创建一个shared_ptr时,若它是第一个指向某对象的shared_ptr,就会为其分配一个新的控制块,控制块是与该对象绑定到一起的,其存储指向自身的指针、引用奇数等信息。

(5) 同一普通指针不能同时为多个shared_ptr对象赋值:

错误例子:

int* ptr = new int;
shared_ptr<int> p1(ptr);
shared_ptr<int> p2(ptr);	//错误

  用同一个原始指针ptr来初始化两个不同的shared_ptr实例p1和p2,p1和p2都认为其拥有对对象的唯一所有权,各自维护的引用计数皆为1,当任何一个销毁都会删除对象。

正确例子:

int* ptr = new int;
shared_ptr<int> p1(ptr);	//ptr的所有权转移给p1,不再使用ptr,p1现在负责管理它
shared_ptr<int> p2 = p1;	//p2拷贝p1,p2和p1共享所有权

  此外,创建shared_ptr时最好使用:

shared_ptr<int> p1 = make_shared<int>();

因为make_shared不仅可以创建shared_ptr,还创建控制块,更为高效。

5. 释放规则

  初始化shared_ptr智能指针时,可以自定义所指向堆内存的释放规则,这样当堆内存的引用计数为0时,会优先调用自定义的释放规则。
  某些场景下,需要自定义释放规则。例如:对于申请的动态数组,shared_ptr指针默认的释放规则不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。

方法一:指定C++11标准提供的模板类default_delete作为释放规则:

shared_ptr<int> p6(new int[10], default_delete<int[]>());

方法二:纯自定义释放规则:

void deleteInt(int* p) { delete[]p; }			//编辑自定义释放规则函数
shared_ptr<int> p7(new int[10], deleteInt);		//初始化智能指针,并自定义释放规则

方法三:借助lambda表达式初始化:

shared_ptr<int> p7(new int[10], [](int* p) {
	delete[]p;
	});

6. 常用方法

方法具体
swap()交换2个相同类型shared_ptr智能指针的内容
reset()当函数无实参时,使当前shared_ptr所指堆内存的引用计数-1,同时将当前对象重置为一个空指针;当参数传递了一个堆内存时,则调用reset()的shared_ptr对象会获得该存储空间的所有权,相当于给shared_ptr换了个所指向的对象。
get()获得shared_ptr对象内部包含的普通指针
use_count()返回同当前shared_ptr对象(包括自己)指向相同对象的所有shared_ptr数量
unique()判断当前shared_ptr对象指向的堆内存,是否不再有其它shared_ptr再指向它,也就是判断是否独占该资源的控制权
operator bool()判断shared_ptr是否不为空,非空–>True,为空–>False,常用形式: if(p1) { … (如果非空,执行)…}

二、unique_ptr智能指针

  作为智能指针的一种,unique_ptr也具备“在适当的时机自动释放堆内存空间”的能力。与shared_ptr最大的不同在于:unique_ptr指向的堆内存无法与其它unique_ptr共享,即每个unique_ptr都独自拥有对其所指内存空间的所有权。也就意味着,每个unique_ptr指向的堆内存空间的引用计数都只能为1。一旦该unique_ptr放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。
  unique_ptr<T> 定义位于<memory> 头文件、std命名空间中,其中T表示指针指向的具体数据类型。

1. 创建方法

(1) 空智能指针:

unique_ptr<int> p1;					//不传入任何参数
unique_ptr<int> p2(nullptr);		//传入空指针nullptr

空的 unique_ptr 指针,其引用计数初始为0,而不是1。

(2) 明确指向:

unique_ptr<int> p3(new int);
*p3 = 42;	//赋值

另一种写法相同:

unique_ptr<int> p3(new int(42));	//直接初始化

C++11中没有make_unique()模板函数,在C++14中引入:

unique_ptr<int> p3 = make_unique<int>();
*p3 = 42;
unique_ptr<int> p3 = make_unique<int>(42);	//与上述两行同作用

  由此创建了unique_ptr智能指针p3,其指向的是可容纳1个整数的堆内存空间。

(3) 构造函数:

  由于unique_ptr类型指针不共享各自拥有的堆内存,因此unique_ptr模板类没有拷贝构造函数,只有移动构造函数。

移动构造:

unique_ptr<int> p4(new int);
unique_ptr<int> p5(p4);		//错误,堆内存不共享,无拷贝
unique_ptr<int> p5(move(p4));	//正确,调用移动构造函数,此时p5获得p4所指堆空间的所有权,而p4变成空指针

2. 释放规则

  默认情况下,unique_ptr指针采用default_delete<T>方法释放堆内存,也可自定义释放规则,而为unique_ptr自定义释放规则,只能采用函数对象(仿函数)方式。
  一般情况下,不应该也不需要调用delete来释放这块内存,但C++11中无法自动释放申请的动态数组。(C++14中有所改变)

方法一:指定C++11标准提供的模板类default_delete作为释放规则:

unique_ptr<int> p6(new int[10], default_delete<int[]>());

方法二:以函数对象(仿函数)方式自定义释放规则:

struct myDel {
	void operator()(int* p) {
		delete p;
	}
};
unique_ptr<int, myDel> p6(new int);

3. 常用方法

方法具体
get()获得unique_ptr对象内部包含的普通指针
get_delete()获取当前unique_ptr指针释放堆内存空间所用的规则,即获取与unique_ptr实例相关的删除器对象
operator bool()判断unique_ptr是否不为空,非空–>True,为空–>False,常用形式: if(p1) { … (如果非空,执行)…}
release()释放当前unique_ptr指针对所指堆内存的所有权,但该存储空间并不会被销毁
swap()交换两个unique_ptr对象所管理的指针。调用swap()后,两个unique_ptr将分别拥有对方原本所管理对象的指针。该方法只是简单交换了两个unique_ptr对象内部的指针,并不会触发任何内存分配或释放操作
reset()用于改变或释放unique_ptr指针当前所管理的对象

  当前unique_ptr指针调用reset()时,若提供了新的指针作为参数,那么unique_ptr会开始管理这个新对象,并自动释放之前管理的对象。若没有提供参数,那么unique_ptr会释放它当前管理的对象,并将自己置为空。

	unique_ptr<int> p1(new int(5));		//p1管理一个值为5的int
	int *raw_ptr = new int(10);
	p1.reset(raw_ptr);		//p1现在管理10这个新int,并自动释放之前管理的int 5
	//此时raw_ptr仍指向10,但p1已成为这块内存的唯一拥有者,不可以再使用raw_ptr
	int* ptr_3 = new int(15);
	p1.reset(ptr_3);
	//此时p1再次reset,则原raw_ptr所指向内存即10被释放
	//此时raw_ptr不再有效,因为原所指向的10被释放
	//ptr_3仍在指向15,但不能再使用

三、weak_ptr智能指针

  weak_ptr<T> 定义位于< memory>头文件、std命名空间中,其中T表示指针指向的具体数据类型。
  weak_ptr类型指针通常不单独使用(没有实际用处),只能和shared_ptr类型指针搭配使用,甚至可以说,weak_ptrshared_ptr的一种辅助工具,借助weak_ptr可获取shared_ptr的一些状态信息,比如有多少指向相同对象的shared_ptr指针、shared_ptr指针指向的堆内存是否已经被释放等。
  weak_ptr与某一shared_ptr指向相同时,weak_ptr类型指针并不会影响所指堆内存空间的引用计数。
  weak_ptr<T> 模板类中没有重载 * 和 ->运算符,即weak_ptr类型指针只能访问所指的堆内存,而无法修改它。

1. 创建方法

(1) 空智能指针:

weak_ptr<int> wp1;					//不传入任何参数

(2) 拷贝构造,凭借已有的weak_ptr指针创建一个新的weak_ptr指针:

weak_ptr<int> wp2(wp1);	

  若wp1为空,则wp2也为空指针;若wp1指向某一shared_ptr指针所拥有的堆内存,则wp2也指向该块存储空间(可访问,但无所有权,不可修改)。

(3) 以shared_ptr对象为参数的构造函数:

  weak_ptr指针更常用于指向某一shared_ptr指针拥有的堆内存,可以利用已有的shared_ptr指针为其初始化:

shared_ptr<int> sp(new int);
weak_ptr<int> wp3 = sp;

  wp3指针和sp指针有相同的指针,但weak_ptr指针不会导致堆内存空间的引用计数加或减。

2. 重要用法

(1) 不控制对象的生命周期:

  weak_ptr的创建、销毁都不影响引用计数,可以说它仅仅只是观察对象。当最后一个shared_ptr被销毁或重置,即引用计数归0后,无论有多少weak_ptr指向该堆内存,堆内存都会被释放,释放后,weak_ptr不会被销毁或置空,仍然是一个有效的weak_ptr对象,只是它观察的对象不再存在,若此时通过weak_ptr访问对象,会得到一个空的shared_ptr,表明原对象已经不存在了。

(2) 升级为shared_ptr:

  weak_ptr可以通过调用成员函数lock()升级为shared_ptr,实际上是通过lock()方法获取一个与原始shared_ptr共享对象所有权的新shared_ptr,整个过程不会增加原有shared_ptr的引用计数。

	shared_ptr<int> sp(new int(42));	//创建一个shared_ptr并初始化,此时该内存引用计数为1
	weak_ptr<int> wp(sp);				//创建一个weak_ptr,观察sp所指向的对象,此时该内存引用计数为1
	shared_ptr<int> sp2 = wp.lock();	
	//尝试从wp中提取一个shared_ptr,成功后该内存引用计数为2
	//如果资源仍然存在(即至少还有一个shared_ptr在管理资源),lock函数将返回一个指向相同资源的新shared_ptr

(3) 解决循环引用问题:

  循环引用是指两个或多个对象通过智能指针shared_ptr相互引用,导致它们的引用计数永远不会降为0,从而无法释放它们占用的内存。使用weak_ptr可以打破这种循环引用,通常在可能导致循环引用的地方,使用weak_ptr代替shared_ptr。

错误示例

	struct A;		//先声明结构体
	struct B;
	struct A { shared_ptr<B> bptr; };	//结构体A中包含一个指向B的shared_ptr成员
	struct B { shared_ptr<A> aptr; };	//结构体B中包含一个指向A的shared_ptr成员
	shared_ptr<A> pa(new A);		//new会在堆上分配一个该类型的对象,并返回指向它的原始指针初始化shared_ptr
	shared_ptr<B> pb(new B);
	pa->bptr = pb;		//将pa所指向的A对象的bptr成员设置为pb,意味着现在A对象通过其bptr成员持有了对B对象的shared_ptr,从而增加了B对象的引用计数
	pb->aptr = pa;

  分析:由于A对象持有B对象的shared_ptr,而B对象又持有A对象的shared_ptr,构成了一个循环引用,即使pa和pb这两个shared_ptr都被销毁,A和B对象也不会自动删除,因为它们的引用计数永远不会降为0。具体来说,当pa和pb构造时,它们的引用计数都是1,后经过循环引用,二者引用计数都为2,离开作用域后,引用计数各自-1,引用计数都变为1,因此不会释放资源。
  解决:为避免这种情况,通常需要弱引用(如weak_ptr)来打破循环引用。weak_ptr是用来监视shared_ptr的,不会使引用计数增加,但它不管理shared_ptr内部的指针(普通指针),它是用来监视shared_ptr生命周期的。

正确示例

	struct A;		//先声明结构体
	struct B;
	struct A { shared_ptr<B> bptr; };	//结构体A中包含一个指向B的shared_ptr成员,A拥有B
	struct B { weak_ptr<A> aptr; };	//结构体B中包含一个指向A的weak_ptr成员,B只是观察A
	shared_ptr<A> pa(new A);		//A对象的引用计数为1(由pa管理)
	shared_ptr<B> pb(new B);		//B对象的引用计数为1(由pb管理)
	pa->bptr = pb;		//B对象由pb和pa的成员bptr共同管理,pa的成员bptr即A类的shared_ptr成员,由此两个shared_ptr共同管理B对象,其引用计数为2
	pb->aptr = pa;		//A对象由pa和pb的成员aptr共同管理,pb的成员aptr即B类的weak_ptr成员,只能观察不能拥有,由此只有pa这一个shared_ptr管理A对象,其引用计数为1

  分析:首先创建时双方的引用计数都为1,接着pb赋值给pa的bptr成员,使得pb的引用计数+1,此时=2,pa仍为1。接着pa赋值给pb的aptr成员,pb的aptr成员是weak_ptr指针,不会增加pa的引用计数。此时引用计数:pa为1、pb为2。离开作用域后,pa-1为0,执行析构销毁;pb为2-1,不销毁,但由于pa析构,A对象已经被销毁(引用计数为0),B对象的aptr成员指向的shared_ptr变得无效,因此B对象的引用计数从1减少到0,导致B对象也被销毁。

3. 常用方法

方法具体
swap()交换2个相同类型weak_ptr智能指针的内容
reset()将当前weak_ptr指针置为空指针
use_count()查看同当前weak_ptr对象指向相同的所有shared_ptr数量
expired()判断当前weak_ptr指针是否过期(指针为空,或者指向的堆内存已经被释放)
lock()若当前weak_ptr已过期,该函数会返回一个空的shared_ptr指针;反之,返回一个和当前weak_ptr指向相同的shared_ptr指针

总结

本篇博客根据网上资料 + 个人总结,若有错误请指正,大家一起讨论。

  • 12
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值