[C++ Primer] 第9章: 顺序容器
顺序容器概述
顺序容器的类型有:
类型 | 说明 |
---|---|
vector | 可变长度数组. 支持快速随机访问. |
deque | 双端队列. 支持快速随机访问. |
list | 双向链表. 只支持双向顺序访问. |
forward_list | 单向链表. 只支持单向顺序访问. |
array | 固定大小数组. |
string | 与vector相似的容器, 专门用于保存字符串. |
string和vector将元素保存在连续的内存空间中, 因而支持随机访问.
list和forward_list是链表的数据结构, 在容器的任何位置添加和删除元素都很快速, 但是不支持随机访问. 要访问一个元素, 只能遍历整个容器.
deque支持快速随机访问, 在中间位置添加或者删除元素代价(可能)很高, 在两端添加或者删除元素都是很快的.
forward_list和array是新C++标准增加的类型, 与内置数组相比, array是一种更安全更容易使用的数组类型. array不支持添加删除元素以及改变大小操作. forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能.
现代C++程序应该使用标准库容器, 而不是更原始的数据结构, 如内置数组.
确定使用那种顺序容器
通常, 使用vector是最好的选择, 除非你有很好的理由选择其他容器.
如果不确定使用那种容器, 可以在程序中只使用vector和list公共操作: 使用迭代器, 不使用下标操作, 避免随机访问. 这样在必要是视同vector或list都很方便.
容器库概览
每个容器都定义在一个头文件中, 文件名与类型名相同.
顺序容器可以保存任意类型的元素, 包括类, 当元素类型是类时且该类没有默认构造函数时, 在构造这种容器时不能只传递给它一个元素数目参数.
vector<ClassName> v1(10, init); //正确, 提供了元素初始化器
vector<ClassName> v2(10); //错误, 必须提供一个元素初始化器.
容器操作 P295
类型别名 | 说明 |
---|---|
iterator | 返回此类型容器的迭代器. |
const_iterator | 只读迭代器. |
size_type | 无符号整型. |
difference_type | 带符号整型, 表示迭代器距离. |
value_type | 元素类型. |
reference | 元素左值类型, 等价value_type&. |
const_reference | 元素cosnt左值类型 |
标准库array具有固定大小
标准库array的大小也是类型的一部分, 当定义一个array时, 除了指定元素类型, 还必须指定容器大小. 使用array类型时也必须同时指定元素类型和大小. 不能对内置数组执行拷贝或对象赋值操作, 但array并无此限制.
array<int, 42> //类型为: 保存42个int的数组.
array<string, 10> //类型为保存10个string的数组
array<int, 10>::size_type i; //正确, 数组类型包含元素类型和大小
array<int>::size_type j; //错误, array<int>不是一个类型, 要想使用array类型, 必须同时指定大小
int arr[10] = {0,1,2,3,4,5,6,7,8,9};
int cpy[10] = arr; //错误, 内置数组不支持拷贝或赋值
array<int, 10> arr = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> cpy = arr; //正确, array支持拷贝赋值操作, 只要数组类型匹配即为合法.
赋值和swap
赋值相关运算(包括 = 和 assign)会导致指向左边的容器内部的迭代器, 引用和指针失效. 而swap操作将容器内容交换, 不会导致指向容器的迭代器, 引用和指针失效(容器类型除array和string外).
swap不对任何元素进行拷贝, 删除或插入操作, 因此可以保证在常数时间内完成(除开array). 对于array, swap会真正交换它们的元素.
// 赋值操作
c1 = c2; // 将c1内容替换为c2
c1 = {1, 2, 3}; // 赋值后c1大小为3
// array类型赋值
array<int, 10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int, 10> a2 = {0}; // 所有元素值均为0
a1 = a2; // 替换a1中元素
a2 = {0}; // 错误, 不能将花括号列表赋予数组
// assign操作(不适用与关联容器和array)
list<string> names;
vector<const char *> oldStyle{"aa", "bb", "cc"};
names.assign(oldStyle.cbegin(), oldStyle.cend()); // names为迭代器范围内的元素
names.assign({"dd", "ee", "ff"}); // names为初始化列表中的元素
names.assign(3, "gg"); // names为3个值为"gg"的元素
// swap操作
vector<string> svec1(10); // 10个元素的vector
vector<string> svec1(24);
swap(svec1, svec2);
svec1.swap(svec2);
新标准库中, 容器即提供成员函数版本的swap, 又提供非成员版本的swap. 就早期标准库只提供成员函数版本的swap. 非成员版本的swap在泛型编程中是非常重要的. 统一使用非成员版本的swap是一个好习惯.
容器大小操作
每个容器类型都支持相等运算符(== 和 !=), 除了无序关联容器外的所有容器都支持关系运算符(>, <, >=, <=). 关系运算符左右两边的运算对象必须是相同类型的容器, 且必须保存相同类型的元素.
比较两个容器实际上是进行元素的逐对比较. 只有当元素类型也定义了相应的比较运算符时, 才可以使用关系运算符来比较两个容器. 如元素类型为一个未定义比较运算符的类时, 就不可以用关系运算符.
顺序容器操作
向顺序容器添加元素
- push_back: 除了array和forward_list之外, 每一个顺序容器(包括string类型)都支持push_back
- push_front: list, forward_list和deque支持push_front
list<int> lst;
for(size_t i = 0; i != 4; ++i)
{
lst.push_front(i); // 将元素添加到lst开头. lst保存的是3 2 1 0
}
- insert: vector, deque, list和string支持insert操作, forward_list提供了特殊版本的insert成员
list<int> lst{1, 2, 3};
vector<int> ivec{4, 5, 6};
// 插入操作都是在指定位置之前插入, 因为迭代器可能指向容器尾部之后不存在的元素的位置.
lst.insert(lst.begin(), 0); // 等价lst.push_front(0);
lst.insert(lst.begin(), 2, 0); // 在begin()之前插入2个0
lst.insert(lst.end(), ivec.begin(), ivec.end());// 在尾部插入迭代器范围内的数据
lst.insert(lst.end(), {7, 8, 9}); // 在尾部插入元素值列表
// 某些容器部支持push_front操作, 但它们对于insert操作并无类似的限制, 因此我们可以将元素插入到容器的开始位置, 而不必担心容器是否支持push_front. 但是这样做可能很耗时.
ivec.insert(ivec.begin(), 3); // vector虽然不支持push_front, 但是可以在begin()之前插入元素
// 使用insert的返回值, 新标准下, 接收元素个数或者范围的insert版本返回指向第一个新加入元素的迭代器. 旧版本标准库中, 这些操作返回void
auto iter = lst.begin();
int i = 0;
while(i != 10)
{
iter = lst.insert(iter, i++); // 等价调用push_front()
}
- emplace: emplace_front, emplace和emplace_back这些操作构造而不是拷贝元素. 当调用push或insert成员函数时, 将元素类型的对象传递给它们, 这些对象被拷贝到容器中, 而当调用一个emplace函数时, 则是将参数传递给元素类型的构造函数, emplace成员使用这些参数在容器管理的内存空间中直接构造元素, 传递给emplace函数的参数必须与元素类型的构造函数相匹配.
struct MyClass {
MyClass(int x1 = 0, string s1 = ""): x(x1), s(s1) { }
int x;
string s;
};
list<MyClass> mclst;
mclst.emplace_front(11); // 调用1个参数的构造函数, vector和string不支持emplace_front
mclst.emplace_back(); // 调用默认构造函数, 在尾部直接构造元素
mclst.emplace(mclst.begin(), 24, "World"); // 调用两个参数的构造函数, 在begin()之前直接构造元素
// 调用emplace_back是, 会在容器管理的内存空间中直接创建对象
// 调用push_back则会创建一个局部临时对象, 并将其压入容器中.
mclst.push_back(MyClass(10, "Hello"));
访问元素
at和下标操作只适用于string, vector, deque, array.
back不适用于 forward_list
vector<int> ivec{1, 2, 3, 4};
if(!ivec.empty()) // 对空容器调用front(), back()就像使用一个越界下标一样, 是严重错误!
{
// front(), back()分别返回首尾元素的引用
ivec.front() = 10;
int b = ivec.back(); // b是尾元素的拷贝, 修改b不会改变ivec中的尾元素
auto &f = ivec.front(); // f是引用, 改变f会改变ivec中元素
ivec[0] = 24; // 返回下标元素的引用, 若下标越界则行为未定义
ivec.at(0) = 36;// 返回下标元素的引用, 若下标越界则抛出 out_of_range 异常
}
删除元素
这些操作会改变容器大小, 所以不适用于array.
forward_list 有特殊的版本的erase
forward_list 不支持pop_back; vector和string不支持pop_front
list<int> ilst{1, 2, 3, 4, 5, 6, 7, 8};
ilst.pop_back(); // 返回void, 删除尾元素, 若ilst为空则行为未定义
ilst.pop_front(); // 返回void, 删除首元素, 若ilst为空则行为未定义
auto p = ilst.begin();
ilst.erase(p); // 删除迭代器p所指定的元素, 返回被删除元素的下一个元素的迭代器, 若p是尾后迭代器则行为未定义
ilst.erase(ilst.begin(), ilst.end()); // 删除迭代器范围(p, e)元素, 返回指向最后被删除的元素的下一个元素迭代器
// 型如: c.erase(p, p+n); 删除连续n个元素, vector等连续存储的标准库才支持, 因为只有连续存储的标准路的迭代器才定义p+n操作
ilst.clear(); // 删除所有元素, 返回void
特殊的forward_list操作
forward_list是单向链表, 没有简单的办法获取一个节点的前驱, 考虑到链表的插入和删除操作, 在一个forwart_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的.
这些操作与其他容器不同, 所以forward_list没有定义insert, emplace和erase操作, 而是定义了insert_after, emplace_after和erase_after操作. forward_list定义了before_begin, 它返回一个首前迭代器, 它允许我们在链表首元素之前并不存在的元素之后添加或删除元素.
forward_list<int> flst{1, 2, 3, 4, 5, 6, 7, 8};
auto cprev = flst.cbefore_begin();
// 当向forward_list中添加或者删除元素时, 必须关注两个迭代器: 一个指向我们要处理的元素, 一个指向其前驱
auto prev = flst.before_begin(); // 返回首前迭代器, 该元素不存在, 不能解引用
auto curr = flst.begin(); // 第一个元素
while(curr != flst.end())
{
if(*curr % 2)
curr = flst.erase_after(prev); // 删除奇数元素, 并移动curr
else
{
prev = curr; // 两个迭代器都向前移动
++curr;
}
}
改变容器大小
resize不适用与array
list<int> lst(10, 42); // 10个int, 每个值都为42
lst.resize(15); // 将5个0添加到末尾
lst.resize(25, -1); // 再将10个-1添加到末尾
lst.resize(5); // 从末尾删除20个元素
如果resize缩小容器, 则指向被删除容器的迭代器, 引用或指针都会失效.
如果在一个循环中插入/删除deque, string或vector中的元素, 不要缓存end返回的迭代器. 通常C++标准库实现end()操作都很快.
vector对象是如何增长的
向vector或string添加元素时, 如果没有空间容纳新元素, 则容器必须分配新的内存空间来保存已有元素和新元素, 将已有元素从旧空间移动到新空间, 然后添加新元素, 释放旧存储空间.
当vector或string不得不获取新的内存空间时, vector和string的实现通常会分配比新的空间需求更大的内存空间.
管理容量的成员函数:
shrink_to_fit只适用于vector, string, deque.
capacity和reserve只适用于vector和string
c.shrink_to_fit(); //请求将capacity()减小为与size()相同大小
c.capacity(); //不重新分配内存空间的话, c可以保存多少元素
c.reserve(n); //分配至少能容纳n个元素的内存空间.
reverse并不改变容器中元素的数量, 仅影响vector预先分配多大的内存空间. 只有当需要的内存空间超过当前容量时, 即 n > capacity时, reverse调用才会改变vector的容量. 若 n < capacity, reverse什么也不做.
reverse永远不会减小容器占用的内存空间. resize成员函数只改变容器中元素的数目, 而不是容器的容量. shrink_to_fit可以要求退回不需要的内存空间.
capacity指不分配新的内存空间的情况下最多可以保存多少个元素. size指已保存的元素的数目.
只要没有操作需求超出vector的容量, vector就不能重新分配内存空间.
shrink_to_fit这是请求归还内存, 标准库并不保证退还内存.
只有在执行insert操作时size和capacity相等, 或者调用resize或reserve时给定的大小超过当前的capacity, vector才会重新分配内存空间.
额外的string操作
构造string的其他方法: | |
---|---|
string s(cp, n); | s是cp指向的数组中前n个字符的拷贝, 数组至少应该包含n个字符. |
string s(s2, pos2); | s是string s2从下标pos2开始的字符的拷贝, 若pos2>s2.size(), 构造函数的行为未定义. |
string s(s2, pos2, len2); | s是string s2从下标pos2开始len2个字符的拷贝. 若pos2>s2.size(),构造函数行为未定义, 不管len2的值是多少, 构造函数最多拷贝s2.size()-pos2个字符. |
子字符串substr操作 | |
s.substr(pos, n) | 返回一个string, 包含s中从pos开始的n个字符的拷贝, pos默认值为0, 即从头开始拷贝, n的默认值为s.size()-pos, 即拷贝从pos开始的所有字符. |
修改string的操作: | |
s.insert(pos, args) | 在pos之前插入args指定的字符. pos可以是一个下标或一个迭代器, 接受下标的版本返回一个指向s的引用, 接受迭代器版本返回指向第一个插入字符的迭代器. |
s.erase(pos, len) | 删除从位置pos开始的len个字符. 若len省略, 则删除到末尾, 返回一个指向s的引用. |
s.assign(args) | 将s中的字符替换为args指定的字符, 返回一个指向s的引用. |
s.append(args) | 将args追加到s, 返回一个指向s的引用. |
s.replace(range, args) | 删除s中range范围内的字符, 替换为args指定的字符, range或者是一个下标加一个长度, 或者是一对指向s的迭代器. 返回一个指向s的引用. |
string搜索操作 | 返回值为string::size_type类型, 表示匹配发生位置的下标, 如果失败, 则返回一个名为string::npos的static成员. |
s.find(args); | 查找s中args第一次出现的位置 |
s.rfind(args); | 在s中查找args最后一次出现的位置, 逆向搜索. |
s.find_first_of(args); | 在s中查找args中任何一个字符第一次出现的位置. |
s.fin_last_of(args); | 在s中查找args中任何一个字符最后一次出现的位置. |
s.find_first_not_of(args); | 在s中查找第一个不在args中的字符. |
s.find_last_not_of(args); | 在s中查找最后一个不在args中的字符. |
args必须是一下形式之一 | |
c, pos | 从s中位置pos查找字符c, pos默认为0 |
s2, pos | 从s中位置pos查找字符串s2, pos默认为0 |
cp, pos | 从s中位置pos查找C风格字符串cp, pos默认为0 |
cp, pos, n | 从s中位置pos查找C风格字符串cp前n个字符, pos和n无默认值 |
string的比较操作: | == != < > >= <= 这些比较运算符可以直接用于字符串比较, 按照字典顺序进行比较. |
s.compare(s2) | 比较s和s2 |
s.compare(pos1, n1, s2) | s中从pos1开始的n1个字符和s2进行比较 |
s.compare(pos1, n1, s2, pos2, n2) | s中从pos1开始的n1个字符和s2中从pos2开始的n2个字符比较 |
s.compare(cp) | cp为以空字符结尾的字符数组 |
s.compare(pos1, n1, cp) | 从pos1开始的n1个字符与cp指向的以空字符结尾的字符数组进行比较 |
s.compare(pos1, n1, cp, n2) | s中从pos1开始的n1个字符与cp指向的地址开始的内n2个字符进行比较 |
string转换成数值: | 要转换为数值的string中第一个非空白字符必须是数值中可能出现的字符(+或-)或数字, 转换的范围为第一个非空白字符到第一个费数字之间或结尾. |
to_string(val) | 一组重载函数, 返回val的string表示. |
stoi(s, p, b) | 返回s的起始子串(表示整数内容)的数值, 返回值分别是int, long, unsigned long, long long, unsigned long long, b表示 |
stol(s, p, b) | 转换所用的基数, 默认值为10, p是size_t指针, 用来保存s中第一个非数值的下标, p默认为0, 即不保存下标 |
stoul(s, p, b) | |
stoll(s, p, b) | |
stof(s, p) | 返回s的起始子串(表示浮点数内容)的数值, 返回值分别是float, double, long double. |
stod(s, p) | |
stold(s, p) |
容器定义和初始化: | |
---|---|
C c; | 默认构造函数. |
C c1(c2); | c1初始化为c2的拷贝. c1和c2必须是相同的类型. |
C c1 = c2; | c1初始化为c2的拷贝. c1和c2必须是相同的类型. |
C c{a, b, c.....}; | 列表初始化c. |
C c = {a, b, c......}; | |
C c(b, e); | b, e为迭代器. |
C seq(n); | seq包含n个元素, 这些元素进行值初始化. |
C seq(n, t); | seq包含n个初始化为t的元素. |
容器的赋值运算 | |
---|---|
c1 = c2; | |
c = {a, b, c......}; | |
swap(c1, c2); | |
c1.swap(c2); | |
seq.assign(b, e); | 将seq中的元素替换为迭代器b和e所表示的范围中的元素. |
seq.assign(il); | 将seq中的元素替换为初始化列表il中的元素. |
seq.assign(n, t); | 将seq中的元素替换为n个值为t的元素. |
向顺序容器添加元素: | |
---|---|
c.push_back(t); | |
c.emplace_back(args); | |
c.push_front(t); | |
c.emplace_front(args); | |
c.insert(p, t); | 在迭代器p指向的元素之前创建一个值为t或有args创建的元素, 返回指向新添加元素的迭代器. |
c.emplace(p, args); | |
c.insert(p, n, t); | 在迭代器p指向的元素之前插入n个值为t的元素, 返回指向新添加的第一个元素的迭代器. |
c.insert(p, b, e); | 将迭代器b和e之间的元素插入到迭代器p指向的元素之前, b和e不能指向c中的元素. 返回指向新添加的第一个元素的迭代器. 若范围为空, 则返回p. |
c.insert(p, il); | il是一个花括号包围的元素值列表, 将这些给定的定值插入到迭代器p指向的元素之前. 返回指向新添加的第一个元素的迭代器. 若范围为空, 则返回p. |
顺序容器中访问元素的操作: | |
---|---|
c.back(); | 返回c中尾元素的引用, 若c为空, 函数行为未定义. |
c.front(); | 返回c中首元素的引用, 若c为空, 函数行为未定义. |
c[n]; | 返回c中下标为n的元素的引用, 若n>=c.size(), 函数行为未定义. |
c.at(n); | 返回c中下标为n的元素的引用, 若下标越界, 抛出一个out_of_range异常. |
注意: 访问成员函数返回的都是引用, 可以用来改变元素的值.
顺序容器删除元素: | |
---|---|
c.pop_back(); | 删除c中尾元素, 若c为空, 函数行为为定义. 返回void. |
c.pop_front(); | 删除c中首元素, 若c为空, 函数行为未定义. 返回void. |
c.erase(); | 删除迭代器p所指定的元素, 返回一个被删元素之后元素的迭代器, 若p指向尾元素, 则返回尾后迭代器, 若p为尾后迭代器, 则函数行为未定义. |
c.erase(b, e); | 删除迭代器b和e之间的元素, 返回最后一个被删元素之后元素的迭代器. 若e本身是尾后迭代器, 则函数也返回尾后迭代器. |
c.clear(); | 删除c中的所有元素, 返回void. |
顺序容器大小操作: (不适用于array) | |
---|---|
c.resize(n); | 调整c的大小为n个元素, 若n<c.size(),则多出的元素被丢弃, 若必须添加新的元素, 对新元素进行值初始化. |
c.erase(n,t); | 调整c的大小为n个元素, 任何新添加的元素都值初始化为值t. |
容器适配器
标准库定义了3中容器适配器: stack, queue和priority_queue.
适配器是一种机制, 能使某种事物的行为看起来像另外一种事物一样. 一个容器适配器接受一种已有的容器类型, 使其行为看起来像是另一种不同的类型.
每个适配器有两个构造函数: 默认构造函数创建一个空对象, 接收一个容器的构造函数拷贝该容器来初始化适配器.
stack<int> stk(deq); // 从deq拷贝元素到stk
默认情况下, stack和queue是基于deque实现的, priority_queue是在vector上实现的. 可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数, 来重载默认容器类型.
stack<string, vector<string>> str_stk; //在vector上实现的空栈
stack<string, vector<string>> str_stk2(str_vec) //str_stk2在vector上实现, 初始化时保存svec的拷贝
适配器的通用操作:
a.empty()
a.size()
swap(a, b)
a.swap(b)
栈适配器操作 | 包含在头文件stack, 栈默认基于deque实现, 也可以在list或vector上实现 |
---|---|
stack |
声明一个int类型的空栈. |
s.pop() | 删除栈顶元素, 但不返回该元素值 |
s.push(item) | 创建一个新元素压入栈顶, 通过拷贝或构造 |
s.emplace(args) | |
s.top() | 返回栈顶元素, 但不删除该元素 |
队列适配器(queue和priority_queue) | |
---|---|
q.pop() | 返回queue的首元素或priority_queue的最高优先级元素, 但不删除此元素 |
q.front() | 返回首元素, 但不删除此元素 |
q.back() | 返回尾元素, 只适用于queue |
q.top() | 返回最高优先级元素, 但不删除该元素, 只适用于priority_queue |
q.push(item) | 在queue末尾或priority_queue中恰当的位置创建一个元素 |
q.emplace(args) |
queue默认基于deque实现, priority_queue默认基于vector实现
queue也可以用list或vector实现, priority_queue也可以用deque实现