动态内存与智能指针shared_ptr

       动态内存的管理是通过new delete运算符来完成的,使用不当会造成内存泄漏或者二次释放指针造成引用非法指针,为了更容易地使用动态内存,新的标准库提供了俩种智能指针类型来管理动态对象,智能指针与常规指针的重要区别是它负责自动释放所指向的对象,新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象,unique_ptr则“独占”所指向的对象,标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象,这三种类型都定义在#include<memory>,类似vector智能指针也是模版,因此,创建一个智能指针时,必须提供额外的信息--指针可以指向的类型。

shared_ptr<string> p1;  //shared_ptr 可以指向string
shared_ptr<list<int>> p2;  //可以指向int的list

默认初始化的智能指针中保存着一个空指针智能指针的使用方式与普通指针类似,解引用一个智能指针返回它所指向的对象,如果在一个条件判断中使用智能指针,效果就是检测它是否为空。

//如果p1不为空,检查它是否指向一个空string
if (p1&&p1->empty())
    *p = "hi";

shared_ptr和unique_ptr的相关操作:(特别注意的是部分接口是.出来的而不是->出来的)

都支持的操作
p.get();          //返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了
swap(p, q); p.swap(q);    //交换p和q中的指针

shared_ptr独有的操作
make_shared<T>(args)     //返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象
shared_ptr<T> p(q)            //p是shared_ptr q的拷贝,此操作会递增q中的计数器,q中的指针必须能转换为T*
p = q  //p和q都是shared_ptr,所保存的指针必须能相互转换,此操作会递减p的引用计数,递增q的引用计数,若p的引用计数变为0,则将其管理的原内存释放
p.use_count()  //返回与p共享对象的智能指针数量,可能很慢,主要用于调试
p.unique() //若p,use_count()为1 则返回true,否则返回false

make_shared函数:

最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象,并初始化它,返回指向此对象的shared_ptr,make_shared也定义在头文件memory中

//指向一个值为42的int的shared_ptr
shared_ptr<int>p3 = make_shared<int>(42);
//p4指向一个值为"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
//p5指向一个初始化的int,值为0
shared_ptr<int> p5 = make_shared<int>();

类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象,make_shared函数的参数必须匹配调用相应类型的构造函数,例如调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配。

shared_ptr的拷贝和赋值:

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象,我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增,例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数,以及作为函数的返回值时,它所关联的计数器就会递增,当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开作用域),计数器就会递减,一旦一个shared_ptr的计数器变为0,它就会释放自己所管理的对象。

auto p = make_shared<int>(41);
auto r = make_shared<int>(42);
cout << "p:" << p.use_count() <<"  "<< "r:" << r.use_count() << endl;  //p:1 r:1
r = p;
cout << "p:" << p.use_count() <<"  "<< "r:" << r.use_count() << endl;  //p:2  r:2
cout << *r << endl; //41

r=p后给r赋值,令r指向p所指对象的地址,这时r指向p所指向的对象,并递增p指向对象的引用计数,递减r原来指向的对象的引用计数,r原来指向的对象已没有引用者,则会自动释放。而r指向p之后,r和p的引用计数都增加为2,俩者都指向41所以打印的结果为41。

shared_ptr自动销毁所管理的对象是通过析构函数来释放对象所分配的资源:

class A
{
public:
	A() = default;
	A(int _i) :i(_i) {}
	~A()
	{
		cout << "~A()" << endl;
	}
	int i;
};

int main()
{
	shared_ptr<A> a1 = make_shared<A>(10);
	shared_ptr<A> a2 = make_shared<A>(100);
	cout << (*a1).i << endl; //10
	a1 = a2;  //值为10的对象的引用计数为0,调用A的析构器进行此对象的析构
	cout << (*a1).i << endl; //100
}
//生命周期结束,调用A的析构器对值为100的对象进行析构

shared_ptr会自动释放相关联的内存:

shared_ptr保存在局部变量中

void use_factory(T arg)
{
	shared_ptr<Foo> p = factory(arg);
	//使用p
}//p离开了作用域,它指向的内存会自动被释放掉

void use_factory(T arg)
{
	shared_ptr<Foo> p = factory(arg);
	//使用p
	return p; //当返回p时,引用计数进行了递增
}//p离开了作用域,但它指向的内存不会被释放掉

对于一块内存,shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放掉

shared_ptr在无用之后仍然保留的一种可能情况是:你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素,在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素也就是说如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。

程序使用动态内存出于以下三种原因:

1.程序不知道自己需要使用多少对象(容器类是出于这种原因而使用动态内存的典型例子)
2.程序不知道所需对象的准确类型
3.程序需要在多个对象间共享数据

