章节目录

std::vector大小调整和容量

本节阅读量:

在本章前面的课程中,我们介绍了容器、数组和std::vector。还讨论了一些主题,例如如何访问数组元素、获取数组长度以及如何遍历数组。虽然示例中使用的是std::vector,但讨论的概念通常适用于所有数组类型。

在本章的其余课程中,我们将重点关注让std::vector与大多数其他数组类型显著不同的一点:实例化后调整自身大小的能力。


固定大小数组与动态大小数组

大多数数组类型都有一个显著限制:数组长度必须在实例化时已知,并且之后不能更改。这称为固定大小数组或固定长度数组。array和C样式数组都是固定大小的数组类型。下一章会进一步讨论这些问题。

另一方面,std::vector是动态数组。动态数组(也称为可调整大小的数组)的大小可以在实例化后更改。这种调整大小的能力让std::vector变得特别。


在运行时调整std::vector的大小

通过调用resize()成员函数并传入所需长度,可以在实例化后调整std::vector的大小:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <vector>

int main()
{
    std::vector v{ 0, 1, 2 }; // 有 3 个元素
    std::cout << "The length is: " << v.size() << '\n';

    v.resize(5);              // resize 到 5 个元素
    std::cout << "The length is: " << v.size() << '\n';

    for (auto i : v)
        std::cout << i << ' ';

    std::cout << '\n';

    return 0;
}

这将打印:

1
2
3
The length is: 3
The length is: 5
0 1 2 0 0

这里有两件事需要注意。首先,调整vector大小时,现有元素值会被保留!其次,新元素会被值初始化(对类类型执行默认初始化,对其他类型执行零初始化)。因此,int类型的两个新元素会被零初始化为值0。

vector也可以调整为更小:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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 }; // 初始长度 5
    printLength(v);

    v.resize(3);                    // resize 为 3
    printLength(v);

    for (int i : v)
        std::cout << i << ' ';

    std::cout << '\n';

    return 0;
}

这将打印:

1
2
3
The length is: 5
The length is: 3
0 1 2

长度与容量

考虑一排12栋房子。我们会说房子的数量(或一排房子的长度)是12。如果我们想知道这些房子中的哪些正在被占用……我们必须以其他方式确定这一点(例如,按门铃,看看是否有人回答)。当我们只有长度时,我们只知道存在多少东西。

现在考虑一盒鸡蛋,其中目前有5个鸡蛋。我们会说,鸡蛋的个数是5。但在这种情况下,还有一个我们关心的维度:如果盒子装满,它可以装多少鸡蛋。我们可以说一盒鸡蛋的容量是12个。盒子有容纳12个鸡蛋的空间,并且只有5个正在使用,因此还可以再添加7个鸡蛋,而不会溢出盒子。当同时有长度和容量时,我们可以区分当前有多少东西,也可以区分总共有多少空间。

到目前为止,我们只讨论了std::vector的长度。但std::vector也有容量。在std::vector的上下文中,容量表示std::vector为多少个元素分配了存储空间,长度表示当前正在使用的元素数。

容量为5的std::vector为5个元素分配了空间。如果其中有2个正在使用的元素,则vector的长度(大小)为2。剩余3个元素的内存已经分配,但它们不被视为正在使用。它们可以在以后使用,而不会溢出。


获取std::vector的容量

可以通过capacity()成员函数查看std::vector的容量。

例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#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 }; // 长度为 3

    printCapLen(v);

    for (auto i : v)
        std::cout << i << ' ';
    std::cout << '\n';

    v.resize(5); // resize 为 5 

    printCapLen(v);

    for (auto i : v)
        std::cout << i << ' ';
    std::cout << '\n';

    return 0;
}

在作者的机器上,这将打印以下内容:

1
2
3
4
Capacity: 3  Length: 3
0 1 2
Capacity: 5  Length: 5
0 1 2 0 0

