1. 顺序容器概述
顺序容器提供了控制元素存储和访问顺序的能力,容器是一些特定类型对象的集合。
访问顺序不依赖于元素的值,与元素加入容器时的位置相关。
vector | 可变大小数组,支持快速随机访问。 |
deque | 双端队列,支持快速随机访问,在头尾位置插入/删除速度很快 |
list | 双向链表,只支持双向顺序访问。 |
forward_list | 单向链表,只支持单向顺序访问。 |
array | 固定大小数组,支持快速随机访问,不能添加或删除元素。 |
string | 可变大小,专门用于保存字符,能随机访问。 |
string和vector将元素保存在连续的内存空间中,可以用元素的下标来计算其地址。
list和forward_list能在容器的任何位置添加或删除元素,但不支持元素的随机访问。
array与内置数组相比,更安全、更容易、性能更优良。
使用哪种顺序容器规则
通常,使用vector是最好的选择。
如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。
如果程序要求随机访问元素,应使用vector或deque。
如果程序要求在容器的中间位置插入或删除元素,应使用list或forward_list。
如果不确定应该使用哪种容器,在程序中可以只使用vector和list,使用迭代器来进行容器的操作。
2.容器库概览
容器类型上的操作基本上分为公有操作和私有操作。
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。
容器均定义为模板类。
顺序容器可以保存任意类型的元素。
vector<vector<string> > lines;
iterator | 容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存容器类型最大的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型,list<string>读取元素 |
reference | 元素的左值类型,类似于元素的引用 |
const_reference | 元素的const左值类型 |
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear() | 删除c中的所有元素,返回void |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin() ,c.rend() | 返回指向c的尾元素和首元素之间位置的迭代器 |
c.crbegin() , c.crend() | 返回const_reverse_iterator |
2.1、迭代器
forward_list迭代器不支持递减运算符。
一个迭代器范围由一对迭代器表示,迭代器指向同一个容器中的首元素或尾元素之后的位置。
这种元素范围被称为左闭合区间。
[begin,end)
确保一对迭代器指向同一个容器,且end不在begin之前,是程序员的责任。
2.2、容器类型成员
每个容器都定义了多个类型:如size_type、iterator、const_iterator等。
在使用这些类型时,必须显式使用其类名:
list<string>::iterator iter;
//iter是通过list<string>定义的一个迭代器类型
2.3、begin和end成员
begin和end操作生成指向容器中第一个元素和尾元素之后位置的迭代器。
begin和end有多个版本:带r的版本返回反向迭代器,带c的版本返回const迭代器。
当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型。
使用以c开头的版本可以获得const版本,而不管容器的类型是什么。
2.4、容器定义和初始化
每个容器类型都定义了一个默认构造函数。
C c; |
默认构造函数,c为空容器 |
C c1(c2) C c1 = c2 |
c1初始化为c2的拷贝。c1和c2是相同类型,保存元素也是相同类型 (对于array,两者也具有相同大小) |
C c = {a,b,c...} | c初始化为初始化列表中元素的拷贝 |
C c(b,e) | c初始化为迭代器b和e指定范围中的元素的拷贝 |
C seq(n) | seq包含n个元素,这些元素进行了值初始化,该构造函数是explicit的 |
C seq(n,t) | seq包含n个初始化为值t的元素 |
将容器中的元素通过一对迭代器指定的元素范围拷贝给新容器中,不要求容器、元素类型相同。
新容器的大小应与范围中元素的数目相同。
在新标准中,可以对一个容器进行列表初始化,初始化列表还隐含地指定容器大小。
list<string> authors = {"milton","austen","shakespeare"};
顺序容器提供了另一个构造函数,它接受一个容器大小和一个元素初始值。
vector<int> ivec(10,-1); //10个int元素,每个初始化为-1
如果元素类型没有默认构造函数,除了大小参数外,还必须指定一个显式的元素初始值。
只有顺序容器的构造函数才接受大小参数,关联容器并不支持。
当定义array时,要显式指定元素类型和容器大小。
array<int,10>; //类型为:保存10个int的数组
array<int,10>::size_type io;
array大小固定的特性影响了它所定义的构造函数的行为;默认构造的array是非空的。
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无这样的限制。
array<int,10> digits = {1,2,3,4,5,6,7,8,9};
array<int,10> copy = digits;
该操作要求初始值的类型必须要与创建的容器类型相同,元素类型和大小一致。
2.5、赋值和swap
标准库array类型允许赋值,赋值号左右两边的运算对象必须具有相同的类型。
如果两边对象的大小不同,array类型不支持assign,也不允许用花括号包围的值列表进行赋值。
c1 = c2 | 将c1中的元素替换成c2中元素的拷贝,c1与c2具有相同的类型 |
c = {a,b,c...} | 将c1中元素替换为初始化列表中元素的拷贝 |
swap(c1,c2) c1.swap(c2) |
交换c1和c2中的元素,c1与c2具有相同的类型 swap速度比拷贝更快 |
seq.assign(b,e) |
将seq中的元素替换为迭代器b和e的范围内的元素 迭代器b和e不能指向seq |
seq.assign(i) | 将seq中的元素替换为初始化列表i中的元素 |
seq.assign(n,t) | 将seq中的元素替换为n个值为t的元素 |
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。
swap操作将容器内容交换不会导致上述情况发生。(array和string除外)
顺序容器定义了assign成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。
list<string> names;
vector<const char*> olds;
names.assign(olds.cbegin(),olds.cend()); //成员元素类型不同但相容,不能直接拷贝
由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。
assign的重载版本接受一个整型值和一个元素值,用指定的数目与元素替换原有元素。
list<string> slist(1); //一个空string
slist.assign(10,"hello"); //10个hello
swap操作交换两个相同类型容器的内容,元素并未交换,只是交换两个容器的内部数据结构。
使用swap操作意味着指向容器的迭代器、引用和指针不会失效,指向同样的元素,但元素属于不同的容器。
与其他容器不同,swap两个array会真正交换它们的元素。
2.6、容器大小操作
每个容器类型都有三个与大小相关的操作:size、empty、max_size。
forward_list支持max_size和empty,但不支持size。
2.7、关系运算符
每个容器类型都支持相等运算符(==或!=)。
除了无序关联容器外的所有容器都支持关系运算符(>、>=、<、<=)。
关系运算符左右两边的运算对象必须是相同类型的容器、且保存相同类型的元素。
比较两个容器实际上是进行元素的逐对比较。
只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器。
3.顺序容器操作
3.1、向顺序容器添加元素
除了array,所有标准库容器都提供灵活的内存管理,可以动态添加或删除元素来改变容器大小。
c.push_back(t) c.emplace_back(args) | 在c的尾部创建一个值为t或由args创建的元素,返回void |
c.push_front(t) c.emplace_front(args) | 在c的头部创建一个值为t或由args创建的元素,返回void |
c.insert(p,t) | 在迭代器之前创建一个值为t的元素,返回指向新添加的元素迭代器 |
c.insert(p,n,t) |
在迭代器p指向的元素之前插入n个值为t的元素, 返回指向新添加的第一个元素的迭代器,若n为0,返回p |
c.insert(p,b,e) |
将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。 b和e不能指向c中的元素。 |
c.insert(p,i1) |
i1是一个元素值列表,将这些给定值插入到迭代器p指向的元素之前 |
除array和forward_list之外,每个顺序容器都支持push_back。
push_back在string末尾添加字符。
当我们用一个对象来初始化容器或插入容器中,实际上放入容器中的是拷贝,而不是对象本身。
list、forward_list和deque容器支持push_front操作。
insert成员允许我们在容器中任意位置插入0个或多个元素。
每个insert函数都接受一个迭代器p来指出元素具体插入位置,p能指向任意位置,包含容器尾部之后的下一个位置。
insert函数将元素插入到迭代器所指定的位置之前。
虽然某些容器不支持push_front操作,但它们对于insert操作并无类似的限制。
如果我们传递给insert一对迭代器,他们不能指向添加元素的目标容器。
接受元素个数或范围的insert版本返回指向第一个新加入元素的迭代器。
当我们调用一个emplace成员函数时,是将参数传递给元素类型的构造函数,在容器中构造元素。
emplace函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。
3.2、访问元素
c.back() | 返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front() |
返回c中首元素的引用。若c为空,函数行为未定义 |
c.[n] |
返回c中下标为n的元素的引用,n是无符号整数,且n<c,size() |
c.at(n) | 返回下标为n的元素的引用,如果下标越界,则抛出out_of_range异常 |
at和下标操作只适用于string、vector、deque和array;back不适用于forward_list。
访问成员函数返回的是引用,不是迭代器。
保证下标有效是程序员的责任,编译器并不检查这种错误。
3.3、删除元素
c.pop_back() | 删除c中尾元素。若c为空,则函数行为未定义,返回void |
c.pop_front() | 删除c中首元素,若c为空,则函数行为未定义,返回void |
c.erase(p) |
删除迭代器p所指定的元素,返回一个指向被删元素之后元素的迭代器, 若p指向尾元素,则返回尾后迭代器。 |
c.erase(b,e) |
删除迭代器b和e所指定范围内元素,返回一个指向最后一个被删除的元素之后 元素的迭代器,若e本身是尾后迭代器,则返回e |
c.clear() | 删除c中的所有元素,返回void |
forward_list不支持pop_back;vector和string不支持pop_front。
删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。
删除元素的成员函数并不检查其参数,因此我们必须确保参数是存在的。
3.4、特殊的forward_list操作
当我们添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生改变,我们需要访问添加或删除的元素的前驱,改变前驱的链接。
forward_list定义了before_begin,它返回一个首前迭代器。
list.before_begin() |
返回指向链表首元素之前不存在的元素的迭代器,此迭代器不能解引用。 cbefore_begin()返回一个const_iterator. |
list.insert_after(p,t) list.insert_after(p,n,t) list.insert_after(p,b,e) list.insert_after(p,il) |
在迭代器p之后插入元素。 t是一个对象,n是数量,b和e是表示范围的一对迭代器(b和e不能指向list),il是一个列表,返回一个指向最后一个插入元素的迭代器。 若范围为空,返回p。 |
emplace_after(p,args) | 使用args在p指定的位置之后创建一个元素,返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数行为未定义。 |
list.erase_after(p) list.erase_after(b,e) |
删除p指向的位置之后的元素。 删除从b之后到e(不包含e)之间的元素,返回一个指向被删元素之后的元素的迭代器。(包含尾后迭代器) |
当我们在forward_list中添加或删除元素时,关注指向要处理的元素、指向其前驱 的两个迭代器。
3.5、改变容器大小
c.resize(n) |
调整c的大小为n个元素,若n<c,size(),则多出的元素被丢弃。 若必须添加新元素,对新元素进行值初始化。 |
c.resize(n,t) | 调整c的大小为n个元素,任何新添加的元素都初始化为值t |
resize操作接受一个可选的元素值参数,用来初始化添加到容器中的元素。
list<int>ilist(10,42); //10个int,值为42
ilist.resize(15); //将5个0添加到容器末尾
ilist.resize(20,-1); //将5个-1添加到容器末尾
ilist.resize(5); //从ilist末尾删除15个元素
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效。
3.6、容器操作可能使迭代器失效
对于vector或string来说,如果存储空间被重新分配,那么迭代器、指针和引用都失效;
若空间未被重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但之后的将会失效
对于deque来说,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效;
在首尾位置添加元素,迭代器会失效,但指向存在的元素的指针和指针不会失效。
对于list和forward_list,指向容器的迭代器,指针和引用仍有效。
由于向迭代器添加或删除元素的操作可能会使迭代器失效,因此在每次改变容器的操作之后都应该重新定位迭代器。
insert函数在给定位置之前插入新元素,然后返回指向新插入元素的迭代器。
在我们添加/删除元素时,end返回迭代器总是会失效,因此不要保存end返回的迭代器。
4.vector对象是如何增长的
为了支持快速随机访问,vector将元素连续存储在一起。
vector和string的实现通常会分配比新的空间需求更大的内存空间,容器预留这些空间作为备用。
c.shrink_to_fit() | 将capacity()减少为与size()相同大小 |
c.capacity() | 不重新分配内存空间的话,c可以保存多少元素 |
c.reserve() | 分配至少能容纳n个元素的内存空间 |
shrink_to_fit只适用于vector、string和deque。
capacity和reserve只适用于vector和string。
reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间。
只有当需要的内存空间超过当前的容量时,reserve调用才会改变vector的容量。
如果需求大小小于或等于当前容量时,reserve不会执行任何指令,容器不会退回内存空间,因此调用reserve永远不会减少容器占用的内存空间。
resize成员函数只改变容器中元素的数目,而不是容器的容量。
调用shrink_to_fit只是一个请求,具体实现可能会忽略,并不保证一定会退回内存空间。
capacity是在不分配新的内存空间的前提下它最多可以保存多少元素。
vector在每次需要重新分配新内存空间时将当前容量翻倍。
5.额外的string操作
标准库string类型定义了大量函数,可以在需要使用时再回头查看。
5.1、构造string的其他方法
string s(cp,n) | s是cp指向的数组中前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()且构造函数最多拷贝s2.size() - pos2个字符 |
通常当我们从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符时停止。
当从一个string拷贝字符时,可以提供一个可选的开始位置和一个计数值,若开始位置大于size,则构造函数抛出out_of_range异常。
s.substr(pos,n) |
返回一个string,包含s中从pos开始的n个字符的拷贝。 pos默认值为0,n的默认值为s.size() - pos. |
5.2、改变string的其他方法
除了接受迭代器的insert和erase版本外,string还提供了接受下标的版本。
s.insert(s.size(),5,'o'); //在s末尾插入5个o
s.erase(s.size() - 5,5); //在s删除最后5个字符
标准库string类型提供接受C风格字符数组的insert和assign版本。
const char *cp = "hello world";
s.assign(cp,5); //s = "hello"
s.insert(s.size(),cp+5); //s = "hello world"
允许指定将来自其他string或子字符串的字符插入到当前的string或赋予当前string。
string s = "hello world";
string s1 = " some string";
s.insert(0, s1); //在s中位置0之后插入s1的拷贝
s.insert(0,s1,0,s1.size()); //在s[0]之后插入s1[0]开始的s1.size()个字符
string类的成员函数append用于在string末尾进行插入操作的一种简化。
s2.append("world"); //将world追加到s2末尾
string的成员函数replace是调用erase和insert的一种简写方式,类似替换作用。
s2.replace(5,3,"gas");
//从位置5开始,删除3个字符并插入"gas"
replace函数的第三个参数文本不一定要与删除文本的长度一致。
assign总是替换string中的所有内容,append总是将新字符追加到string末尾。
5.3、string搜索操作
string提供了多个搜索函数,每个搜索操作都返回一个string::size_type值,表示搜索值的下标。
若搜索失败,则返回一个名为string::npos的static成员。
npos类型为const string::size_type,并初始化值为-1,是unsigned类型。
搜索(以及其他string操作)是大小写敏感的。
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中的字符 |
rfind成员函数从右向左搜索,相当于反转来查找。
5.4、compare函数
string的compare函数类似C的strcmp函数,根据s是等于、大于还是小于参数指定的字符串,返回0、正数或负数。
compare函数有以下6个版本:
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之间的转换。
int i = 42;
string s = to_string(i); //整数转换成字符表示形式
double d = stod(s); //将字符串s转换成浮点数
如果string不能转换为一个数值,这些函数会抛出invalid_argument异常。
to_string(val) | 一组重载函数,返回数值val的string表示,val可以是任何算术类型 |
stoi(s,p,b) |
返回s的起始子串的数值,返回类型是int。 p是size_t指针,用来保存s中第一个非数值字符的下标。 |
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 |
6.容器适配器
适配器是标准库中一个通用概念,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物。
容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
标准库中定义了三个顺序容器适配器:stack、queue和priority_queue。
size_type | 一种类型,足以保存当前类型的最大对象的大小 |
value_type | 元素类型 |
container_type | 实现适配器的底层容器类型 |
A a; | 创建一个名为a的空适配器 |
A a(c); | 创建一个适配器,带有容器c的一个拷贝 |
a.empty() | 若a包含任何元素,返回false,否则返回true |
a.size() | 返回a中的元素数目 |
swap(a,b) a.swap(b) |
交换a和b的内容,a和b必须有相同的类型,底层容器类型也必须相同 |
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。
在创建一个适配器时,将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型。
stack<string,vector<string>> str; //在vector上实现的空栈
stack可以使用除array和forward_list之外的任何容器类型来构造。
queue可以构造于list或deque之上,但不能基于vector构造。
priority_queue可以构造于vector或deque之上,但不能基于list构造。
stack类型定义在stack头文件中。
s.pop() | 删除栈顶元素,但不返回该元素值 |
s.push(item) s.emplace(args) | 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或由args构造 |
s.top() | 返回栈顶元素,但不将元素弹出栈 |
每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作,不能使用底层容器的操作。
queue和priority_queue适配器定义在queue头文件中。
q.pop() |
返回queue的首元素或priority_queue的最高优先级元素, 但不删除该元素 |
q.front() | 返回首元素,但不删除 |
q.back() | 返回尾元素,但不删除 |
q.top() | 返回最高优先级元素,但不删除 |
q.push(item) q.emplace(args) | 在queue末尾或priority_queue中创建一个元素,其值为item,或由args构造 |
标准库queue使用一种先进先出的存储和访问策略。
priority_queue允许我们为队列中的元素建立优先级,新加入的元素会排在所有优先级比它低的已有元素之前。