通过动态内存让多个对象能共享相同的底层数据:

到目前为止,我们使用过的类中,分配的资源都与对应对象生存期一致,例如每个vector拥有其自己的元素,当我们拷贝一个vector时,原vector和副本vector类中的元素是相互分离的

vector<string> v1; //空vector
{//新作用域
	vector<string> v2 = { "a","an","the" };
	v1 = v2;  //从v2拷贝元素到v1中
}//v2被销毁,其中元素也被销毁
//v1有三个元素,是原来v2中元素的拷贝

由一个vector分配的元素只有当这个vector存在时才存在,当一个vector被销毁时,这个vector中的元素也都被销毁,但某些类分配的资源具有与原对象相独立的生存期,假定我们定义一个名为Blob的类,保存一组元素,与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素,即当我们拷贝一个Blob时,原Blob对象及拷贝应该引用相同的底层元素。一般而言,如果俩个对象共享底层的数据,当某个对象被销毁时,我们不能单方面地销毁底层数据

Blob<string> b1; //空Blob
{//新作用域
	Blob<string> b2 = { "a","an","the" };
	b1 = b2;  //b1和b2共享相同的元素
}//b2被销毁了,但b2中的元素不能销毁
//b1指向最初由b2创建的元素

定义StrBlob类:

为了实现我们所希望的数据共享,我们为每个StrBlob设置一个shared_ptr来管理动态分配的vector。此shared_ptr的成员将记录有多少个StrBlob共享相同的vector,并在vector的最后一个使用者被销毁时释放vector。

class StrBlob
{
public:
	typedef vector<string>::size_type size_type;
	StrBlob() :data(make_shared<vector<string>>()) {};
	StrBlob(initializer_list<string> il) :data(make_shared<vector<string>>(il)) {};
	size_type size() const { return data->size(); }
	bool empty() const { return data ->empty(); }
	//添加和删除元素  这俩个函数不能加const修饰,因为是写操作,不能加const
	void push_back(const string&t) { data->push_back(t); }
	void pop_back();
	//元素访问
	string& front();
	string& back();
private:
	shared_ptr<vector<string>> data;
	//如果data[i]不合法,抛出一个异常
	void check(size_type i, const string& msg) const;
};

我们的StrBlob类只有一个数据成员,它是shared_ptr类型,因此当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁,拷贝一个shared_ptr会递增其引用计数,将一个shared_ptr赋予另一个shared_ptr会递增赋值号右侧shared_ptr的引用计数,而递减左侧shared_ptr的引用计数,如果一个shared_ptr的引用计数变为0,它所指向的对象会被自动销毁,因此,对于StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。

:代码结尾b1 b2各包含多少元素
 

	StrBlob b1;
	cout << "b1引用计数:" << b1.data.use_count() << endl;
	{
		StrBlob b2 = { "a","an","the" };
		cout << "b1引用计数:" << b1.data.use_count() << endl;
		cout << "b2引用计数:" << b2.data.use_count() << endl;
		b1 = b2;
		cout << "--------------------" << endl;
		cout << "b1引用计数:" << b1.data.use_count() << endl;
		cout << "b2引用计数:" << b2.data.use_count() << endl;
		b2.push_back("about");
		cout << "--------------------" << endl;
		cout << "b1引用计数:" << b1.data.use_count() << endl;
		cout << "b2引用计数:" << b2.data.use_count() << endl;
	}
	cout << "--------------------" << endl;
	cout << "b1引用计数:" << b1.data.use_count() << endl;

b2含有4个元素但它已经被销毁,b1包含4个元素,在b1=b2后,b1所指的对象就变成了b2所指的对象(包含a an the),在push_back之后,b1所指的对象就又增加一个元素(因为此类中定义的是动态共享指针,所以最后push_back的结果会保存,也就是共享了底层的数据),我们的StrBlob对象只有一个数据成员,它是shared_ptr类型,因此当我们拷贝、赋值或销毁一个StrBlob对象时,它的shared_ptr成员会被拷贝、赋值或销毁,因此,对于StrBlob构造函数分配的vector,当最后一个指向它的StrBlob对象被销毁时,它会随之被自动销毁。

动态分配对象的值初始化和默认初始化:

string *ps1 = new string;   //默认初始化为空string
string *ps = new string();  //值初始化为空string
int *pi1 = new int;			//默认初始化 *pi1的值未定义
int *pi2 = new int();		//值初始化为0 *pi2为0

