C++ Primer学习笔记-----第十二章:动态内存

静态内存:保存局部static对象、类static数据成员、定义在任何函数之外的变量
栈内存:保存定义在函数内的非static对象。
堆内存:存储动态分配的对象。

1.动态内存与只能指针
在C++中,动态内存的管理是通过一对运算符来完成的:
new :在动态内存中为对象分配空间并返回一个指向该对象的指针,可以对对象进行初始化。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

为了更容易更安全地使用动态内存,新的标准提供了两种只能指针类型来管理动态对象。
只能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。
shared_ptr:允许多个指针指向同一个对象。
unique_ptr:独占所指向的对象。
weak_ptr:弱引用,指向shared_ptr所管理的对象。

这三种类型都定义在memory头文件中,智能指针也是模板。

shared_ptr类:
在这里插入图片描述

shared_ptr<string> p1;		//p1可以指向string,默认初始化的智能指针中保存一个空指针
if(p1 && p1->empty()		//和普通指针使用类似
	*p1 = "hi";

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

shared_ptr<int> p1 = make_shared<int>();				//p1指向一个值初始化的int的shared_ptr,值为0
shared_ptr<int> p2 = make_shared<int>(666);				//p2指向一个值为666的int的shared_ptr
shared_ptr<string> p3 = make_shared<string>(3,'9');		//p3指向一个值为999的string

make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配,如果不传递
任何参数,对象就会进行值初始化。
auto p4 = make_shared<vectro<string>>();		//使用auto来简化对象的定义

*****shared_ptr的拷贝和赋值*****
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(3);		//p指向的对象只有p一个引用者
auto q(p);							//p和q指向相同的对象,此对象有两个引用者

可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数。无论何时我们拷贝一个shared_ptr,计数器都会递增。
例如:用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数以及作为函数的返回值时,它所关联的计数器都会递增。
当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(一个局部的shared_ptr离开其作用域)时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
auto p = make_shared<int>(3);		//p的计数器增加1
auto q = make_shared<int>(4);		//q的计数器增加1
p = q;			//q的计数器增加1,p的计数器减1,p的计数现在为0,p原来指向的对象已没有引用者,会自动释放

***注:到底是用一个计数器还是其他数据结构来记录有多少指针共享对象,完全有标准库的具体实现来决定。

*****shared_ptr自动销毁所管理的对象*****
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数:析构函数完成销毁工作。
每个类都有一个析构函数,析构函数控制此类型的对象销毁时做什么操作。
析构函数一般用来释放对象所分配的资源。
shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,释放占用的内存。
shared_ptr<int> fun()
{
	shared_ptr<int> p = make_shared<int>(3);	//计数增加1
	return p;	//返回p时(拷贝操作),引用计数增加1,函数结束p要销毁,引用计数减1,总的引用计数还是1,所以不会销毁原来的对象
}
shared_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素,在这种情况下
你应该确保用erase删除那些不再需要的shared_ptr元素。

*****使用了动态生存期的资源的类*****
程序使用动态内存出于以下三种原因之一:
1.程序不知道自己需要使用多少对象		:容器类出于这个原因使用动态内存
2.程序不知道所需对象的准确类型		:15章说
3.程序需要在多个对象间共享数据		

自定义类含有共享数据

class Test
{
public:
	typedef vector<int>::size_type size_type;
	Test();
	Test(initializer_list<int> il);
	size_type size() const { return data->size(); }
	bool empty() const { return data->empty(); }
	void push_back(const int i) { data->push_back(i); }
	void pop_back();
	int front();
	int back();
private:
	shared_ptr<vector<int>> data;							//共享数据
	void check(size_type i, const string& msg) const;
};

Test::Test() :data(make_shared<vector<int>>()) {}
Test::Test(initializer_list<int> il) :data(make_shared<vector<int>>(il)) {}

void Test::pop_back()
{
	check(0, "pop_back on empty");
	data->pop_back();
}

int Test::front()
{
	check(0, "front on empty");
	return data->front();
}

Test使用默认版本的拷贝、赋值和销毁成员函数来对此类型的对象进行这些操作,默认情况下,这些操作拷贝、赋值和销毁类的数据成员
Test类只有一个数据成员,它是shared_ptr类型,因此当我们拷贝、赋值或销毁一个Test对象时,它的shared_ptr成员会被拷贝、赋值或销毁。

直接管理内存

new分配内存,delete释放new分配的内存。
相对于只能指针,使用这两个运算符非常容易出错。

*****使用new动态分配和初始化对象*****
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。
int *pi = new int;		//pi指向一个动态分配的未初始化的无名对象

默认情况下,动态分配的对象是默认初始化的,意味着内置类型或组合类型的对象的值是未定义的,而类类型对象将用默认构造函数进行初始化。
string *ps = new string;		//初始化为空string
int *pi = new int; 				//pi指向一个未初始化的int
直接初始化:
int *pi = new int(1);
string *ps = new string(3,'s');
vector<int> *pv = new vector<int>{0,1,3};
值初始化:
string *ps = new string;		//默认初始化为空string(string不是内置类型,走默认构造)
string *ps2 = new string();		//值初始化为空string
int *pi = new int;				//默认初始化,*pi的值未定义(内置类型)
int *pi2 = new int();			//值初始化为0

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

类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的。

*****动态分配的const对象*****
const int *pci = new const int(666);
const int *pcs = new const string;

*****内存耗尽*****
程序用光所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
可以使用new的方式来阻止它抛出异常:称这种形式的new为定位new(在19章讲)
int *p1 = new int;				//分配失败,抛出异常
int *p2 = new (nothrow) int;	//分配失败,new返回一个空指针

释放动态内存

delete p;		//p必须指向一个动态分配的对象或是一个空指针
delete执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

释放一个非new分配的内存或者将相同的指针值释放多次,其行为是未定义的。
const对象本身是可以被销毁的:
const int *pci = new const int(2);
delete pci;

动态对象的生存期直到被释放时为止

返回指向动态内存的指针(不是智能指针)的函数给调用者增加一个额外的负担:调用者必须记得释放内存。
int* fun()
{
	return new int(1);		//调用者负责释放此内存
}

void fun2()
{
	auto p = fun();		//使用p但不delete它
}//p离开它的作用域,但它所指向的内存没有释放
内置类型的对象被销毁时什么也不会发生,特别是当一个指针离开其作用域时,它所指向的对象什么也不会发生,如果这个指针指向的是动态内存
那么内存将不会自动释放。

小心:动态内存的管理非常容易出错

使用newdelete管理动态内存存在三个常见问题:
1.忘记delete内存:导致内存泄漏
2.使用已经释放掉的对象:
3.同一块内存释放两次

***坚持只使用只能指针,就可以避免所有这些问题。

delete之后重置指针值

当我们delete一个指针后,指针值就变为无效了,虽然指针已经无效,但在很多机器上指针仍然保持着(已经释放了的)动态内存地址。
在delete之后,指针就变成了空悬指针:指向一块曾经保存数据对象但现在已经无效的内存的指针。

未初始化指针的所有缺点空悬指针也都有,避免空悬指针的方法:
***在再指针即将离开其作用域之前提前释放掉它所关联的内存。***
如果要保留指针可以在delete之后,将nullptr赋予指针,这时指针就不指向任何对象。
delete p;		//空悬指针
p = nullptr;

还有一个问题:可能有多个指针指向相同的内存,在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放)内存的指针
是没有作用的。例如:
int *p(new int(666));
auto q = p;				//p和q指向相同的内存
delete p;				//p和q均变为无效
p = nullptr;			//p不再绑定到任何对象

