前言
动态内存主要就是靠自行分配和自行销毁内存。内存的正确释放是非常容易出错的,所以有智能指针类型——shared_ptr、unique_ptr和weak_ptr,可令动态内存管理更为安全。需要了解更详细知识,大家可自行查看书籍,这里只介绍一些细节地方。
最后,如果有理解不对的地方,希望大家不吝赐教,谢谢!
九、动态内存
全局对象在程序启动时分配,在程序结束时销毁。对于局部自动对象,当我们进入其定义所在的程序块时被创建,在离开块时销毁。局部static对象在第一次使用前分配,在程序结束时销毁。
而动态分配的对象的生存期与他们在哪里创建是无关的,只有显示地被释放时,这些对象才会销毁。动态对象的正确释放被证明是编程中极其容易出错的地方,为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它的智能指针可以确保自动地释放它。
- 栈:函数内非static对象
- 静态内存:局部static对象、类static数据成员以及定义在任何函数之外的变量
- 堆:动态分配的对象
栈和静态内存中的对象由编译器自动创建和销毁的,而堆中的对象的生存期由程序来控制。
动态内存与智能指针
new:在动态内存中为对象分配空间并返回一个指向该对象的指针,即初始化工作。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
新标准库提供了两种智能指针类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。 这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。智能指针类型不支持指针算术运算,必须用get获取一个内置指针。
shared_ptr类
智能指针也是模板,必须提供指针可以指向的类型。在尖括号内给出类型,之后定义指针名字。
shared_ptr<string> p1; //p1可以指向string
- p.get():返回p中保存的指针,要小心使用,若智能指针释放了其对象,返回的指针所指向的对象也就消失了。
- make_shared<T>(args):返回一个shared_ptr,指向一个动态分配的类型为T的对象,使用args初始化此对象
- p.use_count():返回与p共享的智能指针数量
- p.unique():若p.use_count()为1,返回true;否则返回false
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数,此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
//指向一个值为42的int的shared_ptr shared_ptr<int> p3=make_shared<int>(42); //还可以指向一个值初始化的 shared_ptr<int> p4=make_shared<int>(); //通常用auto定义一个对象来保存make_shared的结果 auto p5=make_shared<vector<string>>()
shared_ptr的拷贝和赋值
每个shared_ptr都有一个关联的计数器,通常称其为引用计数,无论何时我们拷贝一个shared_ptr,计数器都会递增;当给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域)时,计数器就会被递减。
shared_ptr自动销毁所管理的对象
当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数完成销毁工作的。
shared_ptr还会自动释放相关联的内存
如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象 (例如:容器类)
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
直接管理内存
自己直接管理内存的类与使用智能指针的类不同,它们不能依赖类对象拷贝、赋值和销毁操作的任何默认定义,因此,使用智能指针的程序更容易编写和调试。
运算符new分配内存,delete释放new分配的内存。
出于与变量初始化相同的原因,对动态分配的对象进行初始化通常是个好主意。
如果我们提供了一个括号包围的初始化器,就可以使用auto从此初始化器来推断我们要分配的对象的类型。
一个动态分配的const对象必须进行初始化。
内存耗尽
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常,我们可以改变使用new的方式来阻止它抛出异常:
int *p=new (nothrow) int; //如果分配失败,new返回一个空指针
bad_alloc和nothrow都定义在头文件new中。
释放动态内存
delete p; //p必须指向一个动态分配的对象或是一个空指针
虽然一个const对象的值不能被改变,但它本身是可以被销毁的。
由内置指针(而不是智能指针)管理的动态内存在被显示释放前一直都会存在。
使用new和delete管理动态内存存在的三个常见问题:
- 忘记delete内存,造成“内存泄漏”
- 使用已释放掉的对象。
- 同一块内存释放两次
delete之后重置指针值
delete一个指针后,指针值就变为无效了。虽然指针已经无效,但在很多机器上指针仍然保存着(已经释放了的)动态内存的地址。在delete之后,指针就变成了人们所说的空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。避免空悬指针的问题:释放掉它所关联的内存。delete一个内存后,指向它的指针都会变为无效。
shared_ptr和new结合使用
接受指针参数的智能指针构造函数是explicit的,必须使用直接初始化形式来初始化一个智能指针。默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
shared_ptr<int> p(new int(1024)); //使用直接初始化形式
不要混合使用普通指针和智能指针
使用一个内置指针来访问一个智能指针所赋值的对象是很危险的,因为我们无法知道对象何时会被销毁。
get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
其他shared_ptr操作
可以用reset来将一个新的指针赋予一个shared_ptr。reset会更新引用计数,如果需要的话,会释放p指向的对象。reset成员经常会与unique一起用,来控制多个shared_ptr共享的对象。
if(!p.unique()) p.reset(new string(*p)) //我们不是唯一用户,分配新的拷贝 *p+=newVal; //现在我们知道自己是唯一的用户,可以改变对象的值
正确使用智能指针:
- 不使用相同的内置指针值初始化(或reset)多个智能指针
- 不delete get()返回的指针
- 不使用get()初始化或reset另一个智能指针
- 如果使用get()返回的指针,记住当最后对应的智能指针销毁后,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器
unique_ptr
某个时刻只有一个unique_ptr指向一个给定对象,当unique_ptr被销毁时,它所指向的对象也被销毁。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上,且必须采用直接初始化形式。由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。但可以通过release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique。
p2.reset(p3.release()); //将所有权从p3转移给p2
调用release会切断unique_ptr和它原来管理的对象间的连续。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。但是,如果我们不用另一个智能指针来保存release返回的指针,我们的程序就要负责资源的释放:
auto p=p2.release(); //必须记得delete(p)
可以传递unique_ptr参数和返回unique_ptr
weak_ptr
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。调用look函数来检查weak_ptr指向的对象是否仍存在,返回一个指向共享对象的shared_ptr。可以用来判断它所指向的对象是否已被销毁。
动态数组
当容器需要重新分配内存时,必须一次性为很多元素分配内存。提供了两种一次分配一个对象数组的方法:(1)另一种new表达式;(2)使用一个allocator的类,允许我们将分配和初始化分离。
大多数应用应该使用标准库容器而不是动态分配的数组,使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
new和数组
int *pia=new int[get_size()]; //方括号中的大小必须是整数,但不必是常量 typedef int arrT[42]; int *p=new arrT; //int *p=new int[42]
分配一个数组会得到一个元素类型的指针
当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end,也不能使用范围for语句来处理动态数组中的元素。
动态分配一个空数组是合法的,但此指针不能解引用。
释放动态数组
在指针前加上一个空方括号对
delete [] pa; //pa必须指向一个动态分配的数组或为空
智能指针和动态数组
为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号:
//up指向一个包含10个未初始化int的数组 unique_ptr<int[]> up(new int[10]); up.release(); //自动用delete[]销毁其指针
当一个unique_ptr指向一个数组时,不能使用点和箭头成员运算符,可以使用下标运算符来访问数组中的元素。与unique_ptr不同,shared_ptr不直接支持管理动态数组,如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。shared_ptr未定义下标运算符,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素
allocator类
当分配一大块内存时,我们通常计划在这块内存上按需构造对象,在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。allocator是一个模板,为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型,当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置。
allocator<string> alloc; auto const p=alloc.allocate(n); //分配n个未初始化的string
allocator分配未构造的内存
allocator分配的内存是未构造的,我们按需要在此内存中构造对象。为了使用allocate返回的内存,我们必须使用construct构造对象,使用未构造的内存,其行为是未定义的。当我们用完对象后,必须对每个构造的元素调用destroy来销毁它。记住我们只能对真正构造了的元素进行destroy操作。释放内存通过调用deallocate来完成。
拷贝和填充未初始化内存的算法
//分配比vi中元素所占用空间大一倍的动态内存 auto p=alloc.allocate(vi.size()*2); //通过拷贝vi中的元素来构造从p开始的元素 auto q=uninitialized_copy(vi.begin(),vi.end(),p); //将剩余元素初始化为42 unitialized_fill_n(q,vi.size(),42);