参考资料:
- 《C++ Primer》第5版
- 《C++ Primer 习题集》第5版
C++ 中的容器可以分为 3 类:顺序容器、关联容器、无序关联容器。
9.1 顺序容器概述(P292)
所有顺序容器都提供了快速顺序访问的能力,但在以下方面的性能有所不同:
- 向容器添加、删除元素
- 非顺序访问容器中的元素
除了固定大小的 array
外,其他容器提供高效、灵活的内存管理。不同的存储管理策略,将会影响容器操作的效率以及是否支持特定操作:
string
和vector
将元素保存在连续的空间中,所以支持随机访问,但在中间位置添加或删除元素就非常耗时。添加一个元素可还需要分配额外的存储空间,此时容器中的每个元素都将移动到新的存储空间中。list
和forward_list
可以快速在任何位置添加或删除元素,但不支持随机访问。这两个容器的额外内存开销也较大。deque
支持快速随机访问,在中间位置添加或删除元素代价高,但在两端添加或删除元素的速度与list
和forward_list
相当。
forward_list
和 array
是 C++ 新标准新添加的类型。array
是比内置数组更安全、更易使用的数组类型;forward_list
的设计目标是达到与最好的手写单向链表相当的性能,所以其没有 size
操作,因为这会产生额外开销。
新标准库的容器性能很高,所以现在 C++ 程序应该更多地使用标准库容器
确定使用哪种顺序容器
选择容器的基本原则:
- 除非有很好的理由选择其他容器,否则选择
vector
。 - 如果你的程序有很多小的元素,且空间开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问元素,使用
vector
或deque
。 - 如果程序要求在中间插入或删除元素,使用
list
或forward_list
。 - 如果程序要求在头尾插入或删除元素,使用
deque
。
9.2 容器库概览(P294)
本小节介绍的操作对所有容器都适用。
对容器可以保存的元素类型的限制
虽然顺序容器几乎可以保存任何元素类型,但某些容器操作需要对元素类型有特殊要求。例如,顺序容器的构造函数的一个版本接受容器大小参数,它使用了元素的默认构造函数,但如果某个类没有默认构造函数,我们可以定义这个类的顺序容器,但不能适用这个版本的构造函数:
// noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10); // 错误
9.2.1 迭代器(P296)
迭代器范围
一个迭代器范围(iterator range)由一对迭代器表示,这两个迭代器通常被称为 begin
和 end
,并满足如下要求:
- 它们指向同一个容器中的元素,或容器的尾后位置。
- 可以通过大于等于 0 次的递增运算,使
begin
到达end
。
迭代器范围是一个左闭右开区间。
适用左闭合范围蕴含的编程假定
左闭合范围有一些方便的性质:
- 如果
begin
和end
相等,则范围为空。 - 如果
begin
和end
不等,则范围中至少包含一个元素,且begin
指向范围中的首元素。 - 我们可以对
begin
递增若干次,使得begin == end
。
9.2.2 容器类型成员97)
每个容器都定义了很多类型,如 size_type
、iterator
、const_iterator
。大多数容器还提供反向迭代器,执行 ++
操作会得到上一个元素。
每个容器还定义了很多类型别名,如 value_type
、reference
或 const_reference
。
为了使用这些类型,我们必须显式使用类名:
list<string>::iterator iter;
vector<int>::difference_type count;
9.2.3 begin
和end
成员(P298)
不以 c
开头的 begin
和 end
函数都是被重载过。例如,实际上存在两个名为 begin
的成员,一个是 const
的,返回 const_iterator
,另一个不是常量,返回 iterator
。
以 c
开头的版本是 C++ 新标准引入的,用以支持 auto
与 begin
和 end
结合使用:
vector<int> vi(5);
auto it1 = a.begin();
auto it2 = a.cbegin();
练习
9.10 :下面四个对象分别是什么类型?
vector<int> v1;
const vector<int> v2;
auto it1 = v1.begin(), it2 = v2.end();
auto it3 = v1.cbegin(), it4 = v2.cend();
答:第一条 auto
语句显然是不正确的,第二条 auto
语句解析出的类型是 vector<int>::const_iterator
。
9.2.4 容器定义和初始化(P299)
将一个容器初始化为另一个容器的拷贝
将一个容器初始化为另一个容器的拷贝的方式有两种:直接拷贝整个容器、( array
除外)拷贝由一对迭代器指定的元素范围。
直接拷贝整个容器时,需保证容器类型和元素类型完全相同;范围拷贝只要求能将被拷贝的元素类型转换成目标类型即可:
list<string> ls = { "hello", "hi", "world" };
vector<const char *> vcc = { "a", "an", "the" };
list<string> ls1(ls); // 正确
deque<string> ds(ls); // 错误,容器类型不相同
vector<string> vs(vcc); // 错误,元素类型不相同
forward_list<string> fls(vcc.begin(), vcc.end()); // 正确
范围初始化用被拷贝容器中的元素对目标容器中的对应元素进行初始化。
列表初始化
在新标准中,我们可以对一个容器进行列表初始化。对于除 array
之外的容器类型外,初始化列表还隐含指定了容器的大小。
与顺序容器大小相关的构造函数
只有顺序容器的构造函数才接受大小参数。
标准库array
具有固定大小
定义一个 array
容器时,除了要指定元素类型外,还要指定容器大小:
array<int, 42> ai;
大小是 array
类型的一部分:
array<int, 10>::size_type i;
array<int>::size_type j; // 错误
与内置数组不同的是,array
支持拷贝、赋值操作,此时要求容器类型、元素类型、元素个数都一样。
9.2.5 赋值和swap
(P302)
赋值操作 c1 = c2
将 c1
中的元素替换为 c2
中元素的拷贝,要求 c1
和 c2
必须具有相同的类型(容器类型、元素类型,array
还额外要求元素数量)。
使用assign
(仅顺序容器)
assign
允许我们从一个不同但相同的类型赋值:
list<string> ls = { "hello", "hi", "world" };
vector<const char *> vcc = { "a", "an", "the" };
ls = vcc; // 错误
ls.assign(vcc.begin(), vcc.end()); // 正确
使用swap
swap
可以交换两个相同类型容器的内容:
vector<string> svec1(10);
vector<string> svec2(20);
swap(svec1, svec2);
单独提起“相同类型容器”,其实就等价于容器类型相同、元素类型相同、元素数量相同(仅
array
)。
除 array
外,swap
并不会真的交换元素本身,所以效率很高,可以在常数时间完成。由于元素不会被移动,除 string
外,指向容器的迭代器、引用、指针在 swap
操作后不会失效,它们仍然指向原来的元素,但这些元素已经不属于原来的容器了。
array
的 swap
操作会真正交换元素,所需时间与元素数量成正比。swap
操作后,迭代器、引用、指针扔指向原来的元素,但元素值已经进行了交换
建议使用非成员版本的
swap
。
9.2.6 容器大小操作(P304)
除 forward_list
外,每个容器都支持 size
、empty
、max_size
;forward_list
只支持前两个。
9.2.7 关系运算符(P304)
每个容器类型都支持 ==
和 !=
;除无序关联容器外的所有容器都支持 >
、>=
、<
、<=
。关系运算符要求左右两边对象有相同的容器类型。
两个容器的比较实际上是元素的逐对比较:
- 逐个比较元素,一旦遇到不相等的情况,就用这对不相等元素的比较结果作为容器的比较结果。
- 如果没有遇到不相等的情况,此时用元素数量的比较结果作为容器的比较结果。
容器的关系运算符使用元素的关系运算符完成
容器的 ==
和 !=
实际上是使用元素的 ==
实现的,其他运算符是使用元素的 <
实现的。