序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素值,而是与元素加入容器时的位置相对应。
9.1 概述
所有顺序容器都提供了快速顺序访问元素的能力。但是,这些容器在以下方面都有不同的性能折中:
-向容器添加或从容器删除元素的代价;
-非顺序访问容器中元素的代价。
顺序容器类型:
vector 可变大数组。支持随机快速访问。在尾部之外的位置插入或删除元素可能很慢。
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快。
list 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快。
forawrd_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快。
array 固定大小数组。支持快速随机访问。不能添加或删除元素。
string 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快。
array固定大小;vector和string在中部插入/删除需要调整元素位置保持连续性;list和forward_list元素散布,依靠指针维持前后顺序,内存开销较大;deque支持快速随机访问,但支持头尾两端快速插入/删除,中部插入/删除也较慢(其结构较复杂)。
forward_list和array是C++11新增加。array与内置数组相比,空间是在堆上分配。forward没有size操作,其设计用来达到与最好的手写单向链表性能接近,没有多余操作。
对顺序容器使用时使需求和容器性能相适应,还可以使用多个容器配合使用,容器之间内容的复制很方便。
9.2 容器库概览
容器类型上的操作形成了一种层次:
-某些操作是所有容器类型都提供的。
-另外一些操作仅针对顺序容器、关联容器或无序容器。
-还有一些操作只适用于一小部分容器。
容器操作:(常用的)
类型别名
iterator 迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_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(array不支持);
C c{a,b,c...}; 列表初始化;
赋值与swap
c1=c2 将c1中的元素替换为c2中元素
c1={a,b,c...} 将c1中的元素替换为列表中元素(不适用于array)
a.swap(b) 交换a和b的元素
swap(a,b) 交换a和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
9.2.1迭代器
标准迭代器的运算符:*it.smt ,it->smt, ++, --, == ,!=
forward_list不支持迭代器的--,单向链表(单向指针)
一个迭代器范围由一堆迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。左闭右开区间。
9.2.4 容器定义和初始化
只有顺序容器的构造函数才能接受大小参数。
C c; C c1(c2); C c1=c2; C c{a,b,c,...}; C c={a,b,c...}; C c(b,e);迭代器区间内容复制;
顺序容器特有的构造函数: C seq(n);默认初始化; C seq(n,t);n个元素,每个元素初始化为t。
标准库array具有固定大小:当定义一个array时,必须指定其大小
array<int,42> //类型为:保存42个int的数组
array<string,10> //保存10个string的数组
与其它容器不同,一个默认构造的array是非空的:它包含了于其大小一样多的元素。这些元素都被默认初始化,就像一个内置数组那样。
如果我们对array进行列表初始化,初始值的数目必须等于或小于array的大小。如果初始值数目小于array的大小,则它们被用来初始化array中靠前的元素,所有剩余元素都会进行值初始化。在这两种情况下,如果元素类型是一个类类型,那么该类必须有一个默认构造函数,以使值初始化能够进行。
9.2.5 赋值和swap
容器赋值运算:
c1=c2; 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型。
c1={a,b,c...}; 将c1中元素替换为列表中元素的拷贝(array不适用,但array构造时可用列表形式构造,列表元素数目小于等于array的大小)。
swap(c1,c2);c1.swap(c2); 交换c1和c2。
assign操作不适用于关联容器和array
seq.assign(b,e); 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素。
seq.assign(il); 将seq中的元素替换为初始化列表il中的元素。
seq.assign(n,t); 将seq中的元素替换为n个值为t的元素。
赋值运算符要求左边和右边的运算对象具有相同类型。顺序容器(除array外)还定义了一个名为assign的成员,允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值。
swap交换两个相同类型容器的内容。除array外,swap不对任何元素进行拷贝、删除或插入操作,因此可以保证在常数时间内完成。
元素不会被移动意味着,除了string外,指向容器的迭代器、引用和指针在swap操作后都不会失效。而对一个string调用swap会导致迭代器、引用和指针失效。
而swap两个array会真正交换它们的元素。因此,交换两个array的时间与元素数目成正比。
在新标准中,容器即提供成员函数版本的swap,也提供非成员版本的swap。
9.3 顺序容器操作
9.3.1 向顺序容器添加元素
这些操作会改变容器大小,所以array不支持这些操作。
forward_list有自己专有版本的insert和emplace;forward_list不支持push_back和emplace_back。
vector和string不支持push_front和emplace_front。
c.push_back(t)
c.emplace_back(inits);
c.push_front(t);
c.emplace_front(inits);
c.insert(p,t); c.emplace(args); 在迭代器p指向的元素之前创建一个值为t或由args创建的元素。返回指向新添加的元素的迭代器。
c.insert(p,n,t); 在迭代器p指向的元素之前插入n个值为t的元素,返回指向新添加的第一个元素的迭代器。若n为0,则返回p。
c.insert(p,b,e); 在迭代器p指向的元素之前插入由迭代器b和e指向的区间,返回指向新添加的第一个元素的迭代器。b和e不能是p所在容器的迭代器。
c.insert(p,il); il是一个花括号包围的元素值列表。将这些给定值插入到迭代器p指向的元素之前。返回指向新添加的第一个元素的迭代器;若列表为空,则返回p。
向一个vector、string和deque插入元素会使所有指向容器的迭代器、引用和指针失效。
在一个vector或string的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。
emplace系列函数是将参数传递给元素类型的构造函数,emplace成员使用这些参数在容器管理的内存空间中直接构造函数。
而push_back、push_front、insert会创建一个临时对象,并将对象压入要添加的容器中。
9.3.2 访问元素
如果容器中没有元素,访问操作的结果是未定义的。
包括array在内的每个顺序容器都有一个front成员函数,而除了forward_list之外所有顺序容器都有一个back成员函数。这两个函数分别返回首元素和尾元素的引用。
在顺序容器中访问元素的操作:
at和下标操作只适用于string、vector、deque和array。
back不适用于forward_list。
c.back()
c.front();
c[n]; //n越界时函数行为未定义
c.at(n); //如果下标越界,则抛出一out_of_range异常。
访问成员函数返回的是引用。
9.3.3 删除元素
非array容器有很多删除元素的方式。
顺序容器的删除操作:
这些操作会改变容器的大小,所以array不使用。
forward_list有特殊版本的erase,forward_list不支持pop_back;vector和string不支持pop_front。
c.pop_back();
c.pop_front();
c.erase(p); 删除p所指的元素,返回一个指向被删元素之后元素的迭代器。
c.erase(b,e); 删除迭代器b和e所指范围内的元素。返回一个指向最后一个被删元素之后的元素的迭代器,若e本身就是尾后迭代器,则函数行为未定义。
c.clear();
删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效。指向vector或string中删除点之后位置的迭代器、引用和指针都会失效。
删除元素之前,必须由程序员来检查元素是否存在。
9.3.4 特殊的forward_list操作
forward_list中插入或删除元素的操作:
lst.before_begin() 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不能解引用。cbefore_begin()返回一个const_iterator。
lst.cbefore_begin()
lst.insert_after(p,t) lst.insert_after(p,n,t) lst.insert_after(p,b,e) lst.insert_after(p,il) 在迭代器p之后的位置插入元素。t是一个对象,n是数量,b和e是表示一个范围的迭代器(b和e不能是在lst内),il是一个花括号列表。返回一个指向最后一个插入元素的迭代器。如果范围为空,则返回p。若p为尾后迭代器,则函数未定义。
emplace_after(p,args) 使用args在p指定的位置之后创建一个元素。返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数未定义。
lst.erase_after(p) lst.erase_after(b,e) 删除p指向的位置之后的元素,或删除从b之后直到(但不包含)e之间的元素。返回一个指向被删元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代器。如果p指向lst的尾元素或者是一个尾后迭代器,则函数行为未定义。
9.3.5 改变容器大小
顺序容器大小操作:
resize不适用于array
c.resize(n) 调整c的大小为n个元素。若n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化。
c.resize(n,t) 调正c的大小为n个元素。任何新添加的元素都被初始化为值t。
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能会导致迭代器、指针和引用失效。
9.3.6 容器操作可能使迭代器失效
在向容器添加元素后:
-如果容器是vector或string。且存储空间被重新分配,则指向容器的迭代器、指针和引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍有效,但指向插入位置之后的迭代器、指针和引用将会失效。
-对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
-对于list和forward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针和引用仍有效。
当我们删除一个元素后:
-对于list和forward_list,指向容器其它位置的迭代器(包括尾后迭代器和直前迭代器)、引用和指针仍有效。
-对于deque,如果在首尾之外的任何位置删除元素,那么指向被删除元素外其它元素的迭代器、引用或指针也会失效。如果删除deque的尾元素,则尾后迭代器也会失效,但其它迭代器、引用和指针不受影响;如果是删除首元素,这些也不会受影响。
-对于vector和string,指向被删元素之前元素的迭代器、引用和指针仍有效。
注意:当我们删除元素时,尾后迭代器总是会失效。
9.4 vector对象增长
vector将元素连续地存储。分配的内存空间在某时是一定的,但容器大小可以改变。当容器插入元素而空间不足时,需要为vector或string重新分配空间且新空间比原空间大得多,多出来这部分作为备用(防止每次插入都要重新分配)。
容器大小管理操作:
shrink_to_fit只适用于vector、string和deque。
capacity和reverse只适用于vector和string。
c.shrink_to_fit() 将capacity()减少为与size相同大小。
c.capacity() 不重新分配内存空间的话,c可以保存多少元素。
c.reserve(n) 分配至少能容纳n个元素的内存空间。
只有当需要的内存空间超过当前容量时,reserve才会改变vector的容量。reserve需要小于当前容量时,什么也不做。
调用shrink_to_fit来要求deque、vector或string退回不需要的内存空间。
vector内存变长分配时,先在堆中找到一块够大的空间(原空间的1.5或2倍),将原空间的内容移过去,并将在栈中的vector变量指向这个新分配的空间。
9.5 额外的string操作
9.5.1 构造string的其它方法
string除了与其它顺序容器相同的构造函数外,还有另外3个构造函数:
n、len2和pos2都是无符号值。
string s(cp, n) s是cp指向的数组中前n个字符的拷贝。此数组至少应该包含n个字符。
string s(s2, pos2) s是string s2从下标开始的字符的拷贝。若pos2>s2.size(),构造函数的行为未定义。
string s(s2, pos2, len2) s是string s2从下标pos2开始len2个字符的拷贝。若pos2>s.size(),构造函数的行为未定义。不管len2的值是多少,构造函数至多拷贝s2.size()-pos2个字符。
substr操作
substr操作返回一个string,它是原始string的一部分或全部的拷贝,可以传递给substr一个可选的开始位置和计数值:
s.substr(pos, n) 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0.n的默认值为s.size()-pos,即拷贝从pos开始的所有字符。
9.5.2 改变string的其它方法
string类型支持顺序容器的赋值运算符以及assign、insert和erase操作。
除了接收迭代器的insert和erase版本外,string还提供了接受下标的版本,下标指出开始删除的位置,或是insert到给定值之前的位置。
标准库string类型还提供了接受C风格字符数组的insert和assign版本。const char *cp="cjjjjj";s.assign(cp,7);
修改string的操作:
s.insert(pos,args) 在pos之前插入args指定的字符。pos可以是一个下标或一个迭代器。接受下标的版本返回一个指向s的引用;接受迭代器的版本返回指向第一个插入字符的迭代器。
s.erase(pos,len) 删除从位置pos开始的len个字符。如果len被省略,则删除从pos开始直至s末尾的所有字符。返回一个指向s的引用。
s.assign(args) 将s中的字符替换为args指定的字符。返回一个指向s的引用。
s.append(args) 将args追加到s。返回一个指向s的引用。
s.replace(range, args) 删除s中范围range内的字符,替换为args指定的字符。range或者是一个下标和一个长度,或者是一对指向s的迭代器。返回一个指向s的引用。
例如:str.append("cjj"); str.replace(11,3,"5th");
9.5.3 string的搜索操作
搜索操作返回指定字符出现的下标,如果未找到则返回npos。
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中的字符。
args必须是以下形式之一:
c,pos 从s中位置pos开始查找字符c。pos默认为0
s2,pos 从s中位置pos开始查找字符串s2。pos默认为0
cp,pos 从s中位置pos开始查找指针cp指向的以空字符结尾的C风格字符串。pos默认为0
cp,pos,n 从s中位置pos开始查找指针cp指向的数组的前n个字符。pos和n无默认值。
9.5.4 compare函数
标准库string类型提供了一组compare成员函数。
compare有6个版本:3个与string比较,3个与C风格字符串比较
s.compare(s2) 比较s和s2
s.compare(pos1,n1,s2) 将s中从pos1开始的n1个字符与s2比较
s.compare(pos1,n1,s2,pos2,n2) 将s中从pos1开始的n1个字符与s1中从pos2开始的n2个字符进行比较
s.compare(cp)
s.compare(pos1,n1,cp)
s.compare(pos1,n1,cp,n2)
9.5.5 数值转换
string和数值之间的转换
to_string (val) 一组重载函数,返回数值val的string表示。val可以是任何算术类型。对每个浮点类型和int或更大的整型,都有相应版本的to_string。小整型会被提升。
stoi(s,p,b) stol(s,p,b) stoul(s,p,b) stoll(s,p,b) stoull(s,p,b) 返回s的起始子串(表示整数内容)的数值,返回值类型分别是int、long、unsigned long、long long、unsigned long long。b表示转换所用的基数,默认值为10。p是size_t指针,用来保存s中第一个非数值字符的下标,p默认值为0,即函数不保存下标。
stof(s,p) stod(s,p) stold(s,p) 返回s的起始子串(表示浮点数内容)的数值,返回值类型分别是float、double或long double。参数p的作用与整数转换函数中一样。
9.6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。适配器是标准库中的一个通用概念。容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另一种事物一样。
容器适配器底层是顺序容器实现的,如stack可以是deque或vector封住了其他功能,留下部分(从尾部插入、尾部删除、empty、size等)
所有容器适配器都支持的操作和类型:
size_type 一种类型,足以保存当前类型的最大对象的大小。
value_type 元素类型。
container_type 实现适配器的底层类型
A a; 创建一个名为a的空适配器。
A a(c); 创建一个名为a的适配器,带有容器c的一个拷贝。
关系运算符 每个适配器都支持所有关系运算符:==、!=、<、<=、>和>=这些运算符都返回底层容器的比较结果。
a.empty() 若a包含任何元素,返回false,否则返回true。
a.size() 返回a中的元素数目。
swap(a, b) a.swap(b) 交换a和b的内容,a和b必须有相同类型,包括底层容器类型也必须相同。
所有适配器都有添加和删除元素的功能:
栈stack的元素操作:
栈默认基于deque实现,也可以在list或vector之上实现。
s.pop() 删除栈顶元素
s.push(item) s.emplace(args) 创建一个新元素压入栈顶,该元素通过拷贝或移动item而来,或者由args构造
s.top() 返回栈顶元素,但不将元素弹出栈。
队列适配器:queue和priority_queue适配器定义在queue文件中。
queue默认基于deque实现,priority_queue默认基于vector实现。
queue也可用list或vector实现,priority_queue也可用deque实现。
q.pop() 删除queue的首元素或priority_queue的最高优先级元素,但不反悔此元素。
q.front() 返回首元素,只适用于queue
q.back() 返回尾元素,只适用于queue
q.top() 返回最高优先级元素,但不删除该元素,只适用于priority_queue
q.push(item) q.emplace(args) 在queue末尾或priority_queue中恰当位置创建一个元素,其值为item,或者由args构造。
标准库queue使用先进先出FIFO的存储和访问策略。
priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。
priority_queue的push和pop实现都是采用heap算法,默认大顶堆。
例如:我们定义一个以vector为顶层顺序容器,大顶堆排序的存放int型数据的优先级队列。
默认下可以使用:priority_queue<int>pq;
展开后:priority_queue<int,vector<int>,greater<int>>pq; //第二个参数指定底层容器,第三个指定比较方式。第三个比较方式我们可以自己定义,在泛型编程中一般可使用仿函数(一个类只重载小括号运算符)。