C++ primer学习笔记——第九章 顺序容器

一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。

一、顺序容器概述

顺序容器类型
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。支持双向顺序访问
forward_list 单向链表。只支持单向顺序访问
array 固定大小数组。支持快速随机访问,不能添加或者删除元素
string 专门用于保存字符。随机访问快。类似于vector

string和vector将元素保存在连续的内存空间内,因此由元素的下标来计算其地址是非常快速的。但是,在这两种容器的中间位置添加或删除元素就会非常耗时。

list和forward_list在任何位置添加/删除元素都非常快速。作为代价,这两个容器不支持元素的随机访问。而且,与vector、array、deque相比,这两个容器的额外内存开销也很大。

forward_list没有size操作。

确定使用哪种顺序容器

  • 通常使用vector是最好的选择,除非你有更好的理由选择其他容器
  • 注重空间开销的,不要使用list或forward_list
  • 只在头尾,不在中间插入/删除元素的,使用deque
  • 在中间插入/删除元素的,使用list或forward_list

二、容器库概览

一般来说,每个容器都定义在一个头文件中,文件名与类型名相同。

容器均定义为模板类,我们需要额外提供元素类型信息:

list<Sales_data>
deque<double>

对容器可以保存的元素类型的限制

顺序容器几乎可以保存任意类型的元素。特别是,我们可以定义一个容器,其元素的类型是另一个容器:

vector<vector<string>> lines;    //vector的vector

某些类没有默认构造函数,我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数:

//假定noDefault是一个没有默认构造函数的类型
vector<noDefault> v1(10,init);   //正确:提供了元素初始化器
vector<noDefault> v2(10);        //错误:必须提供一个元素初始化器
容器操作(通用操作)
类型别名
iterator 此容器类型的迭代器类型
const_iterator 只能读不能修改的迭代器类型
size_type 无符号整数类型,足够保存此类容器类型最大可能容器的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型与value_type&含义相同
const_reference const value_type&
构造函数
C c;  
C c1(c2);  
C c(b,e);  
C c{a,b,c,d.....};  
赋值与swap
c1=c2  
c1={a,b,c,d,e....}  
a.swap(b)  
swap(a,b);  
大小
c.size()  
c.max_size()  
c.empty()  
添加/删除元素(不适用于array)
c.insert(args)  
c.emplace(inits)  
c.erase(args)  
关系运算符
==,!=  
<,<=,>,>=  
获取迭代器
c.begin(),c.end()  
c.cbegin(),c.end()  
反向容器的额外成员
reverse_iterator  
const_reverse_iterator  
c.rbegin(),c.rend()  
c.crbegin(),c.crend()  

1、迭代器

迭代器范围(begin和end)

  • 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置
  • end不在begin之前

这种范围被称为左闭合区间:

[begin,end)

2、容器成员类型

为了使用类型别名,我们必须显示使用其类名:

list<string>::iterator iter;
vector<int>::difference_type count;

3、begin和end成员

对一个非常量对象调用begin、end、rbegin、rend,得到的是返回iterator的版本;对一个const对象调用这些函数时,才会得到一个const版本。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。

当不需要写访问时,应使用cbegin和cend.

4、容器定义和初始化

容器定义和初始化
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<string> authors={"Milton","Shakesperae","Austern"};
vector<const char*> articles={"a","an","the"};

list<string> list2(authors); //正确:类型都匹配
deque<string> authList(authors);  //错误:容器类型不匹配
vector<string> words(articles);   //错误:容器类型不匹配

//正确:可以将const char*元素转换成string
forward_list<string> words(articles.begin(),articles.end());

与顺序容器大小相关的构造函数

只有顺序容器的构造函数才接受大小参数,关联容器并不支持

标准库array具有固定大小

大小也是array类型的一部分。当定义一个array时,除了指定元素类型,还要指定容器大小:

array<int,42> 
array<string,10>

为了使用array类型,我们必须同时指定元素类型和大小:

array<int,10>::size_type i;  //数组类型包括元素类型和大小
array<int>::size_type j;     //错误:array<int>不是一个类型

虽然我们不能对内置数组类型进行拷贝或对象赋值操作,但array并无此限制。此时,要求容器类型、元素类型、大小都必须一致:

int digs[10]={0,1,2,3,4,5,6,7,8,9};
int cpy[10]=digs;                //错误:内置数组不支持拷贝或赋值
array<int,10> digits={0,1,2,3,4,5,6,7,8,9};
array<int,10> copy=digits;       //正确:只要数组类型匹配即合法

