C++ primer 第12章 动态内存

前言

静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。

除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称为自由空间或堆。程序用堆来存储动态分配的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制,也就是说,当动态对象不再使用时,我们的代码必须显式地销毁它们。

动态内存与智能指针

动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针;delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。

动态内存的使用很容易出现问题,因为确保在正确的时间释放内存是及其困难的。忘记释放内存,会产生内存泄漏;在尚有指针引用内存的情况下我们就释放了内存,会产生引用非法内存的指针。

新标准库提供了两种智能指针类型来管理动态对象,它可以自动释放所指向的对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。weak_ptr是一种弱引用,指向shared_ptr所管理的对象,这三种类型都定义在memory头文件中。

shared_ptr类

当我们创建一个智能指针时,必须提供额外的信息——指针可以指向的类型。与vector一样,我们在尖括号内给出类型,之后是所定义的这种智能指针的名字:

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

默认初始化的智能指针中保存着一个空指针。

shared_ptr和unique_ptr都支持的操作

在这里插入图片描述

shared_ptr独有的操作

在这里插入图片描述

make_shared 函数

p3指向一个值为42的int的智能指针
shared_ptr<int>p3 = make_shared<int>(42);

p4指向一个值为"88888"的string的智能指针
shared_ptr<string>p4 = make_shared<string>(5,'8');

p5指向一个值为0的int的智能指针(int的初始化值为0)
shared_ptr<int>p5 = make_shared<int>();

shared_ptr的拷贝和赋值

当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。离开作用域计数器会递减,赋值操作,原指针计算器会递减,赋值指向的指针计数器会递增。示例代码如下:

	auto p = make_shared<int>(42);//p指向的对象只有p一个引用者
	auto r = make_shared<int>(4);//p指向的对象只有p一个引用者
	auto r2(r);//p指向的对象只有p一个引用者
	cout << "p引用数量:" << p.use_count()<<endl;
	{
		auto q(p);//p和q指向相同对象,此对象有两个引用者
		cout << "p引用数量:" << p.use_count() << endl;
		cout << "p指向:" << *p << endl;
		*q = 10;
		cout << "p指向:" << *p << endl;
	}
	cout << "在赋值之前,p引用数量:" << p.use_count() << endl;
	cout << "在赋值之前,原r引用数量:" << r2.use_count() << endl;
	r = p;
	cout << "在赋值之后,原r引用数量:" << r2.use_count() << endl;
	cout << "在赋值之后,p引用数量:" << p.use_count() << endl;
	cout << "p指向:" << *p << endl;

输出结果:

p引用数量:1
p引用数量:2
p指向:42
p指向:10
在赋值之前,p引用数量:1
在赋值之前,原r引用数量:2
在赋值之后,原r引用数量:1
在赋值之后,p引用数量:2
p指向:10

shared_ptr自动销毁所管理的对象

当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会通过析构函数自动销毁此对象。shared_ptr的析构函数会递减它所指向的对象的引用计数,如果引用计数为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。

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

当动态对象不再被使用时,shared_ptr会自动释放动态对象,这一特性使得动态内存的使用变得非常容易。
假设现有一个Foo类,示例代码:

//factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg){
	return make_shared<Foo>(arg);
}

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

//但如果有其他shared_ptr也指向这块内存,则它就不会被释放:
shared_ptr<Foo> use_factory2(T arg){
	shared_ptr<Foo>p=factory(arg);
	//使用p
	return p;//当我们返回p时,引用计数进行了递增
}//p离开作用域,它指向的内存不会被释放

因此,如果将shared_ptr存放于一个容器中,而后不再需要全部元素而只使用其中一部分,要记得用erase删除不再需要的那些元素。

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

  • 程序不知道自己需要使用多少对象 例如:容器类
  • 程序不知道所需对象的准确类型
  • 程序需要在多个对象间共享数据

我们定义一个类,它使用动态内存是为了让多个对象共享相同的底层数据。我们定义一个名为Blob的类,保存一组元素。我们希望Blob对象的不同拷贝之间共享相同的元素,即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。

Blob类的定义如下:

#ifndef __BLOB__
#define __BLOB__

#include<string>
# include<iostream>
# include<vector>
# include<memory>
using namespace std;

class Blob {
public:
	typedef vector<string>::size_type size_type;

	Blob();
	Blob(initializer_list<string>il);

