参考资料:
C++ 中的容器可以分为 3 类:顺序容器、关联容器、无序关联容器。
所有顺序容器都提供了快速顺序访问的能力,但在以下方面的性能有所不同:
除了固定大小的 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
。本小节介绍的操作对所有容器都适用。
虽然顺序容器几乎可以保存任何元素类型,但某些容器操作需要对元素类型有特殊要求。例如,顺序容器的构造函数的一个版本接受容器大小参数,它使用了元素的默认构造函数,但如果某个类没有默认构造函数,我们可以定义这个类的顺序容器,但不能适用这个版本的构造函数:
// noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10); // 错误
一个迭代器范围(iterator range)由一对迭代器表示,这两个迭代器通常被称为 begin
和 end
,并满足如下要求:
begin
到达 end
。迭代器范围是一个左闭右开区间。
左闭合范围有一些方便的性质:
begin
和 end
相等,则范围为空。begin
和 end
不等,则范围中至少包含一个元素,且 begin
指向范围中的首元素。begin
递增若干次,使得 begin == end
。每个容器都定义了很多类型,如 size_type
、iterator
、const_iterator
。大多数容器还提供反向迭代器,执行 ++
操作会得到上一个元素。
每个容器还定义了很多类型别名,如 value_type
、reference
或 const_reference
。
为了使用这些类型,我们必须显式使用类名:
list<string>::iterator iter;
vector<int>::difference_type count;
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
。
将一个容器初始化为另一个容器的拷贝的方式有两种:直接拷贝整个容器、( 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
支持拷贝、赋值操作,此时要求容器类型、元素类型、元素个数都一样。
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
。
除 forward_list
外,每个容器都支持 size
、empty
、max_size
;forward_list
只支持前两个。
每个容器类型都支持 ==
和 !=
;除无序关联容器外的所有容器都支持 >
、>=
、<
、<=
。关系运算符要求左右两边对象有相同的容器类型。
两个容器的比较实际上是元素的逐对比较:
容器的 ==
和 !=
实际上是使用元素的 ==
实现的,其他运算符是使用元素的 <
实现的。