C++Primer笔记-动态内存

前言

该系列是《C++Primer第五版》的笔记,包含本人认为值得记录和整理的主要的知识点,并不是全部内容,也不是具体的内容。
该系列文章的作用应该是作为复习或预习的参考,有哪些知识点忘记或想学,可以大致浏览下该文章,然后再去书中寻找详细解答。(本系列文章基本是按书本顺序罗列的知识点,便于大家去书中寻找)
所以看该文章前,需要有一定的C++基础,否则阅读起来可能有困难。

本文大致整理了第十二章的知识点,涉及到C++关于动态内存和智能指针的知识,重要!

链接目录

动态内存与智能指针

基本概念:

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

动态内存的管理:

  • new:在动态内存中为对象分配空间并返回一个指向该对象的指针。
  • delete:接受一个动态对象的指针,销毁对象并释放内存。

智能指针:定义在头文件memory中。

  • shared_ptr:允许多个指针指向同一个对象。
  • unique_ptr:独占所指的对象。
  • weak_ptr:弱引用,指向shared_ptr所管理的对象。

shared_ptr类

智能指针也是模板,需要提供对象类型。

shared_ptr<string> p1;//默认初始化是一个空指针
//在使用指针前,应当检测指针是否为空
if(p1){
	...
}

shared_ptrunique_ptr都支持的操作:

图片.png

shared_ptr独有的操作:

图片.png

make_shared函数:类似顺序容器中的emplace成员,跳过拷贝直接构造。

shared_ptr的拷贝和赋值:当进行拷贝和赋值操作时,shared_ptr会记录有多少个其他shared_ptr指向相同的对象。这个计数器叫做引用计数。

引用计数递增的情况:

  • 用一个shared_ptr初始化另一个shared_ptr
  • 一个shared_ptr作为参数传递给一个函数。
  • shared_ptr作为函数的返回值。

引用计数递减的情况:

  • 给一个shared_ptr赋予新值。
  • 一个shared_ptr被销毁(离开作用域)。

当一个shared_ptr的计数器为0时,会自动释放所管理的对象。

使用动态内存的原因:

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

直接管理内存

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

int *pi = new int;//默认初始化,值未定义
int *pi2 = new int(42);//显式初始化
int *pi3 = new int();//值初始化为0

//对于定义了构造函数的类型,值初始化和默认初始化都是调用构造函数
int *p1 = new string;
int *p2 = new string();//结果都是初始化为空string

动态分配的const对象:

const int *pci = new const int(1024);//const对象必须初始化

内存耗尽:如果堆内存没有空间,new表达式会失败,抛出bad_alloc异常。

int *p1 = new int;//如果分配失败,抛出bad_alloc异常
int *p2 = new(nothrow)int;//阻止抛出异常,如果分配失败返回一个空指针

释放动态内存:销毁指向的对象,释放对应的内存。

delete p;//p必须指向一个动态分配的对象或是一个空指针

指针值和delete:编译器能分辨p是否是指针,但通常不能分辨指针是指向静态还是动态分配的对象,也不能分辨对象是否以及被释放。

动态对象的生存期直到被释放时为止:如果没有显式释放,它会一直存在直到程序结束。

动态内存的管理非常容易出错!
使用newdelete管理动态内存存在三个常见问题:

  1. 忘记delete内存,会导致内存泄漏问题。
  2. 使用已经释放掉的对象。
  3. 同一块内存释放两次。

delete之后重置指针值:在delete之后,指针就变成了空悬指针,指向一块无效的内存。可以在释放指针之后置为nullptr,避免后续使用空悬指针。

shared_ptr和new结合使用

可以用new返回的指针来初始化智能指针,由于智能指针的构造函数是explicit的,所以不能将内置指针隐式转换为只能指针,必须使用直接初始化。

shared_ptr<int> p(new int(42));
shared_ptr<int> p2 = new int(1024);//错误,必须使用直接初始化形式

//在函数返回值也类似
shared_ptr<int> f(int p){
	return new int(p);//错误,不能隐式转换
	return shared_ptr<int>(new int(p));//正确
}

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

图片.png

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

void process(shared_ptr<int> ptr){
	...
}//ptr离开作用域,被销毁
int *x = new int(1024);
precess(x);//错误,不能将int*转换为一个shared_ptr<int>
pricess(shared_ptr<int>(x));//合法,但process执行完成后,x指向的内存会被释放
int j = *x;//x是一个空悬指针,由于智能指针判定计数为0,x指向的内存被释放