首先,我们用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更改其管理的存储量时,这个过程称为重新分配。非正式地,重新分配过程如下:

  1. vector获取所需数量的元素容量的新内存。这些元素是值初始化的。
  2. 将旧内存中的元素复制(或移动,如果可能)到新内存中。然后将旧内存返回给系统。
  3. 将std::vector的容量和长度设置为新值。

从外部看,std::vector似乎只是调整了大小。但在内部,内存(和所有元素)实际上已经被替换!

由于重新分配通常需要复制数组中的每个元素,因此它是一个昂贵的过程。所以,我们希望尽可能避免重新分配。


为什么区分长度和容量?

如果需要,std::vector会重新分配其存储,但通常不希望这样做,因为重新分配存储的计算开销很大。

如果std::vector只保存长度,则每个resize()请求都会导致针对新长度的昂贵重新分配。区分长度和容量,可以让std::vector更聪明地判断何时需要重新分配。

下面的示例对此进行了说明:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>
#include <vector>

void printCapLen(const std::vector<int>& v)
{
	std::cout << "Capacity: " << v.capacity() << " Length:"	<< v.size() << '\n';
}

int main()
{
    // 初始长度为 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 为 3 个元素
    v.resize(3);
    printCapLen(v);

    for (auto i : v)
        std::cout << i << ' ';
    std::cout << '\n';

    // 再 Resize 回 5 个元素
    v.resize(5);
    printCapLen(v);

    for (auto i : v)
        std::cout << i << ' ';
    std::cout << '\n';

    return 0;
}

这会产生以下结果:

1
2
3
4
5
6
Capacity: 5  Length: 5
0 1 2 3 4 
Capacity: 5  Length: 3
0 1 2 
Capacity: 5  Length: 5
0 1 2 0 0

当我们用5个元素初始化vector时,容量被设置为5,这表示vector最初为5个元素分配了空间。长度也设置为5,表示所有这些元素都在使用中。

调用v.resize(3) 之后,长度被更改为3,以满足较小数组的请求。然而,请注意容量仍然是5,这意味着vector没有进行重新分配!

最后,调用v.resize(5)。因为vector已经具有5的容量,所以不需要重新分配。它只是将长度改回5,并对最后两个元素进行值初始化。

通过分离长度和容量,本例避免了原本可能发生的2次重新分配。下一课中,我们将看到逐个向vector添加元素的示例。在这种情况下,不在每次长度变化时重新分配的能力更加重要。


vector索引基于长度,而不是容量

您可能会惊讶地发现,下标操作符(操作符[])和at()成员函数的有效索引基于vector的长度,而不是容量。

在上例中,当v具有容量5和长度3时,只有0到2的索引有效。即使vector为索引3到4的位置分配了存储,它们的索引仍被认为是越界的。


收缩std::vector

将vector调整为更大的大小会增加vector的长度,并在需要时增加其容量。然而,将vector调整为更小只会减少其长度,而不会减少其容量。

大量减少vector中的元素却不回收内存,并不是理想选择。当vector包含大量不再需要的元素时,内存浪费可能相当明显。

为了解决这种情况,std::vector有一个名为shrink_to_fit()的成员函数,该函数请求vector收缩其容量以匹配其长度。这个请求不是强制的,这意味着具体编译器实现可以自由忽略它。根据实现认为最合适的方式,它可以选择满足请求、部分满足请求(例如减少容量,但不完全匹配长度),或者完全忽略请求。

下面是一个示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#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); // 分配 1000 个元素
	printCapLen(v);

	v.resize(0); // resize 为 0
	printCapLen(v);

	v.shrink_to_fit();
	printCapLen(v);

	return 0;
}

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

1
2
3
Capacity: 1000  Length: 1000
Capacity: 1000  Length: 0
Capacity: 0  Length: 0

可以看到,当调用v.shrink_to_fit()时,vector将其容量重新分配为0,从而释放了1000个元素的内存。


16.8 使用枚举值来作为数组索引

上一节

16.10 std::vector和栈行为

下一节