shared_ptr和new结合使用

可以用new返回的指针来初始化智能指针:
shared_ptr<int> p(new int(3));
接受指针参数的智能指针构造函数时explicit的,因此,不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式。
shared_ptr<int> p = new int(1);		//错误:必须使用直接初始化形式
出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:
shared_ptr<int> fun(int p)
{
	return new int(p);	//错误:不能隐式转换
}

shared_ptr<int> fun(int p)
{
	return shared_ptr<int>(new int(p));	 //正确:显示绑定
}

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

在这里插入图片描述在这里插入图片描述
不要混合使用普通指针和智能指针

shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间,这也是推荐使用make_shared
而不是new的原因。这样我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免无意中将同一块绑定到多个独立创建的shared_ptr上。
例如:
void process(shared_ptr<int> ptr>)
{
	//使用ptr
}	//ptr离开作用域,被销毁
process的参数是传值方式传递的,因此实参会被拷贝到ptr中,拷贝一个shared_ptr会递增其引用计数,因此在process运行过程中,引用计数值至少为2。当process结束时,ptr的引用计数会递减,但不会变为0,当局部变量ptr被销毁时,ptr指向的内存不会被释放。
使用此函数的正确方式是传递给它一个shared_ptr:
shared_ptr<int> p(new int(123));	//计数为1
process(p);							//在process里:计数为2
int i = *p;							//process执行完:计数为1,所以可以修改对象
虽然不能传递给process一个内置指针,但可以传递给它一个(临时的)shared_ptr,这个shared_ptr是用一个内置指针显示构造的。但是,这样
做很可能会导致错误:
int *x(new int(12));
process(shared_ptr<int>(x));	//合法,但内存会被释放,process执行完shared_ptr对象会销毁,指向的内存被释放了
int j = *x;						//未定义:x继续指向已释放的内存,是一个空悬指针

