序列容器
string
string 是模板 basic_string 对于 char 类型的特化,可以认为是一个只存放字符 char 类型数据的容器。“真正”的容器类与 string 的最大不同点是里面可以存放任意类型的对象。
string 具有下列成员函数:
- begin 可以得到对象起始点
- end 可以得到对象的结束点
- empty 可以得到容器是否为空
- size 可以得到容器的大小
- swap 可以和另外一个容器交换其内容
不管是内存布局,还是成员函数,string 和 vector 是非常相似的。
string 当然是为了存放字符串。和简单的 C 字符串不同:
- string 负责自动维护字符串的生命周期
- string 支持字符串的拼接操作(如之前说过的 + 和 +=)
- string 支持字符串的查找操作(如 find 和 rfind)
- string 支持从 istream 安全地读入字符串(使用 getline)
- string 支持给期待 const char* 的接口传递字符串内容(使用 c_str)
- string 支持到数字的互转(stoi 系列函数和 to_string)
- 等等
一般不建议在接口中使用 const string&,除非确知调用者已经持有 string:如果函数里不对字符串做复杂处理的话,使用 const char* 可以避免在调用者只有 C 字符串时编译器自动构造 string,这种额外的构造和析构代价并不低。反过来,如果实现较为复杂、希望使用 string 的成员函数的话,那就应该考虑下面的策略:
- 如果不修改字符串的内容,使用 const string& 或 C++17 的 string_view 作为参数类型。后者是最理想的情况,因为即使在只有 C 字符串的情况,也不会引发不必要的内存复制。
- 如果需要在函数内修改字符串内容、但不影响调用者的该字符串,使用 string 作为参数类型(自动拷贝)。
- 如果需要改变调用者的字符串内容,使用 string& 作为参数类型(通常不推荐)。
vector
动态数组,它基本相当于 Java 的 ArrayList 和 Python 的 list。
和 string 相似,vector 的成员在内存里连续存放,同时 begin、end、front、back 成员函数指向的位置也和 string 一样,大致如下图所示:
所有容器的共同点
- 容器都有开始和结束点
- 容器会记录其状态是否非空
- 容器有大小
- 容器支持交换
除了容器类的共同点,vector 允许下面的操作(不完全列表):
- 可以使用中括号的下标来访问其成员(同 string)
- 可以使用 data 来获得指向其内容的裸指针(同 string)
- 可以使用 capacity 来获得当前分配的存储空间的大小,以元素数量计(同 string)
- 可以使用 reserve 来改变所需的存储空间的大小,成功后 capacity() 会改变(同 string)
- 可以使用 resize 来改变其大小,成功后 size() 会改变(同 string)
- 可以使用 pop_back 来删除最后一个元素(同 string)
- 可以使用 push_back 在尾部插入一个元素(同 string)
- 可以使用 insert 在指定位置前插入一个元素(同 string)
- 可以使用 erase 在指定位置删除一个元素(同 string)
- 可以使用 emplace 在指定位置构造一个元素
- 可以使用 emplace_back 在尾部新构造一个元素
当 push_back、insert、reserve、resize 等函数导致内存重分配时,或当 insert、erase 导致元素位置移动时,vector 会试图把元素“移动”到新的内存区域。vector 通常保证强异常安全性,如果元素类型没有提供一个保证不抛异常的移动构造函数,vector 通常会使用拷贝构造函数。因此,对于拷贝代价较高的自定义元素类型,我们应当定义移动构造函数,并标其为 noexcept,或只在容器中放置对象的智能指针。
对于 vector 里的内容,结果是一样的;但使用 push_back 会额外生成临时对象,多一次(移动或拷贝)构造和析构。如果是移动的情况,那会有小幅性能损失;如果对象没有实现移动的话,那性能差异就可能比较大了。
deque
deque 的意思是 double-ended queue,双端队列。它主要是用来满足下面这个需求:
- 容器不仅可以从尾部自由地添加和删除元素,也可以从头部自由地添加和删除。
deque 的接口和 vector 相比,有如下的区别: - deque 提供 push_front、emplace_front 和 pop_front 成员函数。
- deque 不提供 data、capacity 和 reserve 成员函数。
- 如果只从头、尾两个位置对 deque 进行增删操作的话,容器里的对象永远不需要移动。
- 容器里的元素只是部分连续的(因而没法提供 data 成员函数)。
- 由于元素的存储大部分仍然连续,它的遍历性能是比较高的。
- 由于每一段存储大小相等,deque 支持使用下标访问容器元素,大致相当于 index[i / chunk_size][i % chunk_size],也保持高效。
list
list 在 C++ 里代表双向链表。和 vector 相比,它优化了在容器中间的插入和删除:
- list 提供高效的、O(1) 复杂度的任意位置的插入和删除操作。
- list 不提供使用下标访问其元素。
- list 提供 push_front、emplace_front 和 pop_front 成员函数(和 deque 相同)。
- list 不提供 data、capacity 和 reserve 成员函数(和 deque 相同)。
虽然 list 提供了任意位置插入新元素的灵活性,但由于每个元素的内存空间都是单独分配、不连续,它的遍历性能比 vector 和 deque 都要低。
forward_list
从 C++11 开始,前向列表 forward_list 成了标准的一部分。
对于 forward_list,没有从指定的位置之前插入一个元素。标准库提供了一个 insert_after 作为替代。此外,它跟 list 相比还缺了下面这些成员函数:
- back
- size
- push_back
- emplace_back
- pop_back
queue
先进先出(FIFO)的数据结构。
queue 缺省用 deque 来实现。它的接口跟 deque 比,有如下改变:
- 不能按下标访问元素
- 没有 begin、end 成员函数
- 用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_front;没有其他的 push_…、pop_…、emplace…、insert、erase 函数
stack
栈 stack 是后进先出(LIFO)的数据结构。
stack 缺省也是用 deque 来实现,但它的概念和 vector 更相似。它的接口跟 vector 比,有如下改变:
- 不能按下标访问元素
- 没有 begin、end 成员函数
- back 成了 top,没有 front
- 用 emplace 替代了 emplace_back,用 push 替代了 push_back,用 pop 替代了 pop_back;没有其他的 push_…、pop_…、emplace…、insert、erase 函数
priority_queue
priority_queue 也是一个容器适配器。和其他容器适配器不同是在于它用到了比较函数对象(默认是 less)。它和 stack 相似,支持 push、pop、top 等有限的操作,但容器内的顺序既不是后进先出,也不是先进先出,而是(部分)排序的结果。在使用缺省的 less 作为其 Compare 模板参数时,最大的数值会出现在容器的“顶部”。如果需要最小的数值出现在容器顶部,则可以传递 greater 作为其 Compare 模板参数。
关联容器
关联容器有 set(集合)、map(映射)、multiset(多重集)和 multimap(多重映射)。在 C++ 外这些容器常常是无序的;在 C++ 里关联容器则被认为是有序的。
关联容器是一种有序的容器。名字带“multi”的允许键重复,不带的不允许键重复。set 和 multiset 只能用来存放键,而 map 和 multimap 则存放一个个键值对。
与序列容器相比,关联容器没有前、后的概念及相关的成员函数,但同样提供 insert、emplace 等成员函数。此外,关联容器都有 find、lower_bound、upper_bound 等查找函数,结果是一个迭代器:
- find(k) 可以找到任何一个等价于查找键 k 的元素(!(x < k || k < x))
- lower_bound(k) 找到第一个不小于查找键 k 的元素(!(x < k))
- upper_bound(k) 找到第一个大于查找键 k 的元素(k < x)
如果在声明关联容器时没有提供比较类型的参数,缺省使用 less 来进行排序。如果键的类型提供了比较算符 < 的重载,我们不需要做任何额外的工作。否则,我们就需要对键类型进行 less 的特化,或者提供一个其他的函数对象类型。
对于自定义类型,我推荐尽量使用标准的 less 实现,通过重载 <(及其他标准比较运算符)对该类型的对象进行排序。存储在关联容器中的键一般应满足严格弱序关系: - 对于任何该类型的对象 x:!(x < x)(非自反)
- 对于任何该类型的对象 x 和 y:如果 x < y,则 !(y < x)(非对称)
- 对于任何该类型的对象 x、y 和 z:如果 x < y 并且 y < z,则 x < z(传递性)
- 对于任何该类型的对象 x、y 和 z:如果 x 和 y 不可比(!(x < y) 并且 !(y < x))并且 y 和 z 不可比,则 x 和 z 不可比(不可比的传递性)
无序关联容器
从 C++11 开始,每一个关联容器都有一个对应的无序关联容器,它们是:
- unordered_set
- unordered_map
- unordered_multiset
- unordered_multimap
这些容器和关联容器非常相似,主要的区别就在于它们是“无序”的。这些容器不要求提供一个排序的函数对象,而要求一个可以计算哈希值的函数对象。
从实际的工程角度,无序关联容器的主要优点在于其性能。关联容器和 priority_queue 的插入和删除操作,以及关联容器的查找操作,其复杂度都是 O(log(n)),而无序关联容器的实现使用哈希表 [5],可以达到平均 O(1)!但这取决于我们是否使用了一个好的哈希函数:在哈希函数选择不当的情况下,无序关联容器的插入、删除、查找性能可能成为最差情况的 O(n),那就比关联容器糟糕得多了。
array
C 数组在 C++ 里继续存在,主要是为了保留和 C 的向后兼容性。C 数组本身和 C++ 的容器相差是非常大的:
- C 数组没有 begin 和 end 成员函数(虽然可以使用全局的 begin 和 end 函数)
- C 数组没有 size 成员函数(得用一些模板技巧来获取其长度)
- C 数组作为参数有退化行为,传递给另外一个函数后那个函数不再能获得 C 数组的长度和结束位置