智能指针
智能指针概念和意义
内存泄漏( 臭名昭著的Bug )
- 动态申请堆空间 , 用完后不归还
- C++语言中没有垃圾回收的机制
- 指针无法控制所指堆空间的生命周期
我们需要什么?
- 需要一个特殊的指针
- 指针生命周期结束时主动释放堆空间
- —片堆空间最多只能由一个指针标识
- 杜绝指针运算和指针比较
解决方案
- 重载指针特征操作符( **-> 和 *** )
- 只能通过类的成员函数重载
- 重载函数不能使用参数
- 只能定义一个重载函数
运算符 ->
的重载比较特别,它只能是非静态的成员函数形式,而且没有参数。如果返回值是一个原始指针,那么就将运算符的右操作数当作这个原始指针所指向类型的成员进行访问;如果返回值是另一个类型的实例,那么就继续调用这个返回类型的 operator->()
,直到有一个调用返回一个原始指针为止,然后按第一种情况处理。
如果上述条件不满足(如:右操作数不是返回的原始指针指向的类型的成员,或者,返回的非指针类型没有重载 operator->()
),那么编译将报错。
1 |
|
智能指针的使用军规:只能用来指向堆空间中的对象或者变量
智能指针模板
Pointer抽象类
1 | template < typename T > |
SmartPointer智能指针
1 | template <typename T> |
shared_pointer智能指针
通过计数机制ref标志堆空间
- 堆空间被指向时:ref++
- 指针被置空时:ref–
- ref==0:释放堆空间内存
1 | template < typename T > |
c++标准库有四种智能指针: std:: auto_ ptr(c++98): unique_ ptr (c++11): shared_ ptr(c++11) ; weak ptr (c++11):
帮助我们进行动态分配对象( new出来的对象)的生命周期的管理。能够有效防止内存泄漏;
目前auto_ ptr已经完全被unique_ ptr, 所以大家不要再使用auto_ ptr; c++11标准中反对使用auto_ ptr (弃用) ;
这三种智能指针都是类模板,我们可以将new获得地址赋给他们;
a) shared_ ptr: 共享式指针。多个指针指向同一个对象,最后一个指针被销毁时,这个对象会被释放。weak ptr是辅助shared_ ptr工作的;
b) unique_ ptr: 独占式指针;同一个时间内,只有一个指针能够指向该对象。当然,该对象的所有权还是可以移交出去的;你忘记delete的时候,智能指针帮助你delete,或者说,你压根就不再需要自己delete;智能指针的本份(帮助你delete) ;
shared_ptr
共享所有权,不是被一个shared_ptr拥有,而是被多个shared_ptr之间相互协作; shared_ptr有额外开销;
工作原理:
引用计数,每个shared_ptr的拷贝都指向相同的内存。所以,只有最后一个指向该内存(对象)的shared_ptr不需要再指向该对象的时候,那么这个shared_ptr才会析构所指向的对象。
最后一个指向该内存对象的shared_ptr在什么情况下会释放该对象( shared_ptr所指向的对象)呢?
a)这个shared_ptr被析构的时候;
b)这个shared_ptr指向其他的对象时;
垃圾回收机制;我们从此不用担心对象何时被delete;
类模板,用到<>, <>里,就是指针可以指向的类型,后边再跟智能指针名;
格式: shared_ptr<指向的类型>智能指针名:
1 | shared_ptr<int> pi(new int(100));//pi指向一个值为100的int型数据 |
裸指针也可以初始化shared_ptr,但是不推荐。智能指针和裸指针不要穿插使用。
make_shared函数
标准库里的函数模板.安全,高效的分配和使用shared_ptr ;
它能够在动态内存(堆)中分配并初始化一个对象,然后返回指向此对象的shared_ ptr;
1 | shared_ptr<int> p2 = make_shared<int>(100); |
shared_ptr引用计数的增加和减少
如果参数为引用,则智能指针的引用参数不会增加
shard_ ptr引用计数增加:
- 例如对于p1指针,我们用p1来初始化p这个智能指针;
auto p(p1);
- 把智能指针当做实参往函数里传递;
myfunc(p1);
但是函数结束后,引用计数又将减少1. - 作为函数的返回值
p2 = func();
shard_ ptr引用计数减少:
1.给shared_ptr赋子新值,让该shared_ptr指向一个新对象;
1 | auto p = make_shared<int>(100);//引用计数为1 |
2.局部的shared_ptr离开其作用域;比如函数传参
3.当一个shared_ptr引用计数从1变成0,则它会自动释放自己所管理(指向)的对象;
shared_ptr指针的常用操作
use_count()
返回多少个智能指针指向某个对象,主要用于调试目的
unique()
是否该智能指针独占某个指向的对象,也就是若只有一个智能指针指向某个对象,则返回true,否则返回false。
reset()
reset()不带参数时
若pi是唯一指向该对象的指针,那么释放pi所指向的对象,并将pi置空。
若pi不是唯一指向该对象 的指针,那么不释放pi所指向的对象, 但指向该对象的引用计数会减少1,同时将pi置空
reset()带参数时
若pi是唯一指向该对象的指针,则释放pi指向的对象,让pi指向新对象。
若pi不是唯一指向该对象的指针,则不释放p指向的对象,但指向该对象的引用计数会减少1,同时让pi指向新对象;
解引用*
获得p指向的对象。类似于C语言的用法,比较简单。
get()
p.get():返回p中保存的指针(裸指针),小心使用,如果智能指针释放了所指向的对象,那么这个返回的裸指针也就变的无效了。
1 | shared_ptr<int> myp(new int(100)); |
swap()
交换两个智能指针所指向的对象
1 | shared_ ptr<string> ps1(new string( "I Love China1!" )); |
= nullptr
- 将所指向的对象引用计数减1,若引用计数变为0,则释放智能指针所指向的对象。
- 将智能指针置空
智能指针本身作为判断条件
1 | shared. ptr<string> ps1(new string(" I Love China! ")): |
指定删除器以及数组问题
a)指定删除器;
一定时机帮我们删除所指向的对象; delete :将delete运算符号作为默认的资源析构方式。
我们可以指定自己的删除器取代系统提供的默认删除器,当智能指针需要删除所指向的对象时,编译器就会调用我们自己的删除器
shared_ptr指定删除器方法比较简单,一般只需要在参数中添加具体的删除器函数名即可;
删除器函数也可以时lambda表达式。
1 | void myDelete(int* p) |
有些情况默认删除器处理不了,需要我们提供自己指定的删除器。比如用shared_ptr处理动态数组的时候
1 | shared_ptr<int>p(new int[10],[](int*p){ |
还可以用default_delete(标准库里的模板类)
1 | shared_ptr<A> pA(new A[10],default_delete<A[]>()); |
进行封装一下
1 | template <typename T> |
share_ptr使用技巧
作为局部变量返回不出错
1 | shared_ptr<int> myfunc(int value) |
使用陷阱
1.不允许隐式类型转换,int *p不能转换成shared_ptr<int>
,但是可以临时对象显示构造。
极度不推荐裸指针和智能指针混合使用。
1 | void proc(shared_ptr<int> ptr) |
2.绝对不可以裸指针初始化多个shared_ptr。
1 | int* p = new int(100); |
这会导致p1和p2没有关联,p1和p2的强引用计数都为1,会导致p1和p2指向内存被释放两次。
3.慎用get()返回的裸指针
返回智能指针指向的对象所对应的裸指针(有些函数接口可能只能使用裸指针)
get返回的指针不能delete,否则会异常。
也不能将一个智能指针绑定到get返回的指针上。
这也会造成两次delete,等价于情况2。
1 | shared_ptr<int>myp(new int(100)); |
4.不要把类对象指针(this)作为shared_ptr返回,改用enable_shared_from_this
因为this是一个裸指针,这样会生成一个新的强引用。用裸指针初始化了多个shared_ptr的感觉。
1 | class CT : enable_shared_from_this<CT> |
现在,在外面创建CT对象的智能指针以及通过CT对象返回的this智能指针都是安全的;
这个enable_shared_from_this中有一个弱指针weak_ptr,这个弱指针能够监视this,在我们调用shared_ from this(这个方法时,这个方法内部实际上是调用了这个weak_ ptr的lock(方法;大家都知道lock()方法会让shared_ptr指针计数+1,同时返回这个shared_ptr, 这个就是工作原理;
5.不要循环引用,会造成内存泄漏
1 | class CB; |
解决方式,一个使用强引用,一个使用弱引用。
move()移动赋值
1 | shared_ptr<int> p1(new int(100)); |
移动肯定比复制块;复制你要增加引用计数,移动不需要;移动构造函数快过复制构造函数,移动赋值运算符快过拷贝赋值运算符;
weak_ptr()
强指的就是shared_ ptr, 弱指的就是weak_ ptr;
weak ptr :
也是个类模板,也是个智能指针。这个智能指针指向一个由shared_ptr管理的对象。但是weak_ptr这种指针不控制对象生命周期。
换句话来说,将weak_ptr绑定到shared_ptr上并不会改变shared_ptr的引用计数( 更确切的说,weak_ptr的构造和析构不会增加或者减少所指向对象的引用计数。
当shared_ptr需要释放所指定对象的时候照常释放,不管是否有weak_ptr指向该对象。
能力弱(弱共享/弱引用:共享其他的shared_ptr所指向的对象),控制不了所指向对象的生存期;
这个弱引用(weak_ptr)的作用: 大家可以理解成监视shared_ptr (强引用)的生命周期用的。是一种对shared_ptr的扩充。weak ptr不是一种独立的智能指针, 不能用来操作所指向的资源,所以它看起来象一个 shared_ptr的助手(旁观者)
weak_ptr的创建
一般是用shared_ptr来初始化
1 | auto p1 = make_ shared<int>(100);// shared_ptr |
强引用计数才能决定对象的生存期;弱引用计数并对对象生存期没有影响。
lock()
功能就是检查weak_ptr 所指向的对象是否存在,如果存在,那么这个lock他就能巩固返回一个指向该对象的shared_ptr(指向对象的强引用计数就会+1)
如果他所指向的对象不存在,lock返回一个空的shared_ptr—nullptr
。
1 | auto p1 = make_ shared<int>(100);// shared_ptr |
weak_ptr常用操作
use_count()
获得与该弱指针共享对家的其他shared_ptr的数量,或者说获得当前所观测资源的强引用计数
expired()
是否过期,若该指针的use_count()为0,表示该弱指针所指向的对象已经不存在了,则返回true。
换句话说,这个函数用来判断所观测的资源是否已经被释放了
reset()
将该弱引用指针设置为空,不影响指向该对象的强引用数量,但指向该对象的弱引用数量会减少1
lock()
1 | auto p1 = make_shared<int>(42); |
尺寸问题
weak_ptr的尺寸和shared_ptr尺寸一样大,是裸指针的2倍;
第一个裸指针指向的是这个智能指针所指向的对象;
第二个裸指针指向一个很大的数据结构(控制块) ,这个控制块里有:
1:所指对象的强引用计数
2:所指对象的弱引用计数
3:其他数据,比如自定义的删除器的指针等等;
这个控制块是由第一个指向某个对象的shared_ptr来创建的;
控制块创建时间:
- make_shared:分配开初始化一个对家,返回指向此对象的sharedpur,所以,这个ke shared它总是能够创建一个控制块
- 用裸指针来创建一 个shared_ptr对象时
unique_ptr
独占式的概念(专属所有权):同一个时刻,只能有一个unique_ptr指针指向这个对象(这块内存) ;当这个unique_ptr被销毁的时候,它所指向的对象也被销毁;
格式:unique_ptr<类型> p
初始化
常规初始化:
unique_pte<int>pi;
这样默认初始化时一个空智能指针,(nullptr)unique_pte<int>pi(new int(105));
此时pi指向值为105的int对象
make_unique()函数初始化:
1 | unique_ptr<int> p1 = make_unique<int>(100); |
unique_ptr常用操作
不支持的操作
- 1.拷贝构造不支持
1 | unique_ptr<string> ps1(new string("feng"));//ok |
- 2.赋值操作不允许
1 | unique_ptr<string> ps1(new string("feng"));//ok |
移动赋值(正确的赋值方法)
1 | unique_ptr<string> ps1(new string("feng"));//ok |
release()放弃对指针控制权
切断了智能指针和其所指向的对象之间的联系,返回裸指针并且将智能指针置为空
返回的这个裸指针我们可以手动 delete来释放,也可以用来初始化另外一个智能指针,或者给另外一个智能指针赋值;
1 | unique_ptr<string> ps1(new string("feng"));//ok |
relaese返回的裸指针可能会造成内存泄漏
reset()
reset()不带参数情况:释放智能指针所指向的对象,并将智能指针置空。
reset()带参数的情况:释放智能指针所指向的对象,并让该智能指针指向新对象
1 | unique_ptr<string>ps2(new string(" feng")); |
=nullptr
释放智能指针所指向的对象,并将智能指针置空
1 | unique_ptr<string>ps1(new string(" feng")); |
指向一个数组
数组这里要跟上[]
1 | unqiue_ptr<int[]>ptarray(new int[10]); |
get()
返回智能指针的裸指针。考虑到一些函数的参数需要的是内置裸指针。
如果智能指针释放了对象,那么后续再操纵这个裸指针十分的危险。如果裸指针用了delete释放对象,那么智能指针也很危险
1 | unique_ptr<string>ps1(new string"feng"); |
解引用*
获取该智能指针指向的对象,可以直接操作。但是数组的智能指针不能用*解引用。
swap
交换两个指针指向的对象。
1 | unique_ptr<string>ps1(new string"feng"); |
智能指针名字作为判断条件
1 | unique_ptr<string>ps1(new string"feng"); |
转换为shared_ptr
如果unique_ptr为右值,就可以将它赋值给shared_ptr
因为shared_ptr包含一个显式构造函数,可用于将右值unqiue_ptr转换为shared_ptr, shared_ptr将接管的是原先unique_ptr指向的对象
返回unique_ptr
unique_ptr不支持拷贝,但是有一种特殊情况,函数返回unique_ptr是可以来接收的。
返回这种局部对象,系统会给我们生成一个临时unique_ptr对象,调用unique_ptr的移动构造函数。
1 | unique_ptr<string>func() |
指定删除器
默认是使用delete这个默认删除器释放对象。
指定删除器
格式:unique_ptr<指向的对象类型,删除器>智能指针变量名
删除对象,可调用对象,比如函数,类重载了()。
shared_ptr比较简单,shared_ptr<int>p(new int(123456),myDelete);
//提供自己的删除器
unique_ptr删除器相对复杂一点,多了一步,先要在类型模板参数中传递进去类型名,然后在参数中再给具体的删除其函数名:
1 | void mydeleter(string* pdel){ |
注意事项
shared_ptr:就算两个shared_ptr指定的删除器不相同,只要他们所指向的对象相同,那么这两个shared_ptr也属于同一个类型。
unique_ptr:删除器不相同的话是会影响unique_ptr的类型,从灵活性来讲,shared_ptr更加的灵活。
咱们在讲解shared_ ptr的时候,删除器不同,但指向类型一样的shared. ptr, 可以放到同一个容器里,vector<shared_ptr..>。
unique ptr如果, 删除器不同,那么久等于整个unique ptr类型不同。这种类型不同的unique ptr智能指针是没有办法放到同一个容器。
尺寸问题
unique_ptr尺寸和裸指针一样。
但是如果增加了自己的删除器,unique_ptr的尺寸可能增加,也可能不增加
- lambda表达式这种删除器尺寸无变化
- 函数指针这种删除器尺寸变为原先的2倍
auto_ptr的弃用
C++98时代的指针,具有unique_ptr的一部分特性。
不能在容器中保存,也不能从函数中返回auto_ptr。
auto_ptr的使用缺陷:
1 | shared_ptr<string> ps1(new string("feng")); |
虽然auto_ ptr和unique_ ptr都是独占式的,但unique_ ptr这种情况,编译的时候就会报错; 而不会默默的把ps的所有权转移到ps2上,避免后续如果使用ps导致程序崩溃的问题;
当然如果你用移动语义(move),也能达到auto_ ptr的效果: