第一篇——C++11中的智能指针
前言
1. C++动态内存管理通过一对运算符来完成:
new在动态内存中为对象分配空间并返回一个指向该对象的指针,可以对对象初始化;
delete接受一个动态对象的指针,销毁该对象,并释放与之相关的内存。
2. 释放内存
智能指针可以在适当时机自动释放分配的内存,可以很好地避免“忘记释放内存而内存泄漏”。
3. C++智能指针底层是采用引用计数的方式实现:
在申请堆内存空间时,会配备一个整型值(初始值为1),每当有新对象使用此堆内存时,该值+1;反之,每当使用此堆内存的对象被释放时,该值-1。当堆空间对应的整型值为0时,即表明不会再有任何对象使用它,该堆空间被释放。
一、shared_ptr智能指针
- 实际上,每种智能指针都是以类模板的方式实现的。
- shared_ptr<T> 定义位于< memory>头文件、std命名空间中,其中T表示指针指向的具体数据类型。
- 多个 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_ptr是shared_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指针 |
总结
本篇博客根据网上资料 + 个人总结,若有错误请指正,大家一起讨论。