对于定义了自己的构造函数的类类型来说,要求值初始化是没有意义的,不管采用了什么形式,对象都会通过默认构造函数来初始化,但对于内置类型,俩种形式的差别就很大了,值初始化的内置类型对象有着良好定义的值,而默认初始化对象的值则是未定义的。

shared_ptr和new结合使用:

使用new返回的指针来初始化智能指针
shared_ptr<int> p(new int(42));   //p2指向一个值为42的int

接受指针参数的智能指针构造函数是explicit的,因此我们不能将一个内置指针隐式转换为一个智能指针必须使用直接初始化形式来初始化一个智能指针
shared_ptr<int> p1 = new int(1024);  //错误 必须使用直接初始化形式
shared_ptr<int> p2(new int(1024));   //正确 使用了直接初始化形式
p1的初始化隐式的要求编译器用一个new返回的int*来创建一个shared_ptr,由于不能进行内置指针到智能指针间的隐式转换,因此p2这条初始化语句时错误的,处于相同原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针。

shared_ptr<int> clone(int p)
{
    return new int(p); //错误 隐式转换为shared_ptr<int>
}
我们必须将shared_ptr显式绑定到一个想要返回的指针上
shared_ptr<int> clone(int p)
{
    return shared_ptr<int>(new int(p));  //正确 显式用int*创建shared_ptr<int>
}

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存因为智能指针默认使用delete来释放它所关联的对象,我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是这样做,必须提供自己的操作来替代delete。

定义和改变shared_ptr的其他方法:

shared_ptr<T> p(q)        //p管理内置指针q指向的对象,q必须指向new分配的内存,且能够转换为T*类型
shared_ptr<T> p(u)        //p从unique_ptr u 那里接管了对象的所有权,将u置为空
shared_ptr<T> p(q, d)    //p接管了内置指针q所指向的对象的所有权,q必须能转换为T*类型,p将使用可调用对象d来代替delete
shared_ptr<T> p(p2, d)  //p是shared_ptr p2的拷贝,唯一区别是p将用可调用对象d来代替delete
p.reset()                          //若p是唯一指向其对象的shared_ptr,reset会释放此对象
p.reset(q)                        //若传递了可选的参数内置指针q,会令p指向q,否则会将p置为空
p.reset(q, d)                   //若还传递了参数d,将会调用d而不是delete来释放q

避免混合使用普通指针和智能指针:

推荐使用make_shared而不是new,这样我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上,当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了

//在函数调用时ptr被创建并初始化
void process(shared_ptr<int>ptr)
{
	//使用ptr
}//ptr离开作用域,被销毁
int* x(new int(1024));  //x是一个普通指针
process(x);   //错误 不能将内置指针传递给process
process(shared_ptr<int>(x)); //合法的,但内存会被释放
int j = *x;  //未定义的,x是一个空悬指针

也不要使用get初始化另一个智能指针或为智能指针赋值,智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象,此函数是为了这样的一种情况而设计的,我们需要向不能使用智能指针的代码传递一个内置指针,使用get返回的指针的代码不能delete此指针。将另一个智能指针也绑定到get返回的指针上是错误的(虽然编译器不会给出错误信息)。

shared_ptr<int> p(new int(42)); //引用计数为1
int*q = p.get(); //正确 但是使用q时要注意,不要让它管理的指针被释放
{//新程序块
 //未定义:俩个独立的shared_ptr指向相同的内存
	shared_ptr<int> temp(q);
}//程序块结束,q被销毁,它指向的内存被释放
int foo = *p;  //未定义:p指向的内存已经被释放了

在本例中,p和q指向相同的内存,由于它们是相互独立创建的,因此各自引用计数都是1,当q在程序块结束时q被销毁,这回导致q指向的内存被释放,从而p变成一个空悬指针,当p被销毁时,这块内存会被第二次delete。

智能指针和异常:

即使程序块过早结束,可以使用智能指针来确保在异常发生后,资源能被正确的释放
void f()
{
    shared_ptr<int> sp(new int(42));
    //这段代码抛出了一个异常,且在f中未被捕获
}//在函数结束时shared_ptr自动释放内存
与之相对的,当异常发生时,使用内置指针直接管理的内存是不会自动释放
void f()
{
    int* ip = new int(42);
    //这段代码抛出了一个异常,且在f中未被捕获
    delete ip;
}
如果在new和delete之间发生异常,且未在f中被捕获,则内存就永远不会释放

智能指针陷阱

1.不使用相同的内置指针值初始化(或reset)多个智能指针
2.不delete get()返回的指针
3.不使用get()初始化或者reset另一个智能指针
4.如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效的了
5.如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值