	size_type size()const { return data->size(); }
	bool empty()const{return data->empty();}
	//添加和删除元素
	void push_back(const string &t) { data->push_back(t); }
	void pop_back();
	//元素访问
	string & front();
	string & back();
	//获取元素
	shared_ptr<vector<string>> getData() { return data; }
	//修改元素
	void changeData(size_type i,string str) {
		(*data)[i] = str;
	}

private:
	shared_ptr<vector<string>>data;
	void check(size_type i,const string &msg)const;
};

Blob::Blob() :data(make_shared<vector<string>>()){}
Blob::Blob(initializer_list<string>il) : data(make_shared<vector<string>>(il)) {}
void Blob::check(size_type i, const string &msg)const {
	if (i >= data->size())
		throw out_of_range(msg);
}
string& Blob::front() {
	check(0,"front on empty Blob");
	return data->front();
}
string& Blob::back() {
	check(0, "back on empty Blob");
	return data->back();
}
void Blob::pop_back() {
	check(0, "pop_back on empty Blob");
	data->pop_back();
}
void print(ostream& os,Blob b) {
	for (auto a : *(b.getData())) {
		os << a << " ";
	}
	os << endl;
}

#endif

对Blob进行测试代码如下:

	Blob b1{ "a","an","the" };
	cout << "b1的元素为:";
	print(cout,b1);
	Blob b2(b1);
	cout << "b2的元素为:";
	print(cout, b2);
	b2.changeData(0,"hello");
	cout << "对b2中的元素进行修改后:" << endl;
	cout << "b1的元素为:";
	print(cout, b1);
	cout << "b2的元素为:";
	print(cout, b2);

输出结果:

b1的元素为:a an the
b2的元素为:a an the
对b2中的元素进行修改后:
b1的元素为:hello an the
b2的元素为:hello an the

从输出结果我们可以看出,Blob在多个对象间共享数据。当我们拷贝、赋值或销毁一个Blob对象时,它的shared_ptr成员会被拷贝、赋值或销毁。

我们定 义的Blob与vector的区别:

vector<string>v1;
{
	vector<string>v2={"a","an","the"};
	v1=v2;//从v2拷贝元素到v1中
}//v2被销毁,其中的元素也被销毁
//v1有三个元素,是原来v2元素的拷贝
Blob<string>b1;
{
	Blob<string>b2={"a","an","the"};
	b1=b2;//b1和b2共享相同的元素
}//b2被销毁,其中的元素不能销毁
//b1指向最初由b2创建的元素

直接管理内存

C++语言定义了两个运算符来分配和释放动态内存。运算符new分配内存,delete释放new分配的内存。相对于智能指针,使用这两个运算符管理内存非常容易出错。

使用new动态分配和初始化对象

在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针。
例如:

int *pi = new int(1024);
vector<int> *pv = new vector<int>{1,2,3,4,5};

如果我们提供了一个括号包围的初始化器,就可以使用auto,从此初始化器来推断我们想要分配的对象的类型。只有当括号中仅有单一初始化器时才可以使用auto:

auto p1 = new auto(obj); //p1指向一个与obj类型相同的对象
auto p2 = new auto(a,b,c); //错误:括号中只能有单个初始化器

动态分配的const对象

类似其他任何const对象,一个动态分配的const对象必须进行初始化。

const int *pci = new const int(1024);

内存耗尽 定位new

虽然现代计算机通常都配备大容量内存,但是自由空间被耗尽的情况还是有可能发生。一旦一个程序用光了它所有可用的内存,new表达式就会失败。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常。我们称这种形式的new为定位new。

int *p1 = new int;//如果分配失败,new抛出std::bad_alloc
int *p1 = new (nothrow) int;//如果分配失败,new返回一个空指针

释放动态内存

我们通过delete表达式来将动态内存归还给系统。delete表达式接受一个指针,指向我们想要释放的对象:

delete p;

与new类型类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。

我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。

由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在。

使用new和delete管理动态内存存在三个常见问题

  • 忘记delete内存,会导致“内存泄漏”问题,因为这种内存永远不可能归还给自由空间了。
  • 使用已经释放掉的对象。
  • 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。

delete之后重置指针值

当我们delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即,指向一块曾经保存数据对象但现在已经无效的内存的指针。
未初始化指针的所有缺点空悬指针也都有。有一种方法可以避免空悬指针的问题:在指针即将要离开其作用域之前释放掉它所关联的内存。如果我们需要保留指针,可以在delete之后将nullptr赋予指针,这样就清楚地指出指针不指向任何对象。

