一、顺序容器概述
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
顺序容器类型:
容器类型 | 说明 |
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能会很慢。 |
deque | 双端列表。支持快速随机访问。在头尾位置插入/删除速度很快。 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素。 |
string | 与vector相似的容器,但专门用于保存字符。支持快速随机访问。在尾部插入/删除速度快。 |
string和vector将元素保存在连续的内存空间中。由于元素是连续存储的,由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时。
list和forward_list这两个容器的设计目的是令容器任何位置的添加和删除元素都很快速。作为代价,这两个容器不支持元素的随机访问:为了访问一个元素,我们只能遍历整个容器。而且,与vector、deque和array相比,这两个容器的额外内存开销也很大。
deque是一个更为复杂的数据结构。与string和vector类似,在deque的中间位置添加或删除元素的代价(可能)很高。但是,在deque的两端添加或删除元素都是很快的(只花费常数时间),与list和forward_list添加删除元素的速度相当。
forward_list和array是新C++标准增加的类型。与内置数组相比,array是一种更安全、更容易使用的数组类型。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。
二、容器库概述
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。
顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器。这种容器的定义与任何其他容器类型完全一样。但某些容器操作对元素类型有其自己的特殊要求。我们可以为不支持特定操作需求的类型定义容器,但这种情况下就只能使用那些没有特殊要求的容器操作了。
所有容器类型(也包括关联容器)都支持的操作:
类型别名 | 说明 |
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type&含义相同 |
const_reference | 元素的const左值类型(即,const value_type&) |
构造函数 | |
C c; | 默认构造函数,构造空容器(array不支持) |
C c1(c2); | 构造c2的拷贝c1 |
C c(b, e); | 构造c,将迭代器b和e指定的范围内的元素拷贝到c(array不支持) |
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中元素的数目(不支持forward_list) |
c.max_size() | c可保存的最大元素数目 |
c.empty() | 若c中存储了元素,返回false,否则返回true |
添加/删除元素(array不支持) 注:在不同容器中,这些操作的接口都不同 |
|
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中的所有元素,返回void |
关系运算符 | |
==, != | 所有容器都支持相等(不相等)运算符 |
<, <=, >, >= | 关系运算符(无序关联容器不支持) |
获取迭代器 | |
c.begin(), c.end() | 返回指向c的首元素和尾元素之后位置的迭代器 |
c.cbegin(), c.cend() | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(), c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(), c.crend() | 返回const_reverse_iterator |
1、迭代器
标准容器迭代器的运算符:
运算符 | 说明 |
*iter | 返回迭代器iter所指元素的引用 |
iter->mem | 解引用iter并获取该元素的名为mem的成员,等价于(*iter).mem |
++iter | 令iter指向容器的下一个元素 |
--iter | 令iter指向容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等(不相等),如果两个迭代器指示的是同一个元素或者它们是同一个容器的尾后迭代器,则相等;反之,不相等 |
iter1 != iter2 |
其中,forward_list迭代器不支持递减运算符(--)。
string、vector、deque、array的迭代器支持的算术运算:
算术运算 | 说明 |
iter + n | 迭代器加上一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向前移动了若干个元素。结果迭代器或者指示容器内的一个元素,或者等于容器尾后迭代器 |
iter - n | 迭代器减去一个整数值仍得一个迭代器,迭代器指示的新位置与原来相比向后移动了若干个元素 |
iter += n | 迭代器加法的复合赋值语句,将iter加n的结果赋给iter |
iter -= n | 迭代器减法的复合赋值语句,将iter减n的结果赋给iter |
iter1 - iter2 | 两个迭代器相减的结果是它们之间的距离。参与运算的必须是同一个容器的迭代器 |
>、>=、<、<= | 迭代器的关系运算符,如果某迭代器指向的容器位置在另一个迭代器所指位置之前,则说明前者小于后者。参与运算的必须是同一个容器的迭代器 |
注意:上面的迭代器的范围区间是[begin(),end()],如果运算结果不在这个区间内,会报错。
一个迭代器范围是由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置。这两个迭代器通常被称为begin和end。这种元素范围被称为左闭右合区间,其标准数学描述为:[begin, end)。
当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型,如果对象是常量,返回const_iterator;如果对象不是常量,返回iterator。cbegin和cend不论对象是否是常量,返回值都是const_iterator。
2、容器定义和初始化
操作 | 说明 |
C c | 默认构造函数。如果c是一个array,则c中元素按默认方式初始化;否则c为空 |
C c1(c2) | c1初始化为c2的拷贝。c1和c2必须是相同类型(即,它们必须是相同的容器类型,且保存的是相同的元素类型;对于array来说,两者还必须具有相同大小) |
C c1=c2 | 同上 |
C c{a, b, c, ...} | c初始化为初始化列表中元素的拷贝。列表中元素的类型必须与c的元素类型相容。对于array类型,列表中元素数目必须等于或小于array的大小,任何遗漏的元素都进行值初始化。初始化列表隐含地指定了容器的大小:容器将包含与初始值一样多的元素。 |
C c={a, b, c, ...} | 同上 |
C c(b, e) | c初始化为迭代器b和e指定范围中的元素的拷贝。范围中元素的类型必须与c的元素类型相容(array不适用) |
只有顺序容器(不包括array)的构造函数才能接受大小参数 | |
C seq(n) | seq包含n个元素,这些元素进行了值初始化;此构造函数是explicit的 |
C seq(n, t) | seq包含n个初始化为值t的元素 |
将一个容器初始化为另一个容器的拷贝的例子:
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <deque> 5 #include <list> 6 #include <forward_list> 7 #include <array> 8 9 int main() 10 { 11 std::list<std::string> authors = { "abc", "QAQ", "hello" }; 12 std::vector<const char*> articles = { "a", "ab", "the" }; 13 14 std::list<std::string> list2(authors); // 类型匹配 15 //std::deque<std::string> auth(authors); // 错误:容器类型不匹配 16 std::forward_list<std::string> words(articles.begin(), articles.end()); // 元素类型可以相互转换 17 return 0; 18 }
1)标准库array具有固定大小
与内置数组一样,标准库array的大小也是类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小。使用array时也要指定容器大小。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <deque> 5 #include <list> 6 #include <forward_list> 7 #include <array> 8 9 int main() 10 { 11 std::array<int, 10> arr; // 类型为10个int的数组 12 std::array<int, 10>::size_type i; // 数组类型包括元素类型和大小 13 return 0; 14 }
一个默认构造的array是非空的:它包含了与其大小一样多的元素。这些元素都被默认初始化。如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩下的元素都会进行值初始化。
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <deque> 5 #include <list> 6 #include <forward_list> 7 #include <array> 8 9 int main() 10 { 11 std::array<int, 10> arr = { 2, 3 }; 12 std::array<int, 10> copy = arr; 13 return 0; 14 }
3、赋值和swap
赋值运算符将其左边容器中的全部元素替换为右边容器中元素的拷贝。
所有容器都支持的赋值运算:
操作 | 说明 |
c1=c2 | 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的容器类型,且保存的是相同的元素类型 |
c={a, b, c, ...} | 将c1中的元素替换为初始化列表中元素的拷贝(array不适用)。保存的必须是相同的元素类型 |
swap(c1, c2) | 交换c1和c2中的元素。c1和c2必须具有相同的类型。swap通常比从c2向c1拷贝元素快得多 |
c1.swap(c2) | 同上 |
assign操作不适用于关联容器和array | |
seq.assign(b, e) | 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。保存的元素类型必须相容。 |
seq.assign(items) | 将seq中的元素替换为初始化列表items中的元素 |
seq.assign(n, t) | 将seq中的元素替换为n个值为t的元素 |
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。
swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。
1)使用assign(仅顺序容器)
顺序容器(array除外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 #include <deque> 5 #include <list> 6 #include <forward_list> 7 #include <array> 8 9 int main() 10 { 11 std::vector<int> vec = { 1, 2, 3 }; 12 std::vector<double> vec2 = { 4.5, 5.5, 6.5 }; 13 // vec = vec2; // 错误:类型不同 14 std::list<double> lst = { 7, 8, 9 }; 15 vec.assign(lst.begin(), lst.end()); 16 return 0; 17 }
2)使用swap
除array外,交换两个容器内容的操作保证会很快----元素本身并未交换,swap只是交换了两个容器的内部数据结构。
元素不会被移动的事实意味着,除string外,指向容器的迭代器、引用和指针在swap操作都不会失效。他们仍然指向操作之前所指向的那些元素。但是,在swap之后,这些元素已经属于不同的容器了。
对一个string调用swap会导致迭代器、引用和指针失效。
swap两个array会真正交换它们的元素。因此,交换两个array所需的时间与array中元素的数目成正比。
因此,对于array,在swap操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array中对应元素的值进行了交换。
5、关系运算符
每个容器类型都支持相等运算符(==和!=);除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。关系运算符左右两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素。
比较两个容器实际上是进行元素的比较。这些运算符的工作方式与string的关系运算类似:
a、如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
b、如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
c、如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符比较两个容器。
容器的相等运算实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。如果元素不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。