第12章 动态内存

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

除了静态内存栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间(free store)或(heap)。

21.1 动态内存与智能指针

在C++中,动态内存的管理是通过一对运算符来完成的:new,在动态内存中为对象分配空间并返回一个指向该对象的指针,我们可以选择对对象进行初始化;delete,接受一个动态指针,销毁该对象,并释放与之关联的内存。
为了更容易地使用动态内存,新的标准提供了两种智能指针(smart pointer)类型来管理动态对象。shared_ptr允许多个指针指向同一个对象;unique_ptr则“独占”所指向的对象。标准库还定义了一个名为weak_ptr的伴随类,指向shared_ptr所管理的对象。这三种类型都定义在memory中。

  • shared_ptr类
  1. 类似vector智能指针也是模板。

    表12.1
  2. make_shared函数
    最安全的分配和使用动态内存的方法是调用一个名为make_shared的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的shared_ptr。与智能指针一样,make_shared也定义在memory中。

    类似顺序容器的emplace成员,make_shared用其参数来构造给定类型的对象。例如,调用make_shared<string>时传递的参数必须与string的某个构造函数相匹配。
    当然,我们通常用auto定义一个对象来保存make_shared的结果,这种方式简单:
  3. shared_ptr的拷贝和赋值
    当进行拷贝或赋值操作时,每个shared_ptr都会记录有多少个其他shared_ptr指向相同的对象:

    我们可以认为每个shared_ptr都有一个关联的计数器,通常称其为引用计数(reference count)。无论何时我们拷贝一个shared_ptr,计数器都会递增;当我们给shared_ptr赋予一个新值或是shared_ptr被销毁时,计算器就会递减。
    一旦一个shared_ptr的计数器变为0,它就会自动释放自己所管理的对象:
  4. shared_ptr自动销毁所管理的对象……
    当指向一个对象的最后一个shared_ptr被销毁时,shared_ptr类会自动销毁此对象。它是通过另一个特殊的成员函数——析构函数(destructor)完成销毁工作的。析构函数一般用来释放对象所分配的资源;shared_ptr的析构函数会递减它所指向的对象的引用计数。如果引用计数变为0,shared_ptr的析构函数就会销毁对象,并释放它占用的内存。
  • 直接管理内存

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

  1. 使用new动态分配和初始化对象
    在自由空间分配的内存是无名的,因此new无法为其分配的对象命名,而是返回一个指向该对象的指针:

    默认情况下,动态分配的对象是默认初始化的,这意味着内置类型或组合类型的对象的值将是未定义的,而类类型对象将用默认构造函数进行初始化:

    也可以对动态分配的对象进行值初始化,只需在类型名之后跟一对空括号即可:
  2. 动态分配的const对象
    使用new分配const对象是合法的

    类似其他任何const对象,一个动态分配的const对象必须进行初始化。对于一个定义了默认构造函数的类类型,其const动态对象可以隐式初始化,而其他类型的对象就必须显式初始化。由于分配的对象是const的,new返回的指针是一个指向const的指针。
  3. 内存耗尽
    默认情况下,如果new不能分配所要求的内存空间,它会抛出一个类型为bad_alloc的异常。我们可以改变使用new的方式来阻止它抛出异常:

    我们称这种形式的new为定位new。
  4. 释放动态内存
    为了防止内存耗尽,在动态内存使用完毕后,必须将其归还给系统。我们通过delete表达式(delete expression)来将动态内存归还给系统。

与new类似,delete表达式也执行两个动作:销毁给定的指针指向的对象;释放对应的内存。在delete之后,指针就变成了人们所说的空悬指针,即指向一块曾经保存数据对象但现在已经无效的内存的指针。如果需要保留指针,可以在delete之后将nullptr赋予指针。

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

(1)忘记delete内存。忘记释放动态内存会导致人们常说的“内存泄露”问题,因为这种内存永远不可能归还给自由空间了。查找内存泄露错误是非常困难的。

(2)使用已经释放掉的对象。通过在释放内存后将指针置为空,有时可以检测出这种错误。

(3)同一块内存释放两次。当两个指针指向相同的动态分配对象时,可能发生这种错误。

  • shared_ptr和new结合使用

如前所述,如果我们不初始化一个智能指针,他就会被初始化为一个空指针。如表12.3所示,我们还可以用new返回的指针来初始化智能指针:

接受指针参数的智能指针构造函数是explicit的。因此,我们不能将一个内置指针隐式转换为一个智能指针,必须使用直接初始化形式来初始化一个智能指针

出于相同的原因,一个返回shared_ptr的函数不能在其返回语句中隐式转换一个普通指针:

默认情况下,一个用来初始化智能指针的普通指针必须指向动态内存,因为智能指针默认使用delete释放它所关联的对象。表12.3

  1. 不要混合使用普通指针和智能指针(P413)
  2. 也不要使用get初始化另一个智能指针或为智能指针赋值(P414)
  • unique_ptr
  1. 一个unique_ptr“拥有”它所指向的对象。与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。
  2. 与shared_ptr不同,没有类似make_shared的标准库函数返回一个unique_ptr。当我们定义一个unique_ptr时,需要将其绑定到一个new返回的指针上。类似shared_ptr,初始化unique_ptr必须采用直接初始化形式:

    由于一个unique_ptr拥有它指向的对象,因此unique_ptr不支持普通的拷贝或赋值操作:

    表12.4虽然我不能拷贝或赋值unique_ptr,但可以通过调用release或reset将指针的所有权从一个(非const)unique_ptr转移给另一个unique:

调用release会切断unique_ptr和它原来管理的对象间的联系。release返回的指针通常被用来初始化另一个智能指针或给另一个智能指针赋值。

3.传递unique_ptr参数和返回unique_ptr

不能拷贝unique_ptr的规则有一个例外:我们可以拷贝或赋值一个将要被销毁的unique_ptr。最常见的例子是从函数返回一个unique_ptr:

对于两段代码,编译器都知道要返回的对象将要销毁。在此情况下,编译器执行一种特殊的“拷贝”,我们将在13.6.2节介绍它。

4.向unique_ptr传递删除器

类似shared_ptr,unique_ptr默认情况下用delete释放它指向的对象。与shared_ptr一样,我们可以重载一个unique_ptr中默认的删除器。与重载关联容器的比较操作类似,我们必须在尖括号中unique_ptr指向类型之后提供删除器类型。在创建或reset一个这种unique_ptr类型对象时,必须提供一个指定类型的可调用对象(删除器):

  • weak_ptr
    weak_ptr是一种不控制所指向对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数表12.5当我创建一个weak_ptr时,要用一个shared_ptr来初始化它:

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

new和delete运算符一次分配/释放一个对象,但某些应用需要一次为很多对象分配内存的功能。为了支持这种需求,C++语言和标准库提供了两种一次分配一个对象数组的方法。C++语言定义了一种new表达式,可以分配并初始化一个对象数组。
标准库中包含一个名为allocator的类,允许我们将分配和初始化分离。使用allocator通常会提供更好的性能和更灵活的内存管理能力。

  • new和数组

为了让new分配一个对象数组,我们要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。

  1. 分配一个数组会得到一个元素类型的指针
    虽然我们通常称new T[ ]分配的内存为“动态数组”,但这种叫法某种程度上有些误导。当用new分配一个数组时,我们并未得到一个数组类型的对象,而是得到一个数组元素类型的指针。
    由于分配的内存并不是一个数组类型,因此不能对动态数组调用begin和end。这些函数使用数组维度来返回指向首元素和尾元素的指针。处于相同的原因,也不能用范围for语句来处理(所谓的)动态数组中的元素。
    WARNING:要记住我们所说的动态数组并不是数组类型,这是很重要的。
  2. 初始化动态分配对象的数组
    默认情况下,new分配的对象,不管是单个分配的还是数组中的,都是默认初始化的。可以对数组中的元素进行值初始化,方法是在大小之后跟一对空括号。
  3. 释放动态数组
    为了释放动态数组,我们使用一种特殊形式的delete——在指针前加上一个空括号对:

    第二条语句销毁pa指向的数组中的元素,并释放对应的内存。数组中的元素按逆序销毁,即,最后一个元素首先被销毁,然后是倒数第二个,以此类推。
  4. 智能指针和动态数组
    标准库提供了一个可以管理new分配的数组的unique_ptr版本。为了用一个unique_ptr管理动态数组,我们必须在对象类型后面跟一对空方括号

    指向数组的unique_ptr提供的操作与我们在12.1.5节中使用的那些操作有一些不同,我们在表12.6中描述了这些操作。表12.6与unique_ptr不同,shared_ptr不直接支持管理动态数组(shared_ptr无release成员函数)。如果希望使用shared_ptr管理一个动态数组,必须提供自己定义的删除器。

    本例中我们传递给shared_ptr一个lambda作为删除器,它使用delete[]释放数组。
  • allocator类

new有一些灵活性上的局限,其中一方面表现在它将内存分配和对象构造组合在了一起。类似的,delete将对象析构和内存释放组合在了一起。我们分配单个对象是,通常希望将内存分配和对象初始化组合在一起。因为在这种情况下,我们几乎肯定知道对象应有什么值。
一般情况下,将内存分配和对象构造组合在一起可能会导致不必要的浪费:

new表达式分配并初始化了n个string。但是,我们可能不需要n个string;而且对于那些确实要使用的对象,我们也在初始化之后立即赋予了它们新值。每个使用到的元素都被赋值了两次:第一次是在默认初始化时,随后是在赋值时。更重要的是,那些没有默认构造函数的类就不能动态分配数组了。

  1. 标准库allocator类定义在头文件memory中,它帮助我们将内存分配和对象构造分离开来。它提供一种类型感知的内存分配方法,它分配的内存是原始的、未构造的。表12.7类似vector,allocator是一个模板。为了定义一个allocator对象,我们必须指明这个allocator可以分配的对象类型。
  2. allocator分配未构造的内存
    allocator分配的内存是为构造的(unconstructed)。在新标准中,construct成员函数接受一个指针和零个或多个额外参数,在给定位置构造一个元素。额外参数用来初始化构造的对象。类似make_shared的参数,这些额外参数必须是与构造的对象的类型相匹配的合法的初始化器:

WARNING:为了使用allocate返回的内存,我们必须用construct构造对象。使用未构造的内存,其行为是未定义的。

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

一旦元素被销毁后,就可以重新使用这部分内存来保存其他string,也可以将其归还给系统。释放内存通过调用deallocate来完成:

WARNING:我们只能对真正构造了的元素进行destroy操作。

3.拷贝和填充为初始化内存的算法表12.8

作为一个例子,假定有一个int的vector,希望将其内容拷贝到动态内存中。

 

 

《第12章 动态内存》有2个想法

    1. weak_ptr配合shared_ptr使用,它只能跟踪一个共享的资源,但并不实际拥有,也不会阻碍资源被释放。读取共享的资源前需要执行lock,得到shared_ptr之后才能访问。

发表评论

电子邮件地址不会被公开。 必填项已用*标注