[C++]C++Primer Chapter 12
动态内存
静态内存用来保存局部static对象(参见6.6.1节,第185页)、类static数据成员(参见7.6节,第268页)以及定义在任何函数之外的变量。
栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。
对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或堆(heap)。程序用堆来存储动态分配(dynamically allocate)的对象——即,那些在程序运行时分配的对象。动态对象的生存期由程序来控制。
12.1 动态内存与智能指针
为了更容易(同时也更安全)地使用动态内存,新的标准库提供了两种智能指针(smart pointer)类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是它负责自动释放所指向的对象。新标准库提供的这两种智能指针的区别在于管理底层指针的方式:shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,它是一种弱引用,指向shared_ptr所管理的对象。这三种类型都定义在memory头文件中。
12.1.1 shared_ptr类
make_shared函数
最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。
类似顺序容器的emplace成员(参见9.3.1节,第308页),make_shared用其参数来构造给定类型的对象。
如果我们不传递任何参数,对象就会进行值初始化(参见3.3.1节,第88页)。
shared_ptr的拷贝和赋值
当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象。
我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增。例如,当用一个shared_ptr初始化另一个shared_ptr,或将它作为参数传递给一个函数(参见6.2.1节,第188页)以及作为函数的返回值(参见6.3.2节,第201页)时,它所关联的计数器就会递增。当我们给shared_ptr赋予一个新值或是shared_ptr被销毁(例如一个局部的shared_ptr离开其作用域(参见6.1.1节,第184页))时,计数器就会递减。
一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
shared_ptr自动销毁所管理的对象,shared_ptr还会自动释放相关联的内存
由于在最后一个shared_ptr销毁前内存都不会释放,保证shared_ptr在无用之后不再保留就非常重要了。如果你忘记了销毁程序不再需要的shared_ptr,程序仍会正确执行,但会浪费内存。share_ptr在无用之后仍然保留的一种可能情况是,你将shared_ptr存放在一个容器中,随后重排了容器,从而不再需要某些元素。在这种情况下,你应该确保用erase删除那些不再需要的shared_ptr元素。
如果你将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,要记得用erase删除不再需要的那些元素。
使用了动态生存期的资源的类
程序使用动态内存出于以下三种原因之一:
- 程序不知道自己需要使用多少对象
- 程序不知道所需对象的准确类型
- 程序需要在多个对象间共享数据
容器类是出于第一种原因而使用动态内存的典型例子,我们将在第15章看到出于第二种原因而使用动态内存的例子。在本节中,我们将定义一个类,它使用动态内存是为了让多个对象能共享相同的底层数据。
某些类分配的资源具有与原对象相独立的生存期。例如,假定我们希望定义一个名为Blob的类,保存一组元素。与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。即,当我们拷贝一个Blob时,原Blob对象及其拷贝应该引用相同的底层元素。
使用动态内存的一个常见原因是允许多个对象共享相同的状态。
12.1.2 直接管理内存
使用new动态分配和初始化对象
默认情况下,动态分配的对象是默认初始化的(参见2.2.1节,第40页),这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化。也可以对动态分配的对象进行值初始化(参见3.3.1节,第88页),只需在类型名之后跟一对空括号即可
对于定义了自己的构造函数(参见7.1.4节,第235页)的类类型(例如string)来说,要求值初始化是没有意义的;不管采用什么形式,对象都会通过默认构造函数来初始化。但对于内置类型,两种形式的差别就很大了;值初始化的内置类型对象有着良好定义的值,而默认初始化的对象的值则是未定义的。类似的,对于类中那些依赖于编译器合成的默认构造函数的内置类型成员,如果它们未在类内被初始化,那么它们的值也是未定义的(参见7.1.4节,第236页)。
如果我们提供了一个括号包围的初始化器,就可以使用auto(参见2.5.2节,第61页)从此初始化器来推断我们想要分配的对象的类型。但是,由于编译器要用初始化器的类型来推断要分配的类型,只有当括号中仅有单一初始化器时才可以使用auto
动态分配的const对象
内存耗尽
默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc(参见5.6节,第173页)的异常。我们可以改变使用new的方式来阻止它抛出异常
这种形式的new为定位new(placement new),其原因我们将在19.1.2节(第729页)中解释。定位new表达式允许我们向new传递额外的参数。在此例中,我们传递给它一个由标准库定义的名为nothrow的对象。如果将nothrow传递给new,我们的意图是告诉它不能抛出异常。如果这种形式的new不能分配所需内存,它会返回一个空指针。bad_alloc和nothrow都定义在头文件new中。
释放动态内存
指针值和delete
我们传递给delete的指针必须指向动态分配的内存,或者是一个空指针(参见2.3.2节,第48页)。释放一块并非new分配的内存,或者将相同的指针值释放多次,其行为是未定义的
动态对象的生存期直到被释放时为止
返回指向动态内存的指针(而不是智能指针)的函数给其调用者增加了一个额外负担——调用者必须记得释放内存
小心:动态内存的管理非常容易出错
使用new和delete管理动态内存存在三个常见问题:
- 忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄漏”问题,因为这种内存永远不可能被归还给自由空间了。查找内存泄露错误是非常困难的,因为通常应用程序运行很长时间后,真正耗尽内存时,才能检测到这种错误。
- 使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。
- 同一块内存释放两次。当有两个指针指向相同的动态分配对象时,可能发生这种错误。如果对其中一个指针进行了delete操作,对象的内存就被归还给自由空间了。如果我们随后又delete第二个指针,自由空间就可能被破坏。相对于查找和修正这些错误来说,制造出这些错误要简单得多。
delete之后重置指针值,这只是提供了有限的保护
12.1.3 shared_ptr和new结合使用
如果我们不初始化一个智能指针,它就会被初始化为一个空指针。如表12.3所示,我们还可以用new返回的指针来初始化智能指针
接受指针参数的智能指针构造函数是explicit的(参见7.5.4节,第265页)。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式(参见3.2.1节,第76页)来初始化一个智能指针
默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。
我们可以将智能指针绑定到一个指向其他类型的资源的指针上,但是为了这样做,必须提供自己的操作来替代delete。
不要混合使用普通指针和智能指针
shared_ptr可以协调对象的析构,但这仅限于其自身的拷贝(也是shared_ptr)之间。这也是为什么我们推荐使用make_shared而不是new的原因。这样,我们就能在分配对象的同时就将shared_ptr与之绑定,从而避免了无意中将同一块内存绑定到多个独立创建的shared_ptr上。
也不要使用get初始化另一个智能指针或为智能指针赋值
智能指针类型定义了一个名为get的函数(参见表12.1),它返回一个内置指针,指向智能指针管理的对象。此函数是为了这样一种情况而设计的:我们需要向不能使用智能指针的代码传递一个内置指针。使用get返回的指针的代码不能delete此指针。
将另一个智能指针也绑定到get返回的指针上是错误的。
get用来将指针的访问权限传递给代码,你只有在确定代码不会delete指针的情况下,才能使用get。特别是,永远不要用get初始化另一个智能指针或者为另一个智能指针赋值。
12.1.4 智能指针和异常
一个简单的确保资源被释放的方法是使用智能指针。如果使用智能指针,即使程序块过早结束,智能指针类也能确保在内存不再需要时将其释放
智能指针和哑类
包括所有标准库类在内的很多C++类都定义了析构函数(参见12.1.1节,第402页),负责清理对象使用的资源。但是,不是所有的类都是这样良好定义的。特别是那些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
如果connection有一个析构函数,就可以在f结束时由析构函数自动关闭连接。但是,connection没有析构函数。这个问题与我们上一个程序中使用shared_ptr避免内存泄漏几乎是等价的。使用shared_ptr来保证connection被正确关闭,已被证明是一种有效的方法。
使用我们自己的释放操作
默认情况下,shared_ptr假定它们指向的是动态内存。因此,当一个shared_ptr被销毁时,它默认地对它管理的指针进行delete操作。为了用shared_ptr来管理一个connection,我们必须首先定义一个函数来代替delete。这个删除器(deleter)函数必须能够完成对shared_ptr中保存的指针进行释放的操作。在本例中,我们的删除器必须接受单个类型为connection*的参数
当我们创建一个shared_ptr时,可以传递一个(可选的)指向删除器函数的参数(参见6.7节,第221页)
注意:智能指针陷阱
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
- 不使用相同的内置指针值初始化(或reset)多个智能指针。
- 不delete get()返回的指针。
- 不使用get()初始化或reset另一个智能指针。
- 如果你使用get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
- 如果你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器(参见12.1.4节,第415页和12.1.5节,第419页)。
12.1.5 unique_ptr
一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作
虽然我们不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique
传递unique_ptr参数和返回unique_ptr
不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr
还可以返回一个局部对象的拷贝
对于两段代码,编译器都知道要返回的对象将要被销毁。在此情况下,编译器执行一种特殊的“拷贝”,我们将在13.6.2节(第473页)中介绍它。
向unique_ptr传递删除器
unique_ptr管理删除器的方式与shared_ptr不同,其原因我们将在16.1.6节(第599页)中介绍。
重载一个unique_ptr中的删除器会影响到unique_ptr类型以及如何构造(或reset)该类型的对象。与重载关联容器的比较操作(参见11.2.2节,第378页)类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型的对象时,必须提供一个指定类型的可调用对象(删除器)
12.1.6 weak_ptr
weak_ptr(见表12.5)是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。即使有weak_ptr指向对象,对象也还是会被释放。
当我们创建一个weak_ptr时,要用一个shared_ptr来初始化它。
由于对象可能不存在,我们不能使用weak_ptr直接访问对象,而必须调用lock。此函数检查weak_ptr指向的对象是否仍存在。如果存在,lock返回一个指向共享对象的shared_ptr。与任何其他shared_ptr类似,只要此shared_ptr存在,它所指向的底层对象也就会一直存在。
12.2 动态数组
C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了另一种new表达式语法,可以分配并初始化一个对象数组。标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力,原因我们将在12.2.2节(第427页)中解释。
使用容器的类可以使用默认版本的拷贝、赋值和析构操作(参见7.1.5节,第239页)。分配动态数组的类则必须定义自己版本的操作,在拷贝、复制以及销毁对象时管理所关联的内存。
12.2.1 new和数组
为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。在下例中,new分配要求数量的对象并(假定分配成功后)返回指向第一个对象的指针
分配一个数组会得到一个元素类型的指针
由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin或end(参见3.5.3节,第106页)。这些函数使用数组维度(回忆一下,维度是数组类型的一部分)来返回指向首元素和尾后元素的指针。出于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。
要记住我们所说的动态数组并不是数组类型,这是很重要的。
初始化动态分配对象的数组
默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化(参见3.3.1节,第88页),方法是在大小之后跟一对空括号。
在新标准中,我们还可以提供一个元素初始化器的花括号列表。如果初始化器数目小于元素数目,剩余元素将进行值初始化。如果初始化器数目大于元素数目,则new表达式失败,不会分配任何内存。在本例中,new会抛出一个类型为bad_array_new_length的异常。类似bad_alloc,此类型定义在头文件new中。
动态分配一个空数组是合法的
当n等于0时,调用new[n]是合法的。
new返回一个合法的非空指针。此指针保证与new返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样(参见3.5.3节,第106页),我们可以像使用尾后迭代器一样使用这个指针。可以用此指针进行比较操作,就像上面循环代码中那样。可以向此指针加上(或从此指针减去)0,也可以从此指针减去自身从而得到0。但此指针不能解引用——毕竟它不指向任何元素。
释放动态数组
为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空方括号对
数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,依此类推。
如果我们在delete一个指向数组的指针时忽略了方括号(或者在delete一个指向单一对象的指针时使用了方括号),其行为是未定义的。
智能指针和动态数组
标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号
指向数组的unique_ptr提供的操作与我们在12.1.5节(第417页)中使用的那些操作有一些不同
与unique_ptr不同,shared_ptr不直接支持管理动态数组。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。
shared_ptr未定义下标运算符,而且智能指针类型不支持指针算术运算。因此,为了访问数组中的元素,必须用get获取一个内置指针,然后用它来访问数组元素。
12.2.2 allocator类
new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。
当分配一大块内存时,我们通常计划在这块内存上按需构造对象。在此情况下,我们希望将内存分配和对象构造分离。这意味着我们可以分配大块内存,但只在真正需要时才真正执行对象创建操作(同时付出一定开销)。
allocator类
标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。
类似vector,allocator是一个模板(参见3.3节,第86页)。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。当一个allocator对象分配内存时,它会根据给定的对象类型来确定恰当的内存大小和对齐位置
allocator分配未构造的内存
在新标准库中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。
为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。
当我们用完对象后,必须对每个构造的元素调用destroy来销毁它们。函数destroy接受一个指针,对指向的对象执行析构函数(参见12.1.1节,第402页)
我们传递给deallocate的指针不能为空,它必须指向由allocate分配的内存。而且,传递给deallocate的大小参数必须与调用allocated分配内存时提供的大小参数具有一样的值。
拷贝和填充未初始化内存的算法
标准库还为allocator类定义了两个伴随算法,可以在未初始化内存中创建对象。表12.8描述了这些函数,它们都定义在头文件memory中。
类似copy,uninitialized_copy返回(递增后的)目的位置迭代器。
因此,一次uninitialized_copy调用会返回一个指针,指向最后一个构造的元素之后的位置。