智能指针【C++】
RALL机制:资源获取即初始化,使用==局部对象==来管理资源的技术(这里的资源主要指操作系统中有限的:比如内存、网络套接字、互斥量、文件句柄等,局部对象是指存储在栈的对象,其生理周期有操作系统来管理)
1、使用裸指针存在的问题
- 在使用时,我们无法判断它指向的是一个对象还是一组对象。
int *tmp;//野指针 int *op=NULL;//空指针 //从堆区new出来的指针之后释放了,叫失效指针(或悬空指针)
- 无法判断一个使用完后的指针是否应该被销毁,因为无法判断指针此时还有没有“拥有”指向的对象。
- 指针销毁时,无法确定是使用delete关键字删除,还是有其他的销毁机制。
- 无法保证在代码销毁后的路径有无再次销毁的可能:任何一条路径遗漏都可能导致内存泄漏。
- 理论上也无法判断指针是否处于悬空状态。
2、C++的几种智能指针
- auto_ptr(C++17中已移除)(C11已弃用)
- unique_ptr 唯一性智能指针
- shared_ptr 共享性智能指针
- weak_ptr 弱引用的指针
C++涵盖:①C部分 + ②class类 + ③template模板
智能指针一般不去处理const类型的对象,若有特殊需要,则要修改智能指针本身。
3、auto_ptr
auto_ptr在构造时,会获取对某个对象的所有权,在析构时释放该对象。 一般这样使用:
int *p=new int(10);
auto_ptr<int> op(p);
这样,我们就不用关心指针p什么时候释放,也不用担心会有异常发生而造成内存泄漏。
但是,使用auto_ptr可能会有这样的问题:
- 若有浅拷贝,共享同一对象资源,那么析构释放就会存在问题。
int *p= new int(10);
auto_ptr<int> op1(p);
auto_ptr<int> op2(p);
这个例子,有个问题,因为op1和op2都认为需要管理指针p,那么就会造成指针的p的两次释放,这就会出现问题。 再看这样的情况:
- 不清楚指向的是一个对象还是一组对象。
string *str=new string[10];
auto_ptr<string> op(str);
这个例子中,auto_ptr的析构函数删除指针使用的是delete,而不是delete [],所以,auto_ptr也无法去管理一个数组指针。
![在这里插入图片描述](https://img-blog.csdnimg.cn/1bccdb0bcf1c4e7db6f2c7c072c583c9.png)
- 构造函数的explicit关键字有效阻止从一个“裸”指针隐式转换成auto_ptr类型。
- 拷贝与赋值
一个“裸”指针不能同时被两个以上的auto_ptr所拥有,因此,在拷贝或赋值时,就要考虑到这个问题。auto_ptr的做法就是“所有权转移”:
int *p = new int(10);
auto_ptr<int> op1(p);
auto_ptr<int> op2=op1;
cout<<*op1<<endl;
同样的,auto_ptr也不可以作为函数参数按值传递:
void func(auto<int> op)
{
cout<<*op<<endl;
}
int main()
{
auto_ptr<int> op1(new int(10));
func(op1);
cout<<*op1<<endl;
}
因为在函数调用过程中会产生一个局部对象来接收传入的auto_ptr(拷贝构造),这个时候真正的实参就失去了对其拥有对象的所有权,但这个局部auto_ptr又会在函数结束时自动析构,将对象删除。故,不可将auto_ptr作为参数按值传递。
再比如,我们有这样的两个类:
class Object{};
class Base:public Object{};
auto_ptr<Object> op1=auto_ptr<Base>(new Base);
- 若拷贝构造为转移对象资源,但遇到调用函数传参拷贝时有问题了,主函数实参失去了对对象资源的拥有权(因为函数调用结束后会被析构)。
C++11之前没有移动赋值的概念,所以还使用,但是现在使用就会有问题。
4、unique_ptr唯一性智能指针
它是一种定义在中的智能指针, 特性:
- 不允许用一个对象初始化另一个对象(delete了拷贝构造)、不允许赋值(delete了赋值方法)
- 它“独占”地拥有它指向的对象,两个unique_ptr不能指向同一个对象
- 要是有需要用一个对象初始化另一个对象,可以明确使用std::move() //移动构造
- unique_ptr对象中保存指向某个对象的指针,当他本身被删除或者离开其作用域时会自动释放其指向对象所占用的资源
不具名对象==右值对象
unique_ptr的使用:
unique_ptr<int> up(new int(10));
cout<<*up<<endl;
unique_ptr<int> up2=up1;
unique_ptr<int> up3(up1);
unique_ptr<int> up4=std::move(up1);
unique_ptr<int> up5(std::move(up4);
unique_ptr不允许拷贝,但是有个例外:可以从一个函数中返回一个unique_ptr
unique_ptr<int> clone(int a)
{
unique_ptr<int> res(new int(a));
return res;
}
int main()
{
int a=5;
unique_ptr<int> pa=clone(a);
unique_ptr<int> pb;
pb=clone(a);
cout<<*pa<<endl;
cout<<*pb<<endl;
return 0;
}
5、shared_ptr共享智能指针
shared_ptr是一个引用计数智能指针,用于共享对象的所有权也就是说它允许多个指针指向同一个对象。 它使用一个引用计数指针来计数。(注:每个共享指针若拷贝,则共用同一个引用计数指针)当shared_ptr对象的计数器变为0,他就会自动释放自己所管理的对象。 C11之后,类型有这么几个默认函数(8个):6+2(构造函数、析构函数、拷贝构造、赋值、取址、取常量地址、移动拷贝构造、移动赋值)
- 检查引用计数:unique() 和 use_count(),use_count()函数可能效率很低,应该只把他用于测试或调试,unique()函数用来测试该shared_ptr是否是原始指针的唯一拥有者(也就是use_count()==1时,返回true,否则返回false)。
shared_ptr对象的内存分布:
![在这里插入图片描述](https://img-blog.csdnimg.cn/0bb857a1b51e4bd1ba42c66d37bf7ebb.png)
在使用shared_ptr对象时,又会有这样的一个问题:
class Child;
class Parent
{
public:
shared_ptr<Child> child;
~Parent() { cout << "~Parent()" << endl; }
void hi()const { cout << "hello" << endl; }
};
class Child
{
public:
shared_ptr<Parent> parent;
~Child() { cout << "~Child()" << endl; }
};
int main()
{
shared_ptr<Parent> p = make_shared<Parent>();
shared_ptr<Child> c = make_shared<Child>();
p->child = c;
c->parent = p;
c->parent->hi();
return 0;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/050c8052816a422ea09b1ba2064d9de9.png) 可以看到,程序运行结束只打印了hello,但并没有打印出~Parent() 和 ~Child(),说明这两个类的析构函数并没有被调用。 ![在这里插入图片描述](https://img-blog.csdnimg.cn/b8302ee176794dc092b9e50d2b044141.png) 当程序退出时,即使parent和child被销毁,也仅仅是引用计数变为1,因此并未销毁p和c对象。
此时,我们引入weak_ptr弱引用指针:
6、weak_ptr弱引用智能指针
它是为了配合shared_ptr使用而引入的一种智能指针,他指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是将一个weak_ptr绑定到一个shared_ptr,不会改变shared_ptr的引用计数。 既然weak_ptr并不改变其所共享的shared_ptr实例的引用计数,则可能存在weak_ptr指向的对象释放的这种情况。此时,用lock()函数来判断weak_ptr指向的对象是否存在。
class Child;
class Parent
{
public:
weak_ptr<Child> child;
~Parent() { cout << "~Parent()" << endl; }
void hi()const { cout << "hello" << endl; }
};
class Child
{
public:
weak_ptr<Parent> parent;
~Child() { cout << "~Child()" << endl; }
};
int main()
{
shared_ptr<Parent> p = make_shared<Parent>();
shared_ptr<Child> c = make_shared<Child>();
p->child = c;
c->parent = p;
c->parent.lock()->hi();
return 0;
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/2bc9d0505587416e8362fe99620bbacf.png) ![在这里插入图片描述](https://img-blog.csdnimg.cn/e3889b6a7c9a4dcd8580b7e85a23eadc.png)
7、智能指针在管理非heap资源时如何释放
利用删除器。
例如:给shared_ptr添加自定义删除器:
#include<iostream>
using namespace std;
class Object
{
int value;
public:
Object(int x = 0) :value(x) { cout << "Object::create" << endl; }
~Object() { cout << "Object::~Object()" << endl; }
int Value()
{
return value;
}
int Value()const
{
return value;
}
};
void deleter(Object* p)
{
cout<<"function called Deleter"<<endl;
delete []p;
}
struct Deleter
{
void operator()(Object* p)
{
cout<<"function Object Deleter"<<endl;
delete []p;
}
};
int main()
{
shared_ptr<Object> sp1(new Object[10],deleter);
shared_ptr<Object> sp2(new Object[10],Deleter());
shared_ptr<Object> sp3(new Object[10],[](Object *p){cout<<"lambda Deleter"<<endl; delet []p;});
shared_ptr<Object> sp4(new Object[10],default_delete<Object[]>());
}
![在这里插入图片描述](https://img-blog.csdnimg.cn/3a3a3c999da64c80a90f26ec6f700d35.png)
8、shared_ptr对象创建方法
一般有两种方法去初始化一个std::shared_ptr:
- 通过他自己的构造函数
- 通过std::make_shared
这两种方法的不同在于: 我们了解了shared_ptr和weak_ptr,shared_ptr是非入侵式的,即计数器的值并不存储在shared_ptr内,他其实是存储在其他地方——在堆上的。当一个shared_ptr由一块内存的原生指针创建的时候(原生内存:代指这个时候还没有其他shared_ptr指向这块内存),这个计数器也就随之产生。这个计数器结构会一直存在——知道所有的shared_ptr和weak_ptr都被销毁的时候。当所有的shared_ptr都被销毁时,这块内存就已经被释放了,但是可能还有weak_ptr存在——也就是说,计数器的销毁有可能发生在内存对象销毁很久之后才发生。这个结构大概就是下面这样: ![在这里插入图片描述](https://img-blog.csdnimg.cn/00fb1de49bf843a7883e18a974eb371a.png) 当我们使用创建一个shared_ptr管理一块原生内存时,在堆上实际发生了两次内存分配:
- Object ptr=new Object(10);
- std::shared_ptr pObj(ptr);// 分配内存给shared_ptr的计数器
当我们使用一个原生指针、一个unique_ptr或者通过一个空白shared_ptr来设置指向一块原生内存时,发生的内存分配和上面的类似。我们知道内存分配和回收基本上是C++中最慢的单次操作了,因此,我们再提出一种办法将这两者合二为一:std::make_shared。
std::make_shared可以同时为计数器和原生内存分配内存空间,并把二者的内存视作整体管理,结果就像下面这样: ![在这里插入图片描述](https://img-blog.csdnimg.cn/78e3c2f18fde4f319bd02cca2de45090.png) make_shared的好处:
- 可以减少单词内存分配的次数
- 可以增加Cache局部性:使用make_shared,计数器的内存和原生内存就在堆上挨着。这样,可以提高Cache的命中率。
- 确保执行顺序及异常安全性问题:
在下面这种情况中:
struct Object
{
int i;
};
void doSomething(std::shared_ptr<Object> pt,double d);
double couldThrowException();
int main()
{
doSomething(std::shared_ptr<Object>(new Object{1024}),couldThrowException());
return 0;
}
我们知道,在doSomething函数调用之前,至少有3件事要被完成:构造并给Object分配内存,构造shared_ptr以及调用couldThrowException()。并且在C++17之前,他们的执行顺序应该是这样的:
- [1 ] new Object();
- [2] 调用couldThrowException()
- [3] 构造shared_ptr并管理第1步中开辟的内存
但是,一旦第2步抛出了异常,第3步就永远不会执行,可是,第1步已经开辟的内存就出现了内存泄漏,因为此时不会有智能指针去管理它。 所以,我们要使用make_shared来使得第1步和第3步紧紧连在一起。
doSomething(std::make_shared<Object>(10),couldThrowException());
make_shared的坏处:
- make_shared函数必须能够调用目标类型构造函数或构造方法。然而这个时候即使把make_shared设成类的友元恐怕都不够用,因为其实目标类型的构造是通过一个辅助函数调用的——不是make_shared这个函数。
- 另一问题就是我们目标内存的生存周期问题(不是目标对象的生存周期)。就像上面说的,即使被make_shared管理的目标对象都被释放了,shared_ptr的计数器还会一直持续存在,直到最后一个指向目标内存的weak_ptr被销毁。这个时候,如果我们使用make_shared函数,问题就是:程序自动地把被管理对象占用的内存和计数器占用的堆上内存视作一个整体来管理,也就是说,即使被管理的对象被析构了,空间还在,内存可能并没有归还——他在等着所有的weak_ptr都被清除后和计数器所占用的内存一起被归还。比如你的对象有点大,那就意味着一个相当可观的内存被无意义地锁了一段时间。
![在这里插入图片描述](https://img-blog.csdnimg.cn/528023d9d8aa486781e9aabea9dbfa31.png)
阴影区域就是被shared_ptr管理对象的内存,他在等着weak_ptr的计数器变为0,和上面的计数器内存一起被释放。
|