第十二章 动态内存
我们的程序到目前为止只使用过静态内存或栈内存。
-
静态内存用来保存局部static对象、类static数据成员以及定义在任何函数之外的变量。
-
栈内存用来保存定义在函数内的非static对象。
分配在静态或栈内存中的对象由编译器自动创建和销毁。
-
对于栈对象,仅在其定义的程序块运行时才存在。
-
static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池,这部分内存被称作自由空间或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象。
动态对象的生存周期由程序来控制,当动态对象不再使用时,我们的代码必须显示地销毁它们。
动态内存与智能指针
C++中,动态内存的管理是通过一对运算符来完成的:
-
new,在动态内存中为对象分配空间并返回一个指向该对象的指针。
-
delete,接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
为了更容易(同时也更安全)地使用动态内存,新的标准提供了两种智能指针(smart pointer)类型来管理动态对象。
智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。两种智能指针的区别在于管理底层指针的方式:
-
shared_ptr允许多个指针指向同一个对象;
-
unique_ptr则“独占”所指向的对象。
-
标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。
这些类型定义在memory头文件中。
shared_ptr类
智能指针也是模板,当创建一个智能指针时,必须提供指向的类型:
shared_ptr<string> p1; // shared_ptr, 可以指向string
默认初始化的智能指针中保存着一个空指针。
解引用一个智能指针返回它指向的对象。如果在一个条件判断中使用智能指针,效果就是检测它是否为空:
if (p1) *p1 = "hi";
更多的操作见p401。
make_shared函数
最安全的分配和使用动态内存的方法是调用标准库函数make_shared。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
// 指向一个值为42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p6指向一个动态分配的空vector<string>
auto p6 = make_shared<vector<string>>();
类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。如果我们不传递任何参数,对象就会进行值初始化。
shared_ptr的拷贝和赋值
每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:
auto p = make_shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同的对象,此对象有两个引用者
可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
Note
到底是由一个计数器还是其他数据结构来记录有多少指针共享对象,完全由标准库的具体实现决定。关键是智能指针类能记录有多少个shared_ptr指向相同的对象,并能在恰当的时候自动释放对象。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
1. 程序不知道自己需要多少对象
2. 程序不知道所需对象的准确类型
3. 程序需要在多个对象间共享数据
容器类是出于第一种原因而使用动态内存的典型例子,我们将在第15章看到出于第二种原因的例子。本章介绍出于第三种原因的例子。
直接管理内存
C++提供了new运算符分配内存,delete运算符释放new分配的内存。
相对于智能指针,使用这两个运算符管理内存非常容易出错。
使用new动态分配和初始化对象
在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象
默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型将使用默认构造函数进行初始化。
可以使用直接初始化方式来初始化一个动态分配的对象:
int *pi = new int(1024);
vector<int> *pv = new vector<int>{1, 2, 3};
动态分配的const对象
用new分配const对象是合法的:
const int *pci = new const int(1024);
类似其他任何const对象,一个动态分配的const对象必须进行初始化。
内存耗尽
一旦一个程序用光了它所有可用的内存,new表达式就会失败(并返回一个空指针)。默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。
我们可以改变使用new的方式来阻止它抛出异常:
// 如果分配失败,new返回一个空指针
int *p1 = new int; // 如果分配失败,new抛出std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失败,new返回一个空指针
释放动态内存
为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式(delete expression)来将动态内存归还给系统。
delete p; // p必须指向一个动态分配的对象或是一个空指针
释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的。
更多有关使用原生指针管理动态内存的危险的讨论见书本p409。
shared_ptr和new结合使用
如果不初始化一个智能指针,它就会被初始化为一个空指针。还可以用new返回的指针来初始化智能指针:
shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int
接受指针参数的智能指针构造函数是explicit的,因此必须使用直接初始化形式来初始化一个智能指针:
shared_ptr<int> p1 = new int(1024); // 错误:必须使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正确:使用了直接初始化形式
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象(可以提供自己的操作来替代delete)。
更多关于智能指针使用的讨论见p412。
智能指针和异常
程序需要确保在异常发生后资源能被正确地释放。一个简单的确保资源被释放的方法是使用智能指针:
void f()
{
shared_ptr<int> sp(new int(42)); // 分配一个对象
// 这段代码抛出一个异常,且在f中未被捕获
} // 函数结束时shared_ptr自动释放内存
无论是否发生了异常,局部对象都会被销毁,sp是指向这块内存的唯一指针,因此内存会被释放掉。
如果使用了内置指针管理内存,且在new之后在对应的delete之前发生了异常,则内存不会被释放:
void f()
{
int *ip = new int(42); // 动态分配一个新对象
// 这段代码抛出一个异常,且在f中未被捕获
delete ip; // 在退出以前释放内存
}
如果在new和delete之间发生了异常,且异常未在f中被捕获,则内存就永远不会被释放了。
使用我们自己的释放操作
这里给一个简单的定义删除器的例子,而具体的讨论见书本p416。
auto deleter = [](int* p)
{
std::cout << "delete data: " << *p << std::endl;
delete p;
};
std::shared_ptr<int> p(new int(42), deleter);
unique_ptr
与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。
unique_ptr<double> p1; // 可以指向一个double的unique_ptr
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作。
更多有关unique_ptr操作的讨论见p418。
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>(42);
weak_ptr<int> wp(p); // wp若共享p;p的引用计数未改变
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。如果存在,lock返回一个指向共享对象的shared_ptr。否则返回一个空shared_ptr。
if (shared_ptr<int> np = wp.lock()) { // 如果np不为空则条件成立
// 在if中,np与p共享对象
}
动态数组
C++语言和标准库提供了两种一次分配一个对象数组的方法:
-
一种new表达式语法,可以分配并初始化一个对象数组。
-
标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。
Note
大多数应用应该使用标准库容器而不是动态分配的数组。使用容器更为简单、更不容易出现内存管理错误并且可能有更好的性能。
new和数组
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目:
// 调用get_size确定分配多少个int
int *pia = new int[get_size()]; // pia指向第一个int
方括号中的大小必须是整型,但不必是常量。
分配一个数组会得到一个元素类型的指针
当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
Note
要记住我们所说的动态数组并不是数组类型,这是很重要的。
初始化动态分配对象的数组
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号:
int *pia = new int[10]; // 10个未初始化的int
int *pia2 = new int[10](); // 10个值初始化为0的int
新标准中,我们还可以提供一个元素初始化器的花括号列表:
// 10个int分别用列表中对应的初始化器初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对:
delete p; // p必须指向一个动态分配的对象或为空
delete [] pa; // pa必须指向一个动态分配的数组或为空
数组的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本:
// up指向一个包含10个未初始化int的数组
unique_ptr<int[]> up(new int[10]);
up.release(); // 自动用delete[]销毁其指针
my note: 这里似乎有错误,release方法据p418介绍,是放弃对指针的控制权,返回指针。并不销毁原来指向的对象。另一个事例见:http://zh.cppreference.com/w/cpp/memory/unique_ptr/release
当unique_ptr销毁时,会自动销毁其指向的对象。
allocator类
new和delete有一些灵活性上的局限:
-
new将内存分配和对象构造组合在了一起。
-
delete将对象析构和内存释放组合在了一起。
当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作。
allocator类
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它分配的内存是原始的、未构造的。
allocator也是模板,为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定对象类型来确定恰当的内存大小和对齐位置:
allocator<string> alloc; // 可以分配string的allocator对象
auto const p = alloc.allocate(n); // 分配n个未初始化的string
allocator分配未构造的内存
allocator分配的内存是未构造的(unconstructed)。我们按需要在此内存中构造对象。
auto q = p; // q指向最后构造元素之后的位置
alloc.construct(q++); // *q为空字符串
alloc.construct(q++, "hi"); // *q为hi!
还未构造对象的情况下就使用原始内存是错误的。
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。
while (q != p)
alloc.destroy(--q); // 释放我们真正构造的string
一旦元素被销毁后,就可以重新用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成:
alloc.deallocate(p, n);
我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。
拷贝和填充未初始化内存的算法
标准库为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。见p429。