vector
stack
12.3 - std::vector的容量和类栈行为
Key Takeaway
在 11.17 — 动态数组 std::vector 简介 中我们介绍了 std::vector
以及如何将其当做动态数组使用(可以记录自身长度也可以根据需要调整大小)。
尽管这是 std::vector
最常用的功能,std::vector
还有一些其他的其他的属性和功能使其在别的方面也很有用。
长度和容量
考虑下面的例子:
int * array { new int [ 10 ] { 1 , 2 , 3 , 4 , 5 } };
我们会说这个数组的长度是10,即使我们只使用了分配的元素中的5个。
但是,如果我们只想遍历已经初始化的元素,而保留未使用的元素以备将来扩展呢?在这种情况下,我们需要分别跟踪“使用”了多少元素和分配了多少元素。与只记住其长度的内置数组或std::array
不同,std::vector
包含两个独立的属性:长度和容量。在std::vector
的语境中,length 是数组中使用的元素数量,而capacity 是内存中分配的元素数量。
让我们再看一下之前的一个std::vector
的一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 #include <vector>
#include <iostream>
int main ()
{
std :: vector array { 0 , 1 , 2 };
array . resize ( 5 ); // set length to 5
std :: cout << "The length is: " << array . size () << '\n' ;
for ( auto element : array )
std :: cout << element << ' ' ;
return 0 ;
};
The length is: 5
0 1 2 0 0
在上面的例子中,我们使用了resize()
函数将vector的长度设置为5。这告诉变量数组我们打算使用数组的前5个元素,所以它应该把这些元素当做活动的。然而,这带来了一个有趣的问题:这个数组的容量是多少?
我们可以通过capacity()
函数查询std::vector
的容量:
#include <vector>
#include <iostream>
int main ()
{
std :: vector array { 0 , 1 , 2 };
array . resize ( 5 ); // set length to 5
std :: cout << "The length is: " << array . size () << '\n' ;
std :: cout << "The capacity is: " << array . capacity () << '\n' ;
}
在笔者的电脑上,输出如下:
The length is: 5
The capacity is: 5
在本例中,resize()
函数使std::vector
改变了它的长度和容量。注意,容量保证至少与数组长度一样大(但也可以更大),否则访问数组末尾的元素将在分配的内存之外!
长度和容量——更多细节
为什么要区分长度和容量?vector会在需要的时候重新分配它的内存,但是它希望尽量避免重新分配,因为调整数组的大小计算开销很大。考虑下面代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14 #include <vector>
#include <iostream>
int main ()
{
std :: vector array {};
array = { 0 , 1 , 2 , 3 , 4 }; // okay, array length = 5
std :: cout << "length: " << array . size () << " capacity: " << array . capacity () << '\n' ;
array = { 9 , 8 , 7 }; // okay, array length is now 3!
std :: cout << "length: " << array . size () << " capacity: " << array . capacity () << '\n' ;
return 0 ;
}
打印结果:
length: 5 capacity: 5
length: 3 capacity: 5
注意,虽然我们为vector分配了一个更小的数组,但它并没有重新分配它的内存(容量仍然是5)。它只是更改了它的长度,因此它知道此时只有前3个元素有效。
数组下标和 at()
基于长度而非容量
下标运算符和at()
函数是基于vector的长度而非容量工作的。对于前例中的array,它的长度是3,容量是5。那么如果我们访问索引4的元素会发生什么呢?结果显然是会访问失败,因为4已经超过了数组的长度。
注意:vector并不会因为下标运算符和at()
的调用而调整大小!
std::vector
的类栈行为
如果下标操作符和at()
函数是基于数组长度的,并且容量总是至少与数组长度一样大,为什么还要担心容量呢?虽然std::vector
可以用作动态数组,但它也可以用作堆栈。为此,我们可以使用3个与栈操作相匹配的函数:
push_back()
:元素压栈;
back(
) :返回栈顶元素;
pop_back()
:从栈中弹出元素。
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
38 #include <iostream>
#include <vector>
void printStack ( const std :: vector < int >& stack )
{
for ( auto element : stack )
std :: cout << element << ' ' ;
std :: cout << "(cap " << stack . capacity () << " length " << stack . size () << ") \n " ;
}
int main ()
{
std :: vector < int > stack {};
printStack ( stack );
stack . push_back ( 5 ); // push_back() 压栈
printStack ( stack );
stack . push_back ( 3 );
printStack ( stack );
stack . push_back ( 2 );
printStack ( stack );
std :: cout << "top: " << stack . back () << '\n' ; // back() 返回最后一个元素
stack . pop_back (); // pop_back() 弹出元素
printStack ( stack );
stack . pop_back ();
printStack ( stack );
stack . pop_back ();
printStack ( stack );
return 0 ;
}
COPY
This prints:
(cap 0 length 0)
5 (cap 1 length 1)
5 3 (cap 2 length 2)
5 3 2 (cap 3 length 3)
top: 2
5 3 (cap 3 length 2)
5 (cap 3 length 1)
(cap 3 length 0)
和下标运算符和 at()
不同,栈操作函数会在需要时调整 std::vector
的大小,在这个例子中。vector 的大小被调整了3次(capacity 从 0 到 1, 1 到 2,2 到 3)。
因为调整vector的大小代价很大,所以我们可以使用reserve()
函数预先告诉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
38
39
40 #include <vector>
#include <iostream>
void printStack ( const std :: vector < int >& stack )
{
for ( auto element : stack )
std :: cout << element << ' ' ;
std :: cout << "(cap " << stack . capacity () << " length " << stack . size () << ") \n " ;
}
int main ()
{
std :: vector < int > stack {};
stack . reserve ( 5 ); // 将容量设置为5
printStack ( stack );
stack . push_back ( 5 );
printStack ( stack );
stack . push_back ( 3 );
printStack ( stack );
stack . push_back ( 2 );
printStack ( stack );
std :: cout << "top: " << stack . back () << '\n' ;
stack . pop_back ();
printStack ( stack );
stack . pop_back ();
printStack ( stack );
stack . pop_back ();
printStack ( stack );
return 0 ;
}
程序输出:
(cap 5 length 0)
5 (cap 5 length 1)
5 3 (cap 5 length 2)
5 3 2 (cap 5 length 3)
top: 2
5 3 (cap 5 length 2)
5 (cap 5 length 1)
(cap 5 length 0)
我们可以看到容量被预设为5,并且在程序的生命周期中没有变化。
Vectors 可能会分配多余的容量
当调整一个vector的大小时,该vector可能会分配比所需更多的容量。这样做是为了为其他元素提供一些“喘息的空间”,从而最小化所需的调整操作的数量。让我们来看看这个:
1
2
3
4
5
6
7
8
9
10
11
12
13 #include <vector>
#include <iostream>
int main ()
{
std :: vector v { 0 , 1 , 2 , 3 , 4 };
std :: cout << "size: " << v . size () << " cap: " << v . capacity () << '\n' ;
v . push_back ( 5 ); // add another element
std :: cout << "size: " << v . size () << " cap: " << v . capacity () << '\n' ;
return 0 ;
}
在笔者机器上打印:
size: 5 cap: 5
size: 6 cap: 7
当我们使用push_back()
添加一个新元素时,我们的向量只需要6个元素的空间,但分配了7个空间。这样做的目的是,如果我们要push_back()
另一个元素,它不需要立即调整大小。
是否、何时以及分配多少额外的容量取决于编译器实现者。