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;
}

这将输出:

image

这里有两点需要注意。首先,当我们调整向量大小时,现有元素的值被保留了!其次,新元素会进行值初始化(对于类类型执行默认初始化,对于其他类型执行零初始化)。因此,两个新添加的 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;
}

这将输出:

image


标准容器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;
}

在作者的机器上,这段代码输出如下:

image

首先,我们用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;
}

这将产生以下结果:

image

当我们用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;
}

在作者的机器上,这会产生以下结果:

image

如您所见,当调用 v.shrink_to_fit() 时,向量将其容量重新分配为 0,从而释放了 1000 个元素的内存空间。


测验时间

问题 #1

std::vector 的 length 和 capacity 分别代表什么?

显示解答

长度表示当前正在使用的元素数量。
容量表示已分配存储空间的元素数量。

为什么 length 和 capacity 是两个独立的值?

显示解答

容量会单独进行追踪,这样当长度改变时,向量就能避免部分重新分配操作。

std::vector 的有效索引是基于 length 还是 capacity?

显示解答

Length.
posted @ 2026-01-07 16:13  游翔  阅读(53)  评论(0)    收藏  举报