当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr,一旦这样做了,就不应该再使用内置指针来访问shared_ptr所指向的内存了。

*****也不要使用get初始化另一个智能指针或为智能指针赋值*****
智能指针类型定义了一个名为get的函数:返回一个内置指针,指向智能指针管理的对象。
此函数是为了向不能使用智能指针的代码传递一个内置指针。
使用get返回的指针的代码不能delete此指针。
shared_ptr<int> p(new int(123));
int *q = p.get();
{//新程序块
	shared_ptr<int> m(q);
}//程序块结束
int n = *p;

改成如下更好理解:
void fun(int * pp)
{
	shared_ptr<int> m(pp);		//创建一个新的m
	*m = 888;	
}//离开作用域,m销毁,引用计数为0,会释放指向的内存

void main()
{
	shared_ptr<int> p(new int(666));
	int* q = p.get();
	fun(q);			//fun执行结束后,p指向的内存已经被释放
	int n = *p;		//未定义,p指向的内存已经被释放
}//离开作用域销毁p,p指向的内存会被第二次delete

*****其他shared_ptr操作*****
reset:将一个新的指针赋予一个shared_ptr:
shared_ptr<int> p(new int(1));
p = reset(new int(2));		//p指向一个新对象
与赋值类似,reset会更新引用计数,如果需要的话,会释放p指向的对象。
reset成员经常与unique一起使用,来控制多个shared_ptr共享的对象,在改变底层对象之前,检查自己是否是当前对象的仅有用户。如果不是
在改变之前要制作一份新的拷贝:
if(!p.unique())					//p不是独占的,引用计数大于1
	p.reset(new int(*p));		//解绑p,重新绑定到一个新指针,指针的值是源对象的值
*p += newVal;					//现在p是独占的

智能指针和异常

void fun()
{
	shared_ptr<int> p(new int(1));
	int *p2 = new int(2);
	//这里发生异常
	delete p2;				//在delete之前发生异常,且异常未在fun中捕获,内存永远不会释放
}//函数结束shared_ptr自动释放内存

一个简单的确保资源被释放的方法是使用智能指针

智能指针和哑类

