16-10 std::vector 的动态调整大小与容量
在本章之前的课程中,我们介绍了容器、数组和std::vector。还讨论了如何访问数组元素、获取数组长度以及遍历数组等主题。虽然示例中使用了std::vector,但所讨论的概念普遍适用于所有数组类型。
在本章剩余课程中,我们将重点探讨使std::vector与其他数组类型显著不同的特性:实例化后能够动态调整自身大小的能力。
固定大小数组与动态数组
大多数数组类型存在显著限制:数组长度必须在实例化时确定,且无法更改。此类数组称为固定大小数组fixed-size arrays或固定长度数组fixed-length arrays。std::array和C风格数组均属于固定大小数组类型,我们将在下一章深入探讨。
另一方面,std::vector属于动态数组。动态数组dynamic array(亦称可调整大小数组resizable array)可在实例化后改变其大小。这种可调整特性正是std::vector的独特之处。
运行时调整std::vector的大小
std::vector实例化后可通过调用resize()成员函数并传入新的目标长度来调整大小:
#include <iostream>
#include <vector>
int main()
{
std::vector v{ 0, 1, 2 }; // create vector with 3 elements
std::cout << "The length is: " << v.size() << '\n';
v.resize(5); // resize to 5 elements
std::cout << "The length is: " << v.size() << '\n';
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
这将输出:

这里有两点需要注意。首先,当我们调整向量大小时,现有元素的值被保留了!其次,新元素会进行值初始化(对于类类型执行默认初始化,对于其他类型执行零初始化)。因此,两个新添加的 int 类型元素被初始化为 0。
向量也可以缩小尺寸:
#include <iostream>
#include <vector>
void printLength(const std::vector<int>& v)
{
std::cout << "The length is: " << v.size() << '\n';
}
int main()
{
std::vector v{ 0, 1, 2, 3, 4 }; // length is initially 5
printLength(v);
v.resize(3); // resize to 3 elements
printLength(v);
for (int i : v)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
这将输出:

标准容器std::vector的长度与容量
设想一排12栋房屋。我们会说房屋数量(或房屋排的长度)是12。若想知道其中哪些房屋有人居住...则需通过其他方式确认(例如按门铃查看是否有人应答)。仅有长度时,我们仅知晓事物存在的数量。
现在考虑一个装有5枚鸡蛋的纸盒。我们会说鸡蛋数量为5枚。但在这个情境中,我们还关注另一个维度:纸盒满载时能容纳多少鸡蛋。我们会说这个鸡蛋盒的容量是12。盒子能容纳12个鸡蛋,目前只用了5个——因此还能再放7个鸡蛋而不溢出。当同时拥有长度和容量时,我们就能区分当前存在的事物数量与空间容纳量。
至此我们仅讨论了std::vector的长度。但该容器同样存在容量概念:在std::vector语境中,容量capacity指容器预留存储空间的元素数量,长度length则指当前实际使用的元素数量。
容量为5的std::vector已为5个元素分配空间。若该向量中有2个元素处于活动使用状态,则向量的长度(大小)为2。剩余3个元素虽已分配内存,但不被视为活动使用状态,后续可继续使用这些空间而不会导致向量溢出。
核心要点:
向量的长度表示“正在使用”的元素数量。
向量的容量表示内存中已分配的元素数量。
获取 std::vector 的容量
我们可以使用 capacity() 成员函数查询 std::vector 的容量。
例如:
#include <iostream>
#include <vector>
void printCapLen(const std::vector<int>& v)
{
std::cout << "Capacity: " << v.capacity() << " Length:" << v.size() << '\n';
}
int main()
{
std::vector v{ 0, 1, 2 }; // length is initially 3
printCapLen(v);
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
v.resize(5); // resize to 5 elements
printCapLen(v);
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
在作者的机器上,这段代码输出如下:

首先,我们用3个元素初始化了向量。这导致向量为3个元素分配存储空间(容量为3),且所有3个元素都被视为处于活动使用状态(长度=3)。
随后调用 resize(5),表示需要将向量长度扩展至5。由于当前向量仅有3个元素的存储空间,而新需求为5个元素,因此向量需要获取更多存储空间来容纳新增元素。
调用 resize() 完成后,可见向量现可容纳 5 个元素(容量为 5),且所有 5 个元素均被视为处于使用状态(长度为 5)。
多数情况下无需使用 capacity() 函数,但后续示例中我们将频繁调用该函数,以便清晰观察向量存储空间的变化过程。
存储重新分配及其高成本原因
当 std::vector 改变其管理的存储量时,该过程称为重新分配。非正式地讲,重新分配过程大致如下:
- std::vector 获取具有所需元素数量容量的新内存。这些元素将进行值初始化。
- 旧内存中的元素被复制(或在可能时移动)到新内存中。旧内存随后被释放回系统。
- std::vector的容量和长度被设置为新值。
从外部看,std::vector似乎只是调整了大小。但实际上,其内部存储空间(及所有元素)已被完全替换!
相关内容:
运行时获取新内存的过程称为动态内存分配。我们在第19.1课——使用new和delete进行动态内存分配中对此进行了讲解。
由于重新分配通常需要复制数组中的每个元素,因此重新分配是一个代价高昂的过程。因此,我们应尽可能避免重新分配。
关键要点:
重新分配通常代价高昂。避免不必要的重新分配。
为何区分长度与容量?
std::vector会在需要时重新分配存储空间,但如同梅尔维尔笔下的巴特比,它宁愿不这样做——因为重新分配存储空间会消耗大量计算资源。
若 std::vector 仅记录长度,每次 resize() 请求都会导致耗费资源的重新分配。分离长度与容量使 std::vector 能更智能地判断何时需要重新分配。
以下示例说明了这一点:
#include <iostream>
#include <vector>
void printCapLen(const std::vector<int>& v)
{
std::cout << "Capacity: " << v.capacity() << " Length:" << v.size() << '\n';
}
int main()
{
// Create a vector with length 5
std::vector v{ 0, 1, 2, 3, 4 };
v = { 0, 1, 2, 3, 4 }; // okay, array length = 5
printCapLen(v);
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
// Resize vector to 3 elements
v.resize(3); // we could also assign a list of 3 elements here
printCapLen(v);
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
// Resize vector back to 5 elements
v.resize(5);
printCapLen(v);
for (auto i : v)
std::cout << i << ' ';
std::cout << '\n';
return 0;
}
这将产生以下结果:

当我们用5个元素初始化向量时,容量被设置为5,这意味着向量最初为5个元素分配了空间。长度也被设置为5,表示所有元素均被占用。
调用v.resize(3)后,长度变为3以满足缩小数组的需求。但请注意容量仍为5,这意味着向量并未重新分配内存!
最后调用v.resize(5)时,由于向量容量已为5,无需重新分配内存。它仅将长度改回5,并对最后两个元素进行值初始化。
通过分离长度与容量,本例避免了本应发生的两次重新分配。下节课我们将看到逐个向向量添加元素的示例,此时长度变更时无需每次重新分配的能力就显得尤为重要。
核心要点:
将容量与长度分开追踪,使 std::vector 在长度变更时能够避免部分重新分配操作。
向量索引基于长度而非容量
您可能会惊讶地发现,下标运算符(operator[])和 at() 成员函数的有效索引基于向量的长度,而非容量。
在上例中,当v的容量为5且长度为3时,仅索引0和2有效。即使存在索引介于长度3(含)与容量5(不含)之间的元素,其索引仍被视为越界。
警告:
下标仅在0到向量长度(而非容量)之间时才有效!
缩小 std::vector
将向量调整为更大尺寸会增加其长度,并在必要时增加其容量。然而,将向量调整为更小尺寸只会减少其长度,而不会改变其容量。
仅为回收少量不再需要的元素而重新分配向量内存并非明智之举。但当向量包含大量不再需要的元素时,内存浪费可能相当严重。
为解决此问题,std::vector 提供名为 shrink_to_fit() 的成员函数,用于请求向量缩减容量以匹配其长度。该请求不具强制性,即实现可自由选择忽略。根据具体实现方案的优化策略,可能完全满足请求、部分满足(例如缩减容量但不完全清空),或完全忽略请求。
示例如下:
#include <iostream>
#include <vector>
void printCapLen(const std::vector<int>& v)
{
std::cout << "Capacity: " << v.capacity() << " Length:" << v.size() << '\n';
}
int main()
{
std::vector<int> v(1000); // allocate room for 1000 elements
printCapLen(v);
v.resize(0); // resize to 0 elements
printCapLen(v);
v.shrink_to_fit();
printCapLen(v);
return 0;
}
在作者的机器上,这会产生以下结果:

如您所见,当调用 v.shrink_to_fit() 时,向量将其容量重新分配为 0,从而释放了 1000 个元素的内存空间。
测验时间
问题 #1
std::vector 的 length 和 capacity 分别代表什么?
显示解答
长度表示当前正在使用的元素数量。
容量表示已分配存储空间的元素数量。
为什么 length 和 capacity 是两个独立的值?
显示解答
容量会单独进行追踪,这样当长度改变时,向量就能避免部分重新分配操作。
std::vector 的有效索引是基于 length 还是 capacity?
显示解答
Length.

浙公网安备 33010602011771号