std::vector大小调整和容量
本节阅读量:在本章前面的课程中,我们介绍了容器、数组和std::vector。还讨论了一些主题,例如如何访问数组元素、获取数组长度以及如何遍历数组。虽然示例中使用的是std::vector,但讨论的概念通常适用于所有数组类型。
在本章的其余课程中,我们将重点关注让std::vector与大多数其他数组类型显著不同的一点:实例化后调整自身大小的能力。
固定大小数组与动态大小数组
大多数数组类型都有一个显著限制:数组长度必须在实例化时已知,并且之后不能更改。这称为固定大小数组或固定长度数组。array和C样式数组都是固定大小的数组类型。下一章会进一步讨论这些问题。
另一方面,std::vector是动态数组。动态数组(也称为可调整大小的数组)的大小可以在实例化后更改。这种调整大小的能力让std::vector变得特别。
在运行时调整std::vector的大小
通过调用resize()成员函数并传入所需长度,可以在实例化后调整std::vector的大小:
|
|
这将打印:
|
|
这里有两件事需要注意。首先,调整vector大小时,现有元素值会被保留!其次,新元素会被值初始化(对类类型执行默认初始化,对其他类型执行零初始化)。因此,int类型的两个新元素会被零初始化为值0。
vector也可以调整为更小:
|
|
这将打印:
|
|
长度与容量
考虑一排12栋房子。我们会说房子的数量(或一排房子的长度)是12。如果我们想知道这些房子中的哪些正在被占用……我们必须以其他方式确定这一点(例如,按门铃,看看是否有人回答)。当我们只有长度时,我们只知道存在多少东西。
现在考虑一盒鸡蛋,其中目前有5个鸡蛋。我们会说,鸡蛋的个数是5。但在这种情况下,还有一个我们关心的维度:如果盒子装满,它可以装多少鸡蛋。我们可以说一盒鸡蛋的容量是12个。盒子有容纳12个鸡蛋的空间,并且只有5个正在使用,因此还可以再添加7个鸡蛋,而不会溢出盒子。当同时有长度和容量时,我们可以区分当前有多少东西,也可以区分总共有多少空间。
到目前为止,我们只讨论了std::vector的长度。但std::vector也有容量。在std::vector的上下文中,容量表示std::vector为多少个元素分配了存储空间,长度表示当前正在使用的元素数。
容量为5的std::vector为5个元素分配了空间。如果其中有2个正在使用的元素,则vector的长度(大小)为2。剩余3个元素的内存已经分配,但它们不被视为正在使用。它们可以在以后使用,而不会溢出。
关键点
vector的长度是“正在使用”的元素数。vector的容量是内存中分配了多少个元素的空间。
获取std::vector的容量
可以通过capacity()成员函数查看std::vector的容量。
例如:
|
|
在作者的机器上,这将打印以下内容:
|
|
首先,我们用3个元素初始化vector。这会导致vector为3个元素分配存储空间(容量为3),并且所有3个元素都被认为正在使用(长度=3)。
然后我们调用resize(5),这意味着现在需要一个长度为5的vector。由于vector只有3个元素的存储空间,但需要5个,因此vector需要获得更多存储空间来容纳额外元素。
在resize()调用完成后,可以看到vector现在有5个元素的空间(容量为5),并且所有5个元素都被认为正在使用(长度为5)。
在大多数情况下,您不需要使用capacity()函数,但下面的示例会大量使用它,以便观察vector的存储发生了什么。
存储的重新分配,以及它为什么昂贵
当std::vector更改其管理的存储量时,这个过程称为重新分配。非正式地,重新分配过程如下:
- vector获取所需数量的元素容量的新内存。这些元素是值初始化的。
- 将旧内存中的元素复制(或移动,如果可能)到新内存中。然后将旧内存返回给系统。
- 将std::vector的容量和长度设置为新值。
从外部看,std::vector似乎只是调整了大小。但在内部,内存(和所有元素)实际上已经被替换!
由于重新分配通常需要复制数组中的每个元素,因此它是一个昂贵的过程。所以,我们希望尽可能避免重新分配。
关键点
重新分配通常很昂贵。避免不必要的重新分配。
为什么区分长度和容量?
如果需要,std::vector会重新分配其存储,但通常不希望这样做,因为重新分配存储的计算开销很大。
如果std::vector只保存长度,则每个resize()请求都会导致针对新长度的昂贵重新分配。区分长度和容量,可以让std::vector更聪明地判断何时需要重新分配。
下面的示例对此进行了说明:
|
|
这会产生以下结果:
|
|
当我们用5个元素初始化vector时,容量被设置为5,这表示vector最初为5个元素分配了空间。长度也设置为5,表示所有这些元素都在使用中。
调用v.resize(3) 之后,长度被更改为3,以满足较小数组的请求。然而,请注意容量仍然是5,这意味着vector没有进行重新分配!
最后,调用v.resize(5)。因为vector已经具有5的容量,所以不需要重新分配。它只是将长度改回5,并对最后两个元素进行值初始化。
通过分离长度和容量,本例避免了原本可能发生的2次重新分配。下一课中,我们将看到逐个向vector添加元素的示例。在这种情况下,不在每次长度变化时重新分配的能力更加重要。
关键点
长度与容量分开的能力允许std::vector在长度更改时避免一些重新分配。
vector索引基于长度,而不是容量
您可能会惊讶地发现,下标操作符(操作符[])和at()成员函数的有效索引基于vector的长度,而不是容量。
在上例中,当v具有容量5和长度3时,只有0到2的索引有效。即使vector为索引3到4的位置分配了存储,它们的索引仍被认为是越界的。
警告
下标仅在0和vector的长度(而不是其容量)之间有效!
收缩std::vector
将vector调整为更大的大小会增加vector的长度,并在需要时增加其容量。然而,将vector调整为更小只会减少其长度,而不会减少其容量。
大量减少vector中的元素却不回收内存,并不是理想选择。当vector包含大量不再需要的元素时,内存浪费可能相当明显。
为了解决这种情况,std::vector有一个名为shrink_to_fit()的成员函数,该函数请求vector收缩其容量以匹配其长度。这个请求不是强制的,这意味着具体编译器实现可以自由忽略它。根据实现认为最合适的方式,它可以选择满足请求、部分满足请求(例如减少容量,但不完全匹配长度),或者完全忽略请求。
下面是一个示例:
|
|
在作者的机器上,这会产生以下结果:
|
|
可以看到,当调用v.shrink_to_fit()时,vector将其容量重新分配为0,从而释放了1000个元素的内存。