包括所有标准库类在内的很多C++类都定义了析构函数,负责清理对象使用的资源,但是不是所有的类都是这样良好定义的。
特别是那些为C和C++两种语言设计的类,通常要求用户显示地释放所使用的任何资源。
那些分配了资源,又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误:程序员非常容易忘记释放资源。
类似的,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
与管理动态内存类似,通常可以使用类似的技术来管理不具有良好定义的析构函数的类。
例如:假定正在使用一个C和C++都使用的网络库,代码可能是这样的:
struct destination;
struct connection;
connection connect(destination*);
void disconnect(connection);
void f(destination &d /*其他参数*/)
{
	connection c = connect(&d);		//获得一个连接:记住使用完要关闭它
	//使用连接
	//如果在f退出前忘记调用disconnect,就无法关闭c了
}
如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。
但connection没有析构函数,这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎都是等价的。
使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。

*****使用我们自己的释放操作*****
默认情况下,shared_ptr假定它们指向的是动态内存。因此当一个shared_ptr被销毁时,它默认地对它管理地对它管理的指针进行delete操作。
为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器函数必须能够完成对shared_ptr中保存的指针
进行释放的操作。本例中,删除器必须接受单个类型为connection*d参数:
void end_connection(connection *p){ disconnect(*p); }
当创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数:
void f(destination &d /*其他参数*/)
{
	connection c = connect(&d);		//获得一个连接:记住使用完要关闭它
	shared_ptr<connection> p(&c,end_connection);	
	//使用连接
	//当f退出时(即使异常而退出),connection会被正确关闭
}
当p被销毁时,它不会对自己保存的指针执行delete,而是调用end_connection。end_connection会调用disconnect,从而确保连接被关闭。

在这里插入图片描述
unique_ptr
在这里插入图片描述

一个unique_ptr“拥有”它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
和shared_ptr类似,unique_ptr必须采用直接初始化。
unique_ptr<int> p;
unique_ptr<int> q(new int(666));
由于unique_ptr拥有它所指向的对象,因此unique_ptr不支持普通的拷贝和赋值操作:
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2(p1);			//错误,不支持拷贝
unique_ptr<int> p3;
p3=p1;										//错误,不支持赋值
可以通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:
unique_ptr<int> p1(new int(1));
unique_ptr<int> p2(p1.release());		//将所有权从p1转移给p2,p1置空
unique_ptr<int> p3(new int(2));
p3.reset(p2.release());			//将所有权从p2转移给p3,p2置空,p3释放原来的指针,重新绑定到p2之前的对象

调用release会切断unique_ptr和它原来管理的对象间的联系,release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。
如果不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
p1.release();				//错误:p1不会释放内存,而且我们丢失了指针
auto p = p1.release();		//正确,但我们必须记得delete p;

传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:可以拷贝或赋值一个将要被销毁的unique_ptr。
最常见的例子是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p)
{
	return unique_ptr<int>(new int(p));
}
还可以返回一个局部对象的拷贝:
unique_ptr<int> clone(int p)
{
	unique_ptr<int> ret(new int(p));
	//****
	return ret;
}

在这里插入图片描述
向unique_ptr传递删除器

我们可以重载一个unique_ptr中默认的删除器,但是,unique_ptr管理删除器的方式与shared_ptr不同,原因在16章讲
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作类似,
必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的
可调用对象(删除器):
unique_ptr<objT,delT> p (new ojbT,fun);
用unique_ptr重写之前的连接程序:
void f(destination &d)
{
	connection c = connect(&d);
	unique_ptr<connection, decltype(end_connection)*> p(&c,end_connection);
	//使用连接
}//函数退出,connection会被正确关闭

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。
将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。
即使有weak_ptr指向对象,对象也还是会被释放。

在这里插入图片描述

创建一个weak_ptr时,要用一个shared_ptr来初始化它:
auto p = make_shared<int>(3);
weak_ptr<int> wp(p);
使用前先通过lock判断对象释放存在:
if(shared_ptr<int> np = wp.lock())	//np不为空就进行下面的操作

weak_ptr的使用场景:(网上搜的)
1.可以解决shared_ptr循环引用的问题(也是网上查的)
2.使用对象,但不想管理对象,并在返回对象的shared_ptr时,可以返回一个weak_ptr
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值