也不要使用get初始化另一个智能指针或为智能指针赋值:get函数返回一个内置指针,通常用于向不能使用只能指针的代码传递一个内置指针。不能deleteget返回的指针,否则会导致智能指针无法正常工作(计数未置为0,但内存已经被释放)。

其他shared_ptr操作:

if(!p.unique())
	p.reset(new string(*p));//不是唯一的用户,使用拷贝而不改变原对象
*p += newVal;//是唯一的用户,可以根据自己的需要操作

智能指针和异常

void f(){
	shared_ptr<int> sp(new int(42));
	//代码中抛出一个异常,且在f中未被捕获
}//在函数结束时会释放内存
void f(){
	int *ip = new int(42);
	//代码中抛出一个异常,且在f中未被捕获
	delete ip;//由于异常未被执行
}//ip指向的内存无法释放,导致内存泄漏

智能指针和哑类:对于没有良好定义的析构函数(为C和C++两种语言设计的类,通常要求用户显式释放资源),即使显式释放,也可能在释放前发生异常导致无法释放。这时可以使用shared_ptr避免内存泄露,即使代码段没被执行,局部智能指针离开作用域也会被销毁使内存正常释放。

使用自定义的释放操作:

void our_delete(Sales_data *p){
	//手动执行析构函数的内容
}
void f(...){
	Sales_data c(...);
	shared_ptr<Sales_data> p(&c, our_delete);
	//当f退出时,即使是因为异常退出,也能正确释放内存
}
//此时p被销毁时,不执行delete,而是调用our_delete。

智能指针陷阱:为了正确使用,必须坚持一些规范

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

unique_ptr

图片.png

unique_ptr并不支持拷贝或赋值操作:

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

//但可以转移所有权
unique_ptr<string> p2(p1.release());//转移所有权,p1为空
unique_ptr<string> p3(new string());
p2.reset(p3.release());//reset释放p2的内存,接着指向p3指向的内容

p2.release();//错误,p2指向空,不会释放内存,而且丢失了指向原本内存的指针
auto p = p2.release();//正确,但该类型不是智能指针,需要手动delete(p)

传递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传递删除器:

//decltype(end_connection)*是函数类型,end_connection是函数指针
void f(destination &d){
	connection c = connect(&d);
	unique_ptr<connection, decltype(end_connection)*>
		p(&c, end_connection);
}

weak_ptr

weak_ptr指向由shared_ptr管理的对象,将一个weak_ptr绑定到shared_ptr不会改变引用次数。

图片.png

由于对象可能不存在,不能直接使用weak_ptr直接访问对象,必须调用lock

动态数组

通常STL中内置的类型可以很好的代替动态数组(通常更安全且更高效),如非必要,不使用动态数组。

new和数组:

int *pia = new int[get_size()];//pia指向第一个int

分配一个数组得到的一个元素类型的指针,并不是数组类型,所以不能对动态数组调用begin或end,也不能使用范围for语句。

初始化动态数组:

int *pia = new int[10];//未初始化的int
int *pia2 = new int[10]();//值初始化为0的int,但不能在括号中给出初始化器
int *pia3 = new int[10]{1, 2, 3};//剩下的执行值初始化

动态分配一个空数组是合法的:

int *p = new int[0];//合法,但p不能解引用

释放动态数组:如果错用了释放方式,行为结果是未定义的。

delete p;//p指向一个动态分配的对象
delete[] p;//p指向一个动态分配的数组

智能指针和动态数组:可以使用unique_ptr来管理动态数组,此时不支持点和箭头运算符。

图片.png

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

要使用shared_ptr必须提供自定义的删除器:默认情况下shared_ptr使用delete销毁,如果对象是一个动态数组,则结果是未定义的,所以需要提供自定义的删除器。而且shared_ptr未定义下标运算符,智能指针类型也不支持指针算术运算,为了访问数组元素,必须用get获得内置指针,然后再用它来访问数组元素。

allocator类

将内存分配和对象构造分离开,避免不必要的浪费。

图片.png

allocator<string> alloc;
auto const p = alloc.allocate(n);//分配n个未初始化的string,p指向不可变
auto q = p;//q指向可变
alloc.construct(q++);//构造一个空string
alloc.construct(q++, 10, 'c');//构造一个string(10, 'c')
cout << *p << endl;
cout << *q << endl;//错误,q指向的内存未构造

while(q != p)
	alloc.destroy(--q);//释放构造过的string
alloc.deallocate(p, n);//释放内存

拷贝和填充未初始化内存的算法:

图片.png

上一篇 下一篇

评论 | 0条评论