顺序容器
一个容器就是一些特定类型对象的集合。 顺序容器 为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
1 顺序容器概述
下表列出了标准库中的顺序容器,所有的顺序容器都提供了快速顺序访问元素的能力。
类型 | 含义 |
---|---|
vector | 可变大小数组 |
deque | 双端队列 |
list | 双向列表 |
forward_list | 单项列表 |
array | 固定大小数组,不能添加或删除元素 |
string | 与 vector 相似,专门用于保存字符 |
注: forward_list 没有 size 操作,因为保存或计算其大小就会比手写链表多出额外的开销。 |
确实使用哪种顺序容器
tips: 通常使用 vector 是最好的选择,除非你有很好的理由选择其他容器。
- 除非你有很好的理由选择其他容器,否则应使用 vector。
- 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用 list 或 forward_list。
- 如果程序要求随机访问元素,应使用 vector 或 deque。
- 如果程序奥球在容器的中间插入或删除元素,应使用 list 或 forward_list。
- 如果程序需要在头尾位置插入和删除元素,但不会在中间位置进行插入或删除操作,则使用 deque。
- 如果程序只有在读取输入时才需要在容器中间插入元素,随后需要随机访问元素,则
- 首先,确定是否真的需要在容器中间位置添加元素。通常可以很容易地向 vector 追加数据。
- 如果必须在中间位置插入元素,考虑在输入阶段使用 list,一旦输入完成,将 list 的内容拷贝到一个 vector 中。
注: 如果不确定应该使用哪种容器,那么可以在程序中只使用 vector 和 list 公共操作:使用迭代器,不适用下标操作,避免随机访问。这样,在必要时选择使用 vector 或 list 都很方便。
2 容器库概览
对容器可以保存的元素类型的限制
顺序容器几乎可以保存任意类型的元素,甚至可以定义一个容器其元素的类型是另一个容器。
vector<vector<string>> lines; // vector 的 vector
vector<vector<string> > lines; // 较旧的编译器
此处 lines 是一个 vector,其元素类型是 string 的 vector。
注: 较旧的编译器可能需要在两个尖括号之间加一个空格。
类型 | 含义 |
---|---|
类型别名 | |
iterator | 此容器类型的迭代器类型 |
const_type | 可以读取元素,不能修改元素的迭代器类型 |
size_type | 无符号整数类型,容器的大小 |
difference_type | 带符号整数类型,两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;(value_type&) |
const_reference | 元素的 const 左值类型(const value_type&) |
构造函数 | |
C c; | 默认构造函数,空容器 |
C c1(c2); | 构造 c2 的拷贝 c1 |
C c(b, e); | 构造c,将迭代器 b 和 e 指定范围内的元素拷贝到 c |
C c{a, b, c}; | 列表初始化 c |
赋值与 swap | |
c1 = c2 | 将 c1 中的元素替换为 c2 中的元素 |
c1 = {a, b, c} | 将 c1 中的元素替换为列表中的元素 (不适用于 array) |
a.swap(b) | 交换 a 和 b 的值 |
swap(a, b) | 与 a.swap(b) 等价 |
大小 | |
c.size() | c中元素的数目 (不支持 forword_list) |
c.max_size() | c 可保存的最大元素数目 |
c.empty() | 判断是否为空 |
添加/删除元素(不适用于 array) | 注: 不同容器中,这些操作的接口都不同。 |
c.insert(args) | 将 args 拷贝进 c |
c.emplace(inits) | 使用 inits 构造 c 中的一个元素 |
c.erase(args) | 删除 args 指定的元素 |
c.clear() | 删除 c 中所有的元素,返回 void |
获取迭代器 | |
c.begin(),c.end() | 返回首尾迭代器 |
c.cbegin(),c.cend() | 返回 const_iterator |
反向容器的额外成员(不支持 forword_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(),c.rend() | 返回指向 c 尾元素和首元素之前位置的迭代器 |
c.crbegin(),c.crend() | 返回 const_reverse_iterator |
2.1 迭代器
使用左闭合( [begin, end) )范围蕴含的编程假定
标准库使用左闭合范围是因为这种范围有三种方便的性质。假定 begin 和 end 构成一个合法的迭代器范围,则
- 如果 begin 和 end 相等,则范围为空。
- 如果 begin 和 end 不等,则范围至少包含一个元素,且 begin 指向该范围中的第一个元素
- 我们可以对 begin 递增若干次,使得 begin == end
zh意味着我们可以像下面的代码一样处理一个循环,而且还是安全的:
while(begin != end) {
*begin = val; // 范围非空,begin 指向一个元素
++begin; // 移动迭代器,指向下素
注: 当不需要写访问时,应该使用 cbegin 和 cend。
2.2 容器定义和初始化
每个容器类型都定义了一个默认构造函数。除 array 之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器的大小和元素初始值参数。
类型 | 含义 |
---|---|
C c | 默认构造函数 |
C c1(c2) | c1 初始化为 c2 的拷贝。(对于 array 类型,两者的大小还必须相同) |
C c1 = c2 | 同上 |
C c{a, b, c} | c 初始化为初始化列表中元素的拷贝。(对于 array 类型,数目要小于等于) |
C c = {a, b, c} | 同上 |
C c(b, e) | c 初始化为迭代器 b 和 e 指定范围中的元素的拷贝 |
C seq(n) | seq 包含 n 个元素,这些元素进行了值初始化;此构造函数是 explicit |
C seq(n, t) | seq 包含 n 个初始化为值 t 的元素 |
如果元素类型时内置类型或者时具有默认构造函数的类类型,可以置为构造函数提供一个容器大小参数。如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。
注: 只有顺序容器(不包括 array)的构造函数才能接受大小参数。
标准库 array 具有固定大小
与内置数组一样,标准库 array 的大小也是类型的一部分。当定义一个 array 时,除了指定元素类型,还必须指定容器大小:
array<string, 10>; // 类型为:保存10个 string 的数组
为了使用array类型,我们必须同时指定元素类型和容器大小:
array<int,10>::size_type i; // 数组类型包括元素类型和数组大小
array<int>::size_type j; // 错误:array<int> 不是一个类型
array 大小固定的特性也影响了它所定义的构造函数的行为。与其它容器不同,一个默认构造的 array 是非空的:它包含与其大小一样多的元素。如果进行列表初始化,初始值的数目必须等于或小于 array 的大小。如果初始值数目小于 array 的大小,则它们被用来初始化 array 中考前的元素,所有剩余的元素都会进行值初始化。
虽然我们不能对内置数组进行拷贝或对象赋值操作,但array并无此限制:
int digs[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = digs; // 错误 内置数组不支持拷贝或赋值
array<int,10> digits= { 0,1,2,3,4,5,6,7,8,9 };
array<int, 10> copy = digits; // 正确 只要数组类型匹配即合法
2.3 赋值和 swap
类型 | 含义 |
---|---|
c1 = c2 | 将 c1 中的元素替换为 c2 中元素的拷贝 |
c = {a, b, c} | 将 c 中的元素替换为初始化列表中元素的拷贝(array 不适用) |
swap(c1, c2) | 交换c1和c2中的元素,swap 通常比从 c2 向 c1 拷贝元素快得多 |
c1.swap(c2) | 同上 |
seq.assign(b, e) | 将 seq 中的元素替换为迭代器 b 和 e 所表示的范围中的元素(迭代器b和e不能指向seq中的元素) |
seq.assign(il) | 将 seq 中元素替换为初始化列表 il 中的元素 |
seq.assign(n, t) | 将 seq 中的元素替换为 n 个值为 t 的元素 |
注1: assign 操作不适用于关联容器和 array。
注2: 复制相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而 swap 操作将容器内容交换不会导致只想容器的迭代器、引用和指针失效(容器类型为 array 和 string 的情况除外)。
使用 assign(仅顺序容器)
顺序容器定义了一个 assign 成员,允许我们从一个不同但相容类型赋值,或者从容器的一个子序列赋值。assign 操作用参数所指定的元素(的拷贝)替换左边容器中的所有元素。例如,我们可以用 assign 实现一个 vector 中一段 char * 值赋予一个 list 中的 string:
list<string> names;
vector<const char*> oldstyle;
names = oldstyle; // 错误:容器类型不匹配
// 正确:可以将const char* 转换为 string
names.assign(oldstyle.cbegin, oldstyle.cend);
这段代码中对 assign 的调用将 names 中的元素替换为迭代器指定的范围中的元素的拷贝。assign 的参数决定了容器中将有多少个元素以及它们的值都是什么。
注: 由于其旧元素被替换,因此传递给 assign 的迭代器不能指向调用 assign 的容器。
使用 swap
swap 操作交换两个相同类型容器的内容。调用 swap 之后,两个容器中的元素将会交换:
vector<string> svec1(10);
vector<string> svec2(24);
swap(svec1, svec2);
除 array 外,交换两个容器内容的操作保证很快,因为元素本身并未交换,swap 只是交换了两个容器的内部数据结构。
注: 除 array 外,swap 不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
元素不会移动的事实意味着,除 string 外,指向容器的迭代器、引用和指针在 swap 操作之后不会失效,但是它们指向的元素已经属于不同的容器了。例如,假定 iter 在 swap 之前指向 svec1[3] 的 string,那么在 swap 之后它指向 svec2[3] 的元素。
与其他容器不同,对一个 string 调用 swap 会导致迭代器、引用和指针失效。
与其他容器不同,swap 两个 array 会真正交换它们的元素。因此交换两个 array 所需的时间与 array 中元素的数目成正比。因此,对于 array,在 swap 操作之后,迭代器、引用和指针所绑定的元素保持不变,但元素值已经与另一个 array 中对应元素的值进行了交换。
2.4 关系运算符
每个容器都支持相等运算符(== 和 !=);除了无序关联容器外所有容器都支持关系运算符(>, >=, <, <=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
比较两个容器实际上是进行元素的逐对比较:
- 如果两个容器具有相同大小且所有元素都两两对应相等,则两个容器相等,否则两个容器不相等。
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等元素的比较结果。
注: 只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
3 顺序容器操作
顺序容器和关联容器的不同之处在于两者组织元素的方式。这些不同之处直接关系到了元素如何存储、访问、添加以及删除。
3.1 向顺序容器中添加元素
向一个 vector、string 或 deque 插入元素回事所有只想容器的迭代器、引用和指针失效。
在一个 vector 或 string 的尾部之外的任何位置,或是一个 deque 的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个 vector 或 string 添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
push_back
push_back 将一个元素追加到一个容器的尾部。除了 array 和 forward_list 之外,每个顺序容器(包括 stirng 类型)都支持 push_back。
关键概念: 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。
push_front
push_front 将一个元素追加到一个容器的头部。deque 像 vector 一样提供了随机访问元素的能力,但它提供了 vector 所不支持的 push_front。deque 保证在容器首尾进行插入和删除元素的操作都只花费常数时间。与 vector 一样,在 deque 首尾之外的位置插入元素会很耗时。
在容器中特定位置添加元素 —— insert
insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector、deque、list 和 string 都支持 insert 成员。forward_list 则提供了特殊版本的insert成员。
每个 insert 函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。它可以指向容器中任何位置,包括容器尾部之后的下一个位置。由于迭代器可能指向容器尾部之后不存在的元素的位置,而且在容器开始位置插入元素是很有用的功能,所以 insert 函数将元素插入到迭代器所指定的位置之前。
slist.insert(iter,"Hello!"); // 将"Hello!"添加到 iter 之前的位置
注: 将元素插入到 vector、deque 和 string 中的任何位置都是合法的,然而这样做可能很耗时。
插入范围内元素
除了第一个迭代器参数之外,insert 函数还可以接受更多的参数,这与容器构造函数类似。其中一个版本接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素都按给定值初始化:
svec.insert(svec.end(),10,"Anna");
接受一对迭代器或一个初始化列表的 insert 版本将给定范围中的元素插入到指定位置之前:
vector<string> v = {"quasin","simba',"frollo","nscarH");
// 将 v 的最后两个元素添加到 slist 的开始位置
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(), ("these", “words”, "will", ngon,natn, "the", “end”});
// 运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(), slist.begin(), slist.end);
如果我们传递给 insert 一对迭代器,它们不能指向添加元素的目标容器。
在新标准下,接受元素个数或范围的 insert 版本返回指向第一个新加入元素的迭代器。(在旧版本的标准库中,这些操作返回void。)如果范围为空,不插入任何元素, insert 操作会将第一个参数返回。
insert 的返回值
通过使用 insert 的返回值,可以在容器中一个特定位置反复插入元素:
ist<string> 1st;
auto iter = 1st.begin();
while (cin » word)
iter = 1st. insert (iter, word); // 等价于调用 push_front
在循环之前,我们将 iter 初始化为 lst.begin()。第一次调用 insert 会将我们刚刚读入的 string 插入到 iter 所指向的元素之前的位置。insert 返回的迭代器恰好指向这个新元素。我们将此迭代器赋予 iter 并重复循环,读取下一个单词。只要继续有单词读入,每步 while 循环就会将一个新元素插入到 iter 之前,并将 iter 改变为新加入元素的位置。此元素为(新的)首元素。因此,每步循环将一个新元素插入到 list 首元素之前的位置。
emplace
新标准引入了三个新成员 emplace_front、emplace 和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应 push_front、insert 和 push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
当调用 push 或 insert 成员函数时,我们将元素类型的对象传递给它们,会创建一个局部临时对象,并将这些对象被拷贝到容器中。
而我们调用一个 emplace 成员函数时,则时将参数传递给元素类型的构造函数。emplace 成员使用这些参数在容器管理的内存空间中直接构造元素。
注: emplace 函数在容器中直接构造元素传递给 emplace 函数的参数必须与元素类型的构造函数相匹配。
3.2 访问元素
包括 array 在内的每个顺序容器都有一个 front 成员函数,而除 forward_list 之外的所有顺序容器都有一个 back 成员函数。这两个操作分别返回首元素和尾元素的引用。
程序可以通过两种不同方式来获取首元素和尾元素的引用。直接的方法是调用 front 和 back。间接的方法是通过解引用 begin 返回的迭代器来获得首元素的引用,以及通过递减然后解引用 end 返回的迭代器来获得尾元素的引用。
注: 对一个空容器调用 front 和 back,就像使用一个越界的下标一样,是一种严重的程序设计错误。
访问成员函数返回的是引用
在容器中访问元素的成员函数(即,front、back、下标和 at)返回的都是引用。如果容器是一个 const 对象,则返回值是 const 的引用。如果容器不是const的,则返回值是普通引用。
下标操作和安全的随机访问
提供快速随机访问的容器(string、vector、deque 和 array)也都提供下标运算符。下标运算符接受一个下标参数,返问容器中该位置的元素的引用。如果我们希望确保下标是合法的,可以使用 at 成员函数。at 成员函数类似下标运算符,但如果下标越界,at 会抛出一个 out_of_range 异常。
vector<string> svec; // 空 vector
cout << svec[0]; // 运行时错误:svec中没有元素
cout << svec.at(0); // 抛出一个out_of_range异常
3.3 删除元素
删除元素的成员函数并不检查其参数,在删除元素之前,程序员必须确保它(们)是存在的。
pop_front 和 pop_back 成员函数
pop_front 和 pop_back 成员函数分别删除首元素和尾元素。与 vector 和 string 不支持 push_front 一样,这些类型也不支持 pop_front。类似的,forward_list 不支持 pop_back。
这些操作返回void。如果你需要弹出的元素的值,就必须在执行弹出操作之前保存它:
while(!list.empty()){
process(ilist.front()); // 对ilist的首个元素进行一些处理
ilist.pop_front(); // 完成之后删除首个元素
}
从容器内部删除一个元素
成员函数 erase 从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回指向删除的(最后一个)元素之后位置的迭代器。若 j 是 i 之后的元素,那么 erase(i) 将返回指向 j 的迭代器。
删除多个元素
接受一对迭代器的 erase 版本允许我们删除一个范围内的元素:
// 删除两个迭代器表示的范围内的元素
// 返回指向最后一个被删元素之后位置的送代器
elem1 = slist.erase(elem1,elem2); // 调用后,elem1==elem2
迭代器 elem1 指向我们要删除的第一个元素,elem2 指向我们要删除的最后一个元素之后的位置。
为了删除一个容器中的所有元素,我们既可以调用 clear,也可以用 begin 和 end 获得的迭代器作为参数调用 erase。
3.4 特殊的 forward_list 操作
当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱的链接。但是,forward_list 是单向链表。在一个单向链表中,没有简单的方法来获取一个元素的前驱。出于这个原因,在一个 forward_list 中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除操作所影响的元素。
操作 | 含义 |
---|---|
lst.before_begin() | 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。 |
lst.cbefore_begin() | 同上,返回一个 const——iterator |
lst.insert_after(p, t) | 在迭代器 p 之后的位置插入元素。t 是对象。 |
lst.insert_after(p, n, t) | n 是数量 |
lst.insert_after(p, b, e) | b 和 e 是表示范围的一堆迭代器(不能指向 lst 内) |
lst.insert_after(p, li) | li 是一个花括号列表 |
emplace_after(p, args) | 使用 args 在 p 指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若 p 为尾后迭代器,则函数行为未定义 |
lst.emplace_after§ | 删除 p 指向的位置之后的元素,返回一个指向被删元素之后元素的迭代器 |
lst.emplace_after(b, e) | 删除从 b 之后直到 e(不包含)之间的元素。返回一个指向被删元素之后元素的迭代器 |
当在 forward_list 中添加或删除元素时,我们必须关注两个迭代器—— 一个指向我们要处理的元素,另一个指向其前驱。
3.5 改变容器大小
我们可以用 resize 来增大或缩小容器,与往常一样,array 不支持 resize。如果当前大小,大于所要求的大小,容器后部的元素会被删除;如果当前大小,小于新大小,会将新元素添加到容器后部:
list<int>ilist(10,42); // 10个 int:每个的值都是42
ilist.resize(15); // 将5个值为0的元素添加到 ilist 的末尾
ilist.resize(25,-1); // 将10个值为 -1 的元素添加到 ilist 的末尾从 ilist 末尾
ilist.resize(5); // 从末尾删除20个元素
resize 操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。如果调用者未提供此参数,新元素进行值初始化。如果容器保存的是类类型元素,且 resize 向容器添加新元素,则我们必须提供初始值,或者元素类型必须提供一个默认构造函数。
注: 如果 resize 缩小容器,责之项背删除元素的迭代器、引用和指针都会失效;对 vector、string 或 deque 进行 resize 可能导致迭代器、指针和引用失效。
3.6 容器操作可能使迭代器失效
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的指针、引用或迭代器失效。一个失效的指针、引用或迭代器将不再表示任何元素。使用失效的指针、引用或迭代器是一种严重的程序设计错误,很可能引起与使用未初始化指针一样的问题。
在向容器添加元素后:
- 如果容器是 vector 或 string,且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后元素的迭代器、指针和引用将会失效。
- 对于 deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
- 对于 list 和 forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
当我们从一个容器中删除元素后,指向被删除元素的迭代器、指针和引用会失效,这应该不会令人惊讶。毕竟,这些元素都已经被销毁了。
当我们删除一个元素后:
- 对于 list 和 forward_list,指向容器其他位置的迭代器(包括尾后迭代器和首前迭代器)、引用和指针仍有效。
- 对于 deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器、引用或指针也会失效。如果是删除 deque 的尾元素,则尾后迭代器也会失效,但其他迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
- 对于 vector 和 string,指向被删元素之前元素的迭代器、引用和指针仍有效。注意:当我们删除元素时,尾后迭代器总是会失效。
建议: 当你使用迭代器(或指向容器元素的引用或指针)时,最小化要求迭代器必须保持有效的程序片段是一个好方法。由于向迭代器添加元素和删除元素的代码可能会是迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对 vector、string 和 deque 尤为重要。
不要保存 end 返回的迭代器
当我们添加/删除 vector 或 string 的元素后,或在 deque 中首元素之外任何位置添加/删除元素后,原来 end 返回的迭代器总是会失效。因此,添加或删除元素的循环程序必须反复调用 end,而不能在循环之前保存 end 返回的迭代器,一直当作容器末尾使用。
4 vector 对象是如何增长的
为了支持快速随机访问,vector 将元素连续存储 —— 每个元素紧挨着前一个元素存储。通常情况下,我们不必关心一个标准库类型是如何实现的,而只需关心它如何使用。对于 vector 和 string,其部分实现渗透到了接口中。
向 vector 或 string 中添加元素时,如果没有空间容纳新元素,容器不可能简单地将它添加到内存中其他位置,因为元素必须连续存储。容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。如果我们每添加一个新元素,vector 就执行一次这样的内存分配和释放操作,性能会慢到不可接受。
为了避免这种代价,标准库实现者采用了可以减少容器空间重新分配次数的策略。当不得不获取新的内存空间时,vector 和 string 的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可用来保存更多的新元素。这样,就不需要每次添加新元素都重新分配容器的内存空间了。
这种分配策略比每次添加新元素时都重新分配容器内存空间的策略要高效得多。其实际性能也表现得足够好,虽然 vector 在每次重新分配内存空间时都要移动所有元素,但使用此策略后,其扩张操作通常比 list 和 deque 还要快。
管理容量的成员函数
操作 | 含义 |
---|---|
c.shrink_to_fit() | 将 capacity() 减少为与 size() 相同大小 |
c.capacity() | 不重新分配内存空间的话,c 可以保存多少元素 |
c.reserve(n) | 分配至少能容纳 n 个元素的内存空间 |
注1: shrink_to_fit 只适用于 vector、string 和 deque。
注2: capacity 和 reserve 只适用于 vector 和 string。
注3: reserve 并不改变容器中元素的数量,它仅影响 vector 预先分配多大的内存空间。
有当需要的内存空间超过当前容量时,reserve 调用才会改变 vector 的容量。如果需求大小大于当前容量,reserve 至少分配与需求一样大的内存空间(可能更大)。如果需求大小小于或等于当前容量,reserve 什么也不做。特别是,当需求大小小于当前容量时,容器不会退回内存空间。
capacity 和 size
容器的 size 是指它已经保存的元素的数目;而 capacity 则是在不分配新的内存空间的前提下它最多可以保存多少元素。
下面的代码展示了size和capacity之间的相互作用
vector<int> ivec;
// size 应该为0 capacity 应该依赖于具体实现
cout << "ivec:size: " <<ivec.size()
<< "capacity: " <<ivec.capacity() << endl;
// 向 ivec 添加24个元素
for(vector<int>::size_type ix = 0; ix != 24; ix++){
ivec.push_back(ix);
}
// size 应该为24 capacity 应该大于等于24 具体依赖于标准库的实现
cout << "ivec:size: " <<ivec.size()
<< "capacity: " <<ivec.capacity() << endl;
当在我们的系统上运行时,这段程序得到如下输出:
ivec:size:0 capacity:0
ivec:size:24 capacity:32
现在预分配一些空间:
ivec.reserve(50); // 将 capacity 的容量设置至少为50 可能会更大
// size 的大小应该为24,capacity 的大小应该大于等于50 具体依赖于标准库的实现
程序的输出表明 reserve 严格按照我们需求的大小分配了新的空间
ivec:size:24 capacity:50
接下来用光这些预留空间:
while(ivec.size() != ivec.capacity()) {
ivec.push_back(0);
}
程序的输出表明此时我们确实用光了预留空间,size 和 capacity 相等
ivec:size:50 capacity:50
由于我们只是用了预留空间,因此没有必要为 vector 分配新的空间。实际上,只要没有操作需求超出 vector 的容量,vector 就不能重新分配内存空间。
如果我们现在再添加一个新元素,vector 就不得不重新分配内存空间:
ivec.push_back(42); // 再添加一个元素
此时程序的输出为
ivec:size:51 capacity:100
可以使用 shrink_to_fit 来要求 vector 将超出当前大小的多余内存空间退回给系统,但是调用 shrink_to_fit 只是一个请求,标准库并不保证退还内存。
注: 每个 vector 实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。
5 额外的 string 操作
5.1 构造 string 的其他方法
除了已经介绍过的构造函数,以及与其他顺序容器相同的构造函数外, string 类型还支持另外三个构造函数。
操作 | 含义 |
---|---|
string s(cp, n) | s 是 cp 指向的数组中前 n 个字符的拷贝 |
string s(s2, pos2) | s 是 string s2 从下标 pos2 开始的字符的拷贝 |
string s(s2, pos2, len2) | s 是 string s2 从下标 pos2 开始 len2 个字符的拷贝 |
注: n、len2 和 pos2 都是无符号值。 |
这些构造函数接受一个 string 或一个 const char* 参数,还接受(可选的)指定拷贝多少个字符的参数。当我们传递给它们的是一个 string 时,还可以给定一个下标来指出从哪里开始拷贝。
const char *cp = "Hello World!!!"; // 以空字符串结束的数组
char noNull[] = {'H', 'i'}; // 不是以空的字符串结束
string s1(cp); // 拷贝cp中的字符直到遇到空的字符串 ,因此 s1 == "Hello World!!!"
string s2(noNull, 2); // 从 noNull 拷贝两个字符 s2 == "Hi"
string s3(noNull); // 未定义: noNull 不以空字符结束
string s4(cp + 6, 5); // 从cp[6]开始拷贝5个字符 s4 == "Word"
string s5(s1, 6, 5); // 从s1[6]开始拷贝5个字符 s5 == "Word"
string s6(s1, 6); // 从s1[6]开始拷贝直至末尾 s6 == "Word"
string s7(s1, 6, 20); // 从s1[6]开始拷贝直至末尾 s7 == "Word"
string s8(s1, 16); // 抛出一个 out_of_range 的异常
当我们从一个 const char*创建 string 时
- 指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。
- 如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。
- 如果我们未传递计数值且数组也未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
当从一个string拷贝字符时
- 我们可以提供一个可选的开始位置和一个计数值。开始位置必须小于或等于给定的string的大小。如果位置大于size,则构造函数抛出一 个 out_of_range异常。
- 如果我们传递了一个计数值,则从给定位置开始拷贝这么多个字符。不管我们要求拷贝多少个字符,标准库最多拷贝到string 结尾,不会更多。
substr
substr 操作返回一个 string,它是原始 string 的一部分或全部的拷贝。可以传递给 substr 一个可选的开始位置和计数值。
如果开始位置超过了 string 的大小,则 substr 函数抛出一个 out_of_range 异常。如果开始位置加上计数值大于 string 的大小,则 substr 会调整计数值,只拷贝到string的末尾。
5.2 改变 string 的其他方法
string 类型支持顺序容器的赋值运算符以及 assign、insert 和 erase 操作。除此之外,它还定义了额外的 insert 和 erase 版本。除了接受迭代器的 insert 和 erase 版本外,string 还提供了接受下标的版本。下标指出了开始删除的位置,或是insert到给定值之前的位置:
s.insert(s.size(), 5, '!'); // 在 s 末尾插入5个感叹号
s.erase(s.size() - 5, 5); // 从 s 删除最后5个字符
标准库 string 类型还提供了接受 C 风格字符数组的 insert 和 assign 版本。例如,我们可以将以空字符结尾的字符数组 insert 到或 assign 给一个 string:
const char *cp = "Stately, plump Buck";
s.assign(cp, 7); // s == "Statelyn"
s.insert(s.size(), cp + 7); // s == "Stately, plump Buck"
apend 和 replace
string 类定义了两个额外的成员函数:append 和 replace,这两个函数可以改变 string 的内容。
append 操作是在 string 末尾进行插入操作的一种简写形式:
string s("C++Primer"), s2 = s; // 将s和s2初始化为"C++Primer"
s.insert(s.size(), "4thEd"); // s == "C++Primer 4th Ed."
s2.append("4thEd"); // 等价方法:将"4thEd."追加到s2
replace操作是调用erase和insert的一种简写形式:
// 将 "4th" 替换为 "5th" 的等价方法
s.erase(11, 3); // s == "C++Primer Ed."
s.insert(11, "5th"); // s == "C++ Primer 5th Ed."
// 从位置11开始,删除3个字符并插入 5th
s2.replace(11, 3, "5th"); // 等价方法:s == s2
改变string的多种重载函数
assign 和 append 函数无须指定要替换 string中 哪个部分:assign 总是替换 string 中的所有内容,append 总是将新字符追加到 string 末尾。
replace 函数提供了两种指定删除元素范围的方式。可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。
insert 函数允许我们用两种方式指定插入点:用一个下标或一个迭代器。在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。
可以用好几种方式来指定要添加到string中的字符。新字符可以来自于另一个string,来自于一个字符指针(指向的字符数组),来自于一个花括号包围的字符列表,或者是一个字符和一个计数值。当字符来自于一个 string 或一个字符指针时,我们可以传递一个额外的参数来控制是拷贝部分还是全部字符。
5.3 string 搜索操作
string 类提供了 6个不同的搜索函数,每个函数都有 4个重载版本。
每个搜索操作都返回一个 string::size_type 值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为 string::npos 的 static 成员。标准库将 npos 定义为一个 const string::size_type 类型,并初始化为值 -1。由于 npos 是一个 unsigned 类型,此初始值意味着 npos 等于任何 string 最大的可能大小。
注: string 搜索函数返回 string::size_type 值,该类型是一个 unsigned 类型。因此,用一个 int 或其他带符号类型来保存这些函数的返回值并不是一个好主意。
6个不同的搜索函数:
操作 | 含义 |
---|---|
s.find(args) | 查找 s 中 args 第一次出现的位置 |
s.rfind(args) | 查找 s 中 args 最后一次出现的位置 |
s.find_first_of(args) | 在 s 中查找 args 中任何一个字符第一次出现的位置 |
s.find_last_of(args) | 在 s 中查找 args 中任何一个字符最后一次出现的位置 |
s.find_first_not_of(args) | 在 s 中查找第一个不在 args 中的字符 |
s.find_last_not_of(args) | 在 s 中查找最后一个不在 args 中的字符 |
4个重载版本:
参数 | 含义 |
---|---|
c, pos | 从 s 中位置 pos 开始查找字符 c |
s2, pos | 从 s 中位置 pos 开始查找字符串 s2 |
cp, pos | 从 s 中位置 pos 开始查找指针 cp 指向的以空字符结尾的 C 风格字符串 |
cp, pos, n | 从 s 中位置 pos 开始查找指针 cp 指向的数组的前 n 个字符 |
5.4 compare 函数
除了关系运算符外,标准库 string 类型还提供了一组 compare 函数,这些函数与 C 标准库的 strcmp 函数很相似。类似 strcmp,根据 s 是等于、大于还是小于参数指定的字符串,s.compare 返回0、正数或负数。
参数 | 含义 |
---|---|
s2 | 比较 s 和 s2 |
pos1, n1, s2 | 将 s 中从 pos1 开始的 n1 个字符与 s2 进行比较 |
pos1, n1, s2, pos2, n2 | 将 s 中从 pos1 开始的 n1 个字符与 s2 中从 pos2 开始的 n2 个字符进行比较 |
cp | 比较 s 和 cp 指向的以空字符结尾的字符数组 |
pos1, n1, cp | 将 s 中从 pos1 开始的 n1 个字符与 cp 指向的以空字符结尾的字符数组进行比较 |
pos1, n1, cp, n2 | 将 s 中从 pos1 开始的 n1 个字符与 cp 指向的地址开始的 n2 个字符进行比较 |
5.5 数值转换
新标准引入了多个函数,可以实现数值数据与标准库 string 之间的转换。
要转换为数值的 string 中第一个非空白符必须是数值中可能出现的字符:
string s2 = "pi=3.14";
// 转换 s 中以数字开始的第一个子串,结果 d = 3.14
d=stod (s2.substr(s2.find_first_of(*'+-.0123456789*')));
在这个 stod 调用中,我们调用了 find_first_of 来获得 s 中第一个可能是数值的一部分的字符的位置。我们将S中从此位置开始的子串传递给 stod。stod 函数读取此参数,处理其中的字符,直至遇到不可能是数值的一部分的字符。然后它就将找到的这个数值的字符串表示形式转换为对应的双精度浮点值。
string参数中第一个非空白符必须是符号(+或-)或数字。它可以以 0x 或 0X 开头来表示十六进制数。对那些将字符串转换为浮点值的函数,string 参数也可以以小数点(.)开头,并可以包含 e 或 E 来表示指数部分。对于那些将字符串转换为整型值的函数,根据基数不同,string 参数可以包含字母字符,对应大于数字 9 的数。
操作 | 含义 |
---|---|
to_string(val) | 一组重载函数,返回数组 val 的 string 表示 |
stoi(s, p, b) | 返回 s 的起始字串的数值,返回类型是 int |
stol(s, p, b) | 返回类型是 long |
stoul(s, p, b) | 返回类型是 unsigned long |
stoll(s, p, b) | 返回类型是 long long |
stoull(s, p, b) | 返回类型是 unsigned long long |
stof(s, p) | 返回类型是 float |
stod(s, p) | 返回类型是 double |
stold(s, p) | 返回类型是 long double |
注: 如果 string 不能转换为一个数值,这些函数抛出一个 invalid_argument 异常。如果转换得到的数值无法用任何类型来标识,则抛出一个 out_of_range 异常。
6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue 和 priority_queue。
适配器 是标准库中的一个通用概念。容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
例如,stack 适配器接受一个顺序容器 (除 array 或 forward_lis t外),并使其操作起来像一个 stack 一样。
定义一个适配器
每个适配器都定义两个构造函数:1. 默认构造函数创建一个空对象,2. 接受一个容器的构造函数拷贝该容器来初始化适配器。
例如,假定 deq 是一个 deque<int>,我们可以用 deq 来初始化一个新的 stack,如下所示
stack<int> stk(deq); // 从 deq 拷贝元素到 stk
我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
// 在 vector 实现的空栈
stack<string, vector<string>> str_stk;
// str_stk2 在 vector 上实现(默认底层是 deque),初始化时保存 svec 的拷贝
stack<string, vector<string>> str_stk2(svec);
对于一个给定的适配器,可以使用哪些容器是有限制的。所有适配器都要求容器具有添加和删除元素的能力。因此,适配器不能构造在 array 之上。类似的,我们也不能用 forward_list 来构造适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。
- stack 只要求 push_back、pop_back 和 back 操作,因此可以使用除 array 和 forward_list 之外的任何容器类型来构造 stack。
- queue 适配器要求 back、push_back、front 和 push_front,因此它可以构造于 list 或 deque 之上,但不能基于 vector 构造。
- priority_queue 除了 front、push_back 和 pop_back 操作之外还要求随机访问能力,因此它可以构造于 vector 或 deque 之上,但不能基于 list 构造。
默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是在 vector 之上实现的。
栈适配器
stack 类型定义在 stack 头文件中,下面的程序展示了如何使用 stack:
stack<int> intStack; // 空栈
// 填满栈
for (size_t ix = 0; ix != 10; ++ix)
intStack.push(ix); // intStack 保存 0 到 9 十个数
while (!intStack.empty()){ // intStack 中有值就继续循环
int value = intStack.top();
// 使用栈顶值的代码
intStack.pop(); // 弹出栈顶元素,继续循环
}
操作 | 含义 |
---|---|
s.pop() | 删除栈顶元素,但不返回该元素值 |
s.push(item) | 创建一个新元素压入栈顶,该元素通过拷贝或移动 item 而来 |
s.emplace(args) | 或者由 args 构造 |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
注: 每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作。我们只可以使用适配器操作,而不能使用底层容器类型的操作。
队列适配器
queue 和 priority_queue 适配器定义在 queue 头文件中。
操作 | 含义 |
---|---|
q.pop() | 删除 queue 的首元素 或 priority_queue 最高优先级的元素,但不返回此元素 |
q.front() | 返回首元素,但不删除此元素 |
q.back() | 返回尾元素,但不删除此元素,只适用于 queue |
q.top() | 返回最高优先级元素,但不删除该元素 只适用于 priority_queue |
q.push(item) | 在 queue 末尾,或 priority_queue 适当位置创建一个元素,其值为 item |
q.emplace(agrs) | 或者由 args 创建 |
标准库 queue 使用一种先进先出(first-in,first-out,FIFO)的存储和访问策略。进入队列的对象被放置到队尾,而离开队列的对象则从队首删除。饭店按客人到达的顺序来为他们安排座位,就是一个先进先出队列的例子。
priority_queue 允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认情况下,标准库在元素类型上使用 < 运算符来确定相对优先级。