动态内存的一个基本问题是可能有多个指针指向相同的内存。在delete内存之后重置指针的方法只对这个指针有效,对其他任何仍指向(已释放的)内存的指针是没有作用的,以下代码重置p对q没有任何作用。

int *p(new int(42));
auto q=p;  //p和q指向相同的内存
delete p;//p和q均变为无效
p=nullptr;//指出p不再绑定到任何对象

智能指针和new对比

int *q=new int(42),*r=new int(100);
r=q;
auto q2=make_shared<int>(42),r2=make_shared<int>(100);
r2=q2;

以上代码new指针的两个问题:

  • 内存泄漏问题,r和q都指向42的内存地址,而r原来保存的地址——100的内存再无指针管理,不能进行释放,造成内存泄漏。
  • 空悬指针问题,r和q指向同一个动态对象,如果程序编写不当,很容易产生释放了其中一个指针,而继续使用另一个指针的问题。

智能指针的优势:

  • 将q2赋予r2,赋值操作会将q2指向的对象地址赋予r2,并将r2原来指向的对象的引用计数减1,将q2指向的对象的引用计数加1,这样前者的引用计数变为0,其占用的内存空间会被释放,不会造成内存泄漏。
  • 而后者的引用计数变为2,也不会因为r2和q2之一的销毁而释放它的内存空间,因此也不会造成空悬指针的问题。

shared_ptr和new结合使用

如果我们不出事后一个智能指针,它就会被初始化为一个空指针。我们还可以用new返回的指针来初始化智能指针。接受指针参数的智能指针构造函数是explicit,因此我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针。

shared_ptr<int>p1=new int(1024);//错误
shared_ptr<int>p2(new int(1024));//正确:使用了直接初始化形式

定义和改变shared_ptr的其他方法

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

不要混合使用智能指针和普通指针

void process(shared_ptr<int>ptr){}

shared_ptr<int> x(new int(1024));//引用计数为1
process(x); //正确,拷贝x会递增它的引用计数,在process中引用计数为2
int j=*x;//正确:引用计数为1
void process(shared_ptr<int>ptr){}

int *x(new int(1024));
process(x); //错误,不能将int*转换为shared_ptr<int>
process(shared_ptr<int>(x));//正确,但内存会被释放
int j=*x;//未定义的,x是一个空悬指针

在该调用中,我们将一个临时的shared_ptr传递给process,当这个调用所在的表达式结束时,这个临时对象就被销毁了。销毁这个临时变量会递减引用计数,此时引用计数为0,因此,当临时变量被销毁时,它所指向的内存会被释放,但x继续指向(已经释放的)内存,从而变成一个空悬指针,如果试图使用x的值,其行为是未定义的。
当将一个shared_ptr绑定到一个普通指针时,我们就将内存的管理责任交给了这个shared_ptr。一旦这样做了,我们就不应该再使用内置指针来访问shared_ptr所指向的内存了。
使用一个内置指针来访问一个智能指针所负责的对象是很危险的,因为我们无法知道对象何时会被销毁。

不要使用get初始化另一个智能指针或为智能指针赋值

智能指针类型定义了一个名为get的函数,它返回一个内置指针,指向智能指针管理的对象。当我们需要向不能使用智能指针的代码传递一个内置指针时可用get。使用get返回的指针的代码不能delete此指针。

	shared_ptr<int>p(new int(42));//p的引用计数为1
	int *q = p.get();//正确,但使用q时要注意,不要让它管理的指针被释放,p的引用计数仍然为1
	{
		process(shared_ptr<int>(q));
	}
	int foo = *p;//错误,此时p的引用计数为1,但p指向的内存已经被释放了,p成为类似空悬指针的shared_ptr

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

智能指针和异常

使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放:

void f(){
	shared_ptr<int>sp(new int(42));//分配一个新对象
	//这段代码抛出一个异常,且在f中未被捕获
}//在函数结束时shared_ptr自动释放内存

与之相对的,如果使用内置指针管理内存,且在new之后在对应的delete之前发生异常,则内存不会被释放:

void f(){
	int *ip=new int(42);//动态分配一个新对象
	//这段代码抛出一个异常,且在f中未被捕获,则内存不会被释放
	delete ip;//释放内存的代码
}

使用自己定义的释放操作

现有以下类和函数:

struct destination;//表示我们正在连接什么
struct connection;//使用连接所需的信息
connection connect(destination*);//打开连接的函数
void disconnect(connection);//关闭给定的连接

代码一:

void f(destination &d){
	//获得一个连接
	connection c = connect(&d);
	//使用连接
	//如果我们在f退出前忘记调用disconnect,就无法关闭c了
}

代码二,使用shared_ptr:

//定义删除器:
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,从而确保连接被正确关闭。如果f正常退出,那么p的销毁会作为结束处理的一部分。如果发生了异常,p同样会被销毁,从而连接被正确关闭。

使用智能指针基本规范

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

unique_ptr

与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象,与shared相同的操作在表中

unique_ptr特有的操作

在这里插入图片描述
由于一个 unique_ptr拥有它所指向的对象,因此 unique_ptr不支持普通的拷贝或赋值操作:

unique_ptr<string>p1(new string("hello"));
unique_ptr<string>p2(p1);//错误,不支持拷贝
unique_ptr<string>p3;
p3=p1;//错误,不支持赋值

虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique_ptr:
在这里插入图片描述

release返回指针

如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:

p2.release();//错误,p2不会释放内存,而且我们丢失了指针
auto p=p2.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传递删除器

类似shared_ptr,unique_ptr默认情况下用delete释放它所指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。
对比shared_ptr的连接程序,我们重写连接程序如下:

//定义删除器:
void end_connection(connection *p){disconnect(*p);}
//创建shared_ptr:
void f(destination &d){
	//获得一个连接
	connection c = connect(&d);
	unique_ptr<connection,decltype(end_connection)*>p(&c,end_connection);
	//使用连接
	//当f退出时(即使是由于异常而退出),connection会被正确关闭
}

weak_ptr

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
在这里插入图片描述
示例代码:

	auto p = make_shared<int>(42);
	auto q(p);
	auto p2(p);
	cout << "p.use_count():" << p.use_count() <<endl;
	weak_ptr<int>wp(p);
	cout << "p.use_count():" << p.use_count() << endl;
	cout << "wp.use_count():" << wp.use_count() << endl;

输出结果:

p.use_count():3
p.use_count():3
wp.use_count():3

由于对象可能不存在,我们不能使用weak_ptr直接访问对象,必须调用lock。次函数检查weak_ptr指向的对象是否仍存在。

if(shared_ptr<int>np=wp.lock()){ //如果np不为空则条件成立
//在if中,np与p共享对象
}

动态数组

new和数组

int *pia = new int[10];//分配10个int,pia指向第一个int

分配一个数组会得到一个元素类型的指针。

我们还可以提供一个元素初始化器的花括号列表:
在这里插入图片描述

释放动态数组

delete p;//p必须指向一个动态分配的对象或为空
delete [] p;//p必须指向一个动态分配的数组或为空,
//数组中的元素按逆序进行销毁,即,最后一个元素首先被销毁,
//然后是倒数第二个,以此类推

智能指针和动态数组

标准库提供了一个可以管理new分配的数组的unique_ptr版本。

unique_ptr<int[]>up(new int[10]);
up.release();//自动用delete[]销毁其指针

指向数组的unique_ptr

在这里插入图片描述
与unique_ptr不同,shared_ptr不直接支持管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器:

shared_ptr<int>sp(new int[10],[](int *p){delete[]p;});
sp.reset();//使用我们提供的lambda释放数组,它使用delete[]释放数组

如果未提供删除器,这段代码将是未定义的。默认情况下,shared_ptr使用delete销毁它指向的对象。

连接两个字符串

代码:

	const char *c1 = "hello";
	const char *c2 = "world";

	char *r = new char[strlen(c1)+ strlen(c2)+1];
	strcpy(r,c1);
	strcat(r, c2);
	cout << r << endl;

	string s1 = "hello";
	string s2 = "world";
	strcpy(r,(s1+s2).c_str());
	cout << r << endl;

输出结果:

helloworld
helloworld

allocator类

new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起,这可能会导致不必要的浪费。
标准库allocator类定义在头文件memory中。它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。

标准库allocator类及其算法

在这里插入图片描述

allocator分配未构造的内存

在这里插入图片描述
还未构造对象的情况下就使用原始内存是错误的。
为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数。

allocator算法

在这里插入图片描述
在这里插入图片描述
类似copy,uninitialized_copy返回(递增后的)目的位置迭代器,因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值