一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 |
deque | 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 |
list | 双向链表。支持双向顺序访问 |
forward_list | 单向链表。只支持单向顺序访问 |
array | 固定大小数组。支持快速随机访问,不能添加或者删除元素 |
string | 专门用于保存字符。随机访问快。类似于vector |
string和vector将元素保存在连续的内存空间内,因此由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时。
list和forward_list在任何位置添加/删除元素都非常快速。作为代价,这两个容器不支持元素的随机访问。而且,与vector、array、deque相比,这两个容器的额外内存开销也很大。
forward_list没有size操作。
确定使用哪种顺序容器
一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。
容器均定义为模板类,我们需要额外提供元素类型信息:
list
deque
对容器可以保存的元素类型的限制
顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器:
vector> lines; //vector的vector
某些类没有默认构造函数,我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:
//假定noDefault是一个没有默认构造函数的类型
vector v1(10,init); //正确:提供了元素初始化器
vector v2(10); //错误:必须提供一个元素初始化器
类型别名 | |
iterator | 此容器类型的迭代器类型 |
const_iterator | 只能读不能修改的迭代器类型 |
size_type | 无符号整数类型,足够保存此类容器类型最大可能容器的大小 |
difference_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型;与value_type&含义相同 |
const_reference | const value_type& |
构造函数 | |
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造c2的拷贝c1 |
C c(b,e); | 构造c,将迭代器b和e指定范围内的元素拷贝到c(array不支持) |
C c{a,b,c,d.....}; | 列表初始化c |
赋值与swap | |
c1=c2 | 赋值 |
c1={a,b,c,d,e....} | 列表赋值(不适用于array) |
a.swap(b) | 交换a和b的元素 |
swap(a,b); | 等价于a.swap(b) |
大小 | |
c.size() | c中元素的数量(不支持forward_list) |
c.max_size() | c可保存的最大元素数量 |
c.empty() | c是否存储了元素 |
添加/删除元素(不适用于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.end() | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin(),c.rend() | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin(),c.crend() | 返回const_reverse_iterator |
迭代器范围(begin和end)
这种范围被称为左闭合区间:
[begin,end)
为了使用类型别名,我们必须显示使用其类名:
list::iterator iter;
vector::difference_type count;
对一个非常量对象调用begin、end、rbegin、rend,得到的是返回iterator的版本;对一个const对象调用这些函数时,才会得到一个const版本。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。
当不需要写访问时,应使用cbegin和cend.
C c; | |
C c1(c2); C c1=c2; |
|
C c(b,e); | array不适用 |
C c{a,b,c,d.....} C c={a,b,c,d.....} |
|
只有顺序容器(不包括array)的构造函数才能接受大小参数 | |
C seq(n) | string不适用 |
C seq(n,t) |
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了,元素类型也可以不同,只要能将元素类型转换即可:
list authors={"Milton","Shakesperae","Austern"};
vector articles={"a","an","the"};
list list2(authors); //正确:类型都匹配
deque authList(authors); //错误:容器类型不匹配
vector words(articles); //错误:容器类型不匹配
//正确:可以将const char*元素转换成string
forward_list words(articles.begin(),articles.end());
与顺序容器大小相关的构造函数
只有顺序容器的构造函数才接受大小参数,关联容器并不支持
标准库array具有固定大小
大小也是array类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:
array
array
为了使用array类型,我们必须同时指定元素类型和大小:
array::size_type i; //数组类型包括元素类型和大小
array::size_type j; //错误:array不是一个类型
虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。此时,要求容器类型、元素类型、大小都必须一致:
int digs[10]={0,1,2,3,4,5,6,7,8,9};
int cpy[10]=digs; //错误:内置数组不支持拷贝或赋值
array digits={0,1,2,3,4,5,6,7,8,9};
array copy=digits; //正确:只要数组类型匹配即合法
赋值运算 | |
c1=c2 | |
c1={a,b,c,d,e....}(array不适用) | |
a.swap(b) | |
swap(a,b); | |
assign操作不适用与关联容器和array | |
seq.assign(b,e) | |
seq.assign(l1) | |
seq.assign(n,t) |
赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(array和string的情况除外)
使用assign(仅顺序容器)
赋值运算符(=)要求左边和右边的运算对象具有相同的类型。assign允许我们从一个不同但相容的类型赋值,或者从容器的一个子序列赋值:
list names;
vector oldstyle;
names=oldstytle; //错误:容器类型不匹配
names.assign(oldstyle.cbegin(),oldstyle.cend()); //正确,可以将const char*转换为string
由于其旧元素被替代,因此传递给assign的迭代器不能指向调用assign的容器
assign的第二个版本:
//等价于slist.clear()
//后跟slist.insert(slist,10,"Hiya");
list slist(1); //1个元素,为空string
slist.assign(10,"Hiya"); //10个元素,每个都是“Hiya”
使用swap
swap操作交换两个相同类型容器的内容,调用swap之后,两个容器中的元素将会交换。
除array外,交换两个容器内容的操作保证会很快——元素本身未交换,swap只是交换了两个容器的内部数据结构。
对于array,swap会真正交换它们的元素。
关系运算符两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素
比较两个容器实际上是进行元素的逐对比较,比较方式与string比较类似。
容器的关系运算符使用元素的关系运算符完成比较
只有当其元素类型也定义了相应的比较运算符时,我们才可以使用关系运算符来比较两个容器。
这些操作会改变容器的大小;array不支持这些操作 forward_list有自己专有版本的insert和emplace forward_list不支持push_back和emplace_back vector和string不支持push_front和emplace_front |
|
c.push_back(t) | 在c的尾部创建一个值t或由args创建的元素。返回void |
c.emplace_back(args) | |
c.push_front(c) | 在c的头部创建一个值t或由args创建的元素。返回void |
c.emplace_front(args) | |
c.insert(p,t) | 在p指向的元素之前创建一个值为t或者由args创建的元素。返回指向新添加的元素的指针 |
c.emplace(p,args) | |
c.insert(p,n,t) | 在p指向的元素之前插入元素。返回指向新添加的第一个元素的迭代器。若新元素数量为0,则返回p |
c.insert(p,b,e) | |
c.insert(p,il) | |
向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效 |
使用push_back
string word;
while(cin>>word)
{
container.push_back(word);
}
当我们用一个对象初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝,而不是对象本身。
使用push_front
//此循环使ilist在头部保存3/2/1/0
list list;
for(size_t ix=0;ix!=4;++ix)
ilist.push_front(ix);
在容器中的特定位置添加元素
slist.insert(iter,"hello!");
将元素插入到vector、string和deque中的任何位置都是合法的。然而,这样做可能很耗时。
插入范围内元素
svec.insert(svec.end(),10,"Anna");
vector v={"quasi","simba","forllo","scar"};
slist.insert(slist.begin(),v.end()-2,v.end());
slist.insert(slist.end,{"these","words","will","go","at","the","end"});
//错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器
slist.insert(slist.begin(),slist.begin(),slist.end());
使用insert的返回值
insert返回指向新添加的元素的迭代器
//等价于调用push_front
list lst;
auto iter=lst.begin();
while(cin>>word)
iter=lst.insert(iter,word);
使用emplace操作
当我们调用一个emplace成员函数时,是将参数传递给元素类型的构造函数。emplace成员使用这些参数在容器管理的内存空间中直接构造元素:
//使用三个参数的Sales_data构造函数,在c的末尾构造一个Sales_data对象
c.emplace_back("978-2458930153",25,15.99);
//错误:没有接受三个参数的push_back版本
c.push_back("978-2458930153",25,15.99);
//正确:创建一个临时的Sales_data对象传递给push_back
c.push_back(Sales_data("978-2458930153",25,15.99));
传递给emplace函数的参数必须与元素类的构造函数相匹配:
c.emplace_back(); //使用Sales_data的默认构造函数
c.emplace(iter,"999-999999999"); //隐形转换,使用Sales_data(string)
c.emplace_front("978-2458930153",25,15.99);
at和下标操作符只适用于string、vector、deque和array back不适用于forward_list |
|
c.back() | 返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front() | 返回c中首元素的引用。若c为空,函数行为未定义 |
c[n] | 返回c中下标为n的元素的引用。n是一个无符号整数。n |
c.at(n) | 返回c中下标为n的元素的引用。下标不可越界,否则抛出out_of_range异常 |
对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误 |
这些操作会改变容器的大小,所以不适用于array forward_list有特殊版本的erase forward_list不支持pop_back; vector和string不支持pop_front |
|
c.pop_back() | 删除c中尾元素。若c为空,则函数行为未定义。函数返回void |
c.pop_front() | 删除c中首元素。若c为空,则函数行为未定义。函数返回void |
c.erase(p) | 删除迭代器p指向的元素,返回被删除元素之后元素的迭代器;若p指向尾元素,则返回尾后迭代器;若p是尾后迭代器,则函数行为未定义 |
c.erase(b,e) | 删除迭代器b和e所指范围内的元素。返回一个指向最后一个被删除元素之后元素的迭代器;若e本身就是尾后迭代器,则函数也返回尾后迭代器 |
c.clear() | 删除c中的所有元素。返回void |
删除deque中除首尾位置之外的任何元素都会使所有迭代器、引用和指针失效 指向vector或string中删除点之后位置的迭代器、引用和指针都会失效
|
删除list中的所有奇数:
list lst={0,1,2,3,4,5,6,7,8,9};
auto it=lst.begin();
while(it!=lst.end()){
if(*it%2)
it=lst.erase(it); //删除奇数并移动it
else
++it;
}
在一个单向链表中,没有简单的办法来获取一个元素的前驱。所以,forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。
lst.before_begin() | 返回指向链表首元素之前不存在的元素的迭代器。此迭代器不可引用 |
lst.cbefore_begin() | 返回一个const_iterator |
lst.insert_after(p,t) | 在迭代器p之后的位置插入元素。返回指向最后一个插入元素的迭代器。 |
lst.insert_after(p,n,t) | |
lst.insert_after(p,b,e) | |
lst.insert_after(p,il) | |
emplace_after(p,args) | 在p指定的元素之后创建一个元素。返回一个指向这个新元素的迭代器。 |
lst.erase_after(p) | 删除p指向位置之后的元素,返回指向被删除元素之后元素的迭代器 |
lst.erase_after(b,e) | 删除从b之后直到(但并不包括)e之间的元素,返回指向被删除元素之后元素的迭代器 |
从list中删除奇数元素:
forward_list flst={0,1,2,3,4,5,6,7,8,9};
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来增大或缩小容器
resize不适用于array | |
c.resize(n) | 调整c的大小为n个元素。若n |
c.reseize(n,t) | 调整c的大小为n个元素。任何新添加的元素都初始化为值t |
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效 |
list ilist(10,42); //10个int,每个都是42
ilist.resize(15); //将5个值为0的元素添加到ilist的末尾
ilist.resize(25,-1); //将10个值为-1的元素添加到ilist的末尾
ilist.resize(5); //从ilist末尾删除20个元素
编写改变容器的循环程序
vector vi={0,1,2,3,4,5,6,7,8,9};
auto iter=vi.begin();
while(iter!=vi.end()){
if(*iter%2){
iter=vi.insert(iter,*iter); //在指定位置之前复制当前元素,并返回指向新元素的迭代器
iter+=2;
}else
iter=vi.erase(iter);//删除偶数元素
//不应该向前移动迭代器,iter指向我们删除的元素之后的元素
}
插入操作迭代器前移,删除操作迭代器后移
不要保存end返回的迭代器
如果在一个循环中插入/删除deque、string或vector中的元素,不要缓存end返回的迭代器。
必须在操作前重新调用end()
while(begin!=v.end())
{
//做一些处理
begin=v.insert(iter,42);
begin+=2;
}
当不得不获取新的内存空间时,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预先分配多大的内存空间。
如果需求大小小于或等于当前容量,reverse什么也不做。当需求大小小于当前容量,容器不会退回内存空间。
resize成员函数只改变容器中元素的数目,而不是容器的容量。
capacity和size
容器的size是指它已经保存的元素数目;而capacity则是在不分配新的内存空间的前提下它最多可以保存多少元素
每个vector实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间
n、len2和pos2都是无符号值 | |
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个字符 |
通常我们从一个const char*创建string时,指针指向的数组必须以空字符结尾,拷贝操作遇到空字符停止。如果我们还传递给构造函数一个计数值,数组就不必以空字符结尾。如果我们未传递计数值且数组未以空字符结尾,或者给定计数值大于数组大小,则构造函数的行为是未定义的。
s.substr(pos,n) | 返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0,如果pos>s.size(),抛出一个out_of_range异常。n的默认值为s.size()-pos,即拷贝从pos开始的所有字符 |
s.insert(pos,args) | 在pos之前插入args指定的字符,pos可以是一个下标,下标版本返回一个指向s的引用 |
s.erase(pos,len) | 删除从位置pos开始的len个字符,返回一个指向s的引用 |
s.assign(args) | 将s中的字符替换为args指定的字符。返回一个指向s的引用 |
s.append(args) | 将args追加到s。返回一个指向s的引用 |
s.replace(range,args) | 删除s中范围rang内的字符,替换为args指定的字符。返回一个指向s的引用 |
assign和append函数无须指定要替换string中的哪个部分:assign总是替换string中的所有内容,append总是将新字符追加到string末尾
replace有两个版本:
replace(pos,len,args)
replace(b,e,args)
insert有两个版本:
insert(pos,args)
insert(iter,args)
搜索操作返回指定字符出现的下标,如果未找到则返回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 | 在s中查找第一个不在args中的字符 |
s.find_last_not_of | 在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指向的以空字符结尾的C风格字符串的前n个字符。pos和n无默认值 |
每个搜索操作都返回一个string::size_type值,表示匹配发生位置的下标。如果搜索失败,则返回一个名为string::npos的static成员。npos的类型为const string::size_type
搜索是对大小写敏感的
指定在哪里开始搜索
循环地搜索子字符串出现的所有位置:
string::size_type pos=0;
//每步循环查找name中下一个数
while((pos=name.find_first_of(numbers,pos))
!=string::npos){
cout<<"found number at index: "<
类似于C语言中的strcmp,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、整数或负数
S2 | |
pos1,n1,s2 | |
pos1,n1,s2,pos2,n2 | |
cp | |
pos1,n1,cp | |
pos1,n1,cp,n2 |
to_string(val) | 返回数值val的string表示,val可以是任何算术类型 |
stoi(s,p,b) | 返回s的起始子串(表示整数内容)的数值,返回类型分别对应;b表示转换所用的基数,默认值是10;p是size_t指针,用来保存s中第一个非数值字符的下标,默认为0,即,函数不保存下标 |
stol(s,p,b) | |
stoul(s,p,b) | |
stoll(s,p,b) | |
stoull(s,p,b) | |
stof(s,p) | 返回s的起始子串(表示浮点数内容)的数值 |
stod(s,p) | |
stold(s,p) |
如果string不能转换为一个数值,这些函数抛出一个invalid_argument异常。如果转换得到的数值无法用任何类型来表示,则抛出一个out_of_range异常。
除了顺序容器,还定义了三个顺序容器适配器:stack(栈)、queue(队列)和priority_queue。
本质上,适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
size_type | |
value_type | |
container_type | |
A a; | |
A a(c); | |
关系运算符 | |
a.empty() | |
a.size() | |
a.swap(b) | |
swap(a,b) |
定义一个适配器
//deq是一个deque
stack stk(deq); //从deq拷贝元素到str
默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型:
//在vector上实现的空栈
stack> str_stk;
//str_stk2在vector上实现,初始化时保存svec的拷贝
stack> str_stk2(svec);
适配器的容器限制:
stack——(不能)array、forward_list
queue——list、deque——(不能)vector
priority_queue——vector、deque——(不能)list
栈适配器
stack类型定义在stack头文件中。每个容器适配器都基于底层容器类型的操作定义了自己的特殊操作,但是我们只能使用适配器操作,而不能使用底层容器类型的操作。
栈默认基于deque实现,也可以在list或vector上实现 | |
s.pop() | |
s.push(item) | |
s.emplace(args) | |
s.top() |
队列适配器
queue和priority_queue适配器定义在queue头文件中。
priority_queue允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。
queue默认基于deque实现,priority_queue默认基于vector实现 queue也可以用list或者vector实现,priority_queue也可以用deque实现 |
|
q.pop() | |
q.front() | |
q.back() | |
q.top() | (只适用于priority_queue) |
q.push(item) | |
q.emplace(args) |