5、赋值和swap

赋值运算
c1=c2  
c1={a,b,c,d,e....}  
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<string> names;
vector<const char*> oldstyle;
names=oldstytle;   //错误:容器类型不匹配
names.assign(oldstyle.cbegin(),oldstyle.cend());    //正确,可以将const char*转换为string

由于其旧元素被替代,因此传递给assign的迭代器不能指向调用assign的容器

assign的第二个版本:

//等价于slist.clear()
//后跟slist.insert(slist,10,"Hiya");
list<string> slist(1);     //1个元素,为空string
slist.assign(10,"Hiya");   //10个元素,每个都是“Hiya”

使用swap

swap操作交换两个相同类型容器的内容,调用swap之后,两个容器中的元素将会交换。

除array外,交换两个容器内容的操作保证会很快——元素本身未交换,swap只是交换了两个容器的内部数据结构。

对于array,swap会真正交换它们的元素。

7、关系运算符

关系运算符两边的运算对象必须是相同类型的容器,且必须保存相同类型的元素

比较两个容器实际上是进行元素的逐对比较,比较方式与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<int> 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<string> 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<string> 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);

2、访问元素

在顺序容器中访问元素的操作

at和下标操作符只适用于string、vector、deque和array

back不适用于forward_list

c.back() 返回c中尾元素的引用。若c为空,函数行为未定义
c.front() 返回c中首元素的引用。若c为空,函数行为未定义
c[n] 返回c中下标为n的元素的引用。n是一个无符号整数。n<c.size()
c.at(n) 返回c中下标为n的元素的引用。下标不可越界,否则抛出out_of_range异常
对一个空容器调用front和back,就像使用一个越界的下标一样,是一种严重的程序设计错误

3、删除元素

顺序容器的删除操作

这些操作会改变容器的大小,所以不适用于array

forward_list有特殊版本的erase

forward_list不支持pop_back;

vector和string不支持popfront

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<int> 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;
     

4、特殊的forward_list操作

在一个单向链表中,没有简单的办法来获取一个元素的前驱。所以,forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。

在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<int> 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;
    }
}

5、改变容器大小

我们可以使用resize来增大或缩小容器

顺序容器大小操作
resize不适用于array
c.resize(n) 调整c的大小为n个元素。若n<c.size(),则多出的元素被丢弃。若必须添加新元素,对新元素进行值初始化
c.reseize(n,t) 调整c的大小为n个元素。任何新添加的元素都初始化为值t
如果resize缩小容器,则指向被删除元素的迭代器、引用和指针都会失效;对vector、string或deque进行resize可能导致迭代器、指针和引用失效
list<int> ilist(10,42);  //10个int,每个都是42
ilist.resize(15);        //将5个值为0的元素添加到ilist的末尾
ilist.resize(25,-1);     //将10个值为-1的元素添加到ilist的末尾
ilist.resize(5);         //从ilist末尾删除20个元素

6、容器操作可能使迭代器无效

编写改变容器的循环程序

vector<int> 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!=end())
{
    //做一些处理
    begin=v.insert(iter,42);
    begin+=2;
}

四、vector对象是如何增长的

当不得不获取新的内存空间时,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实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间

 

五、额外的string操作

1、构造string的其他方法

构造string的其他方法
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开始的所有字符

2、改变string的其他方法

修改string的操作
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)

3、string搜索操作

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 在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: "<<pos
        <<" element is "<<name[pos]<<endl;
        ++pos;   //移动到下一个字符,否则会无限循环
}

4、compare函数

类似于C语言中的strcmp,根据s是等于、大于还是小于参数指定的字符串,s.compare返回0、整数或负数

compare的几种参数模式
S2  
pos1,n1,s2  
pos1,n1,s2,pos2,n2  
cp  
pos1,n1,cp  
pos1,n1,cp,n2  

5、数值转换

string和数值之间的转换
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<int>
stack<int> stk(deq);  //从deq拷贝元素到str

默认情况下,stack和queue是基于deque实现的,priority_queue是在vector之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器类型:

//在vector上实现的空栈
stack<string,vector<string>> str_stk;
//str_stk2在vector上实现,初始化时保存svec的拷贝
stack<string,vector<string>> 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和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)  

猜你喜欢

转载自blog.csdn.net/oil_you/article/details/82821833