顺序容器
顺序容器概述
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢
deque 双端队列。支持快速随机访问。在头尾位置插入/删除速度很快
list 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快
forward_list 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快
array 固定大小数组。支持快速随机访问。不能添加或删除元素
string 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快
确定使用那种容器
- 除非你有很好的理由选择其他容器,否则应使用vector。
- 如果你的程序有很多小的元素,且空间的额外开销很重要,则不要使用list或forward_list。
- 如果程序要求随机访问元素,应使用vector或deque。
- 如果程序要求在容器的中间插入或删除元素,应使用list或forward_list。如果程序需要在头尾位置插入或删除元素,但不会在中间位置进行插入或删除操作,则使用deque。
- 如果程序只有在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素,则
- 首先,确定是否真的需要在容器中间位置添加元素。当处理输入数据时,通常可以很容易地向vector追加数据,然后再调用标准库的sort函数来重排容器中的元素,从而避免在中间位置添加元素。
- 如果必须在中间位置插入元素,考虑在输入阶段使用list,一旦输入完成,将list中的内容拷贝到一个vector 中。
如果程序兼具插入和访问,那么取决于哪一种操作更多。
容器库概览
首先定义时需要确定容器中的元素类型:
1 | list<sales_data> //保存Sales_data对象的list |
对容器可以保存的元素类型的限制
顺序几乎可以保存任意类型的元素,但若类型没有默认构造函数,虚特殊处理:
1 | //假定noDefault是一个没有默认构造函数的类型 |
类型别名
iterator 此容器类型的迭代器类型
const_iterator 可以读取元素,但不能修改元素的迭代器类型
size_type 无符号整数类型,足够保存此种容器类型最大可能容器的大小
difference_type 带符号整数类型,足够保存两个迭代器之间的距离
value_type 元素类型
reference 元素的左值类型;与value_type&含义相同
const_reference 元素的const左值类型(即,const value_type&)
构造函数
c c; 默认构造函数,构造空容器(array,参见第301页)
c c1(c2); 构造c2的拷贝c1
cc(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
迭代器
一个迭代器范围有一对迭代器表示,通常是begin和end,[begin, end)。
他们应指向同一个容器中的元素或这最后一个容器的下一个位置,且begin<=end
左闭右合的范围
- 如果begin 与 end相等,则范围为空
- 如果begin与 end不等,则范围至少包含一个元素,且begin指向该范围中的第一个元素
- 我们可以对 begin递增若干次,使得begin==end
使用方法:
1 | while (begin != end){ |
容器类型成员
如果需要元素类型,可以使用容器的value_type。如果需要元素类型的一个引用,可以使用reference或const_reference。这些元素相关的类型别名在泛型编程中非常有用。为了使用这些类型,我们必须显式使用其类名:
1 | // iter是通过list<string>定义的一个迭代器类型 |
begin和end成员
1 | list<string> a ={ "Milton","Shakespeare","Austen" }; |
C++新版本可以使用auto来定义类型:
1 | //显式指定类型 |
当auto与begin或end结合使用时,获得的迭代器类型依赖于容器类型,与我们想要如何使用迭代器毫不相干。但以c开头的版本还是可以获得const_iterator的,而不管容器的类型是什么。
容器的定义和初始化
将一个容器初始化为另一个容器的拷贝
拷贝时容器类型及元素类型必须匹配。使用迭代器拷贝不需要容器类型相同,且元素类型只需要可以类型转换。
1 | //每个容器有三个元素,用给定的初始化器进行初始化 |
初始化拷贝容器类型和元素类型都必须相同。
由于两个迭代器表示一个范围,因此可以使用这种构造函数来拷贝一个容器中的子序列。例如,假定迭代器it表示authors中的一个元素:
1 | //拷贝元素,直到(但不包括)it指向的元素 |
列表初始化
1 | //每个容器有三个元素,用给定的初始化器进行初始化 |
构造函数
接受一个容器大小,及初始值来创建容器:
1 | vector<int> ivec ( 10,-1); // 10个int元素,每个都初始化为-1 |
如果元素类型具有没有默认构造函数,则必须提供初始值。
标准库array具有固定的大小
定义array,必须指定类型和容器大小:
1 | array<int, 42> //类型为:保存42个int的数组 |
与其他容器不同点在于,默认构造的array是非空的,因为指定大小后,其中的元素都被默认初始化了。其次使用列表初始化元素数目必须小于等于容器大小,剩余部分用0填充:
1 | array<int, 10> ia1; //10个默认初始化的int |
与内置数组不同,array可以进行拷贝赋值操作:
1 | int digs [10]= {0,1,2,3,4,5,6,7,8,9}; |
但初始值类型,元素类型,大小都必须相同。
赋值和swap
1 | c1 =c2; //将c1的内容替换为c2中元素的拷贝 |
与内置数组不同,array类型允许赋值,但对象类型需要相等:
1 | array<int,10> al = {0,1,2,3,4,5,6,7,8,9}; |
c1=c2 将c1中的元素替换为c2中元素的拷贝。c1和c2必须具有相同的类型
c={a,b,c…} 将cl中元素替换为初始化列表中元素的拷贝(array不适用)
swap(c1,c2) 交换c1和 c2中的元素。c1和 c2必须具有相同的类型。swap通常
c1.swap (c2) 比从c2向c1拷贝元素快得多
assign 操作不适用于关联容器和array
seq.assign (b,e) 将seq中的元素替换为迭代器b和e所表示的范围中的元素。迭代器b和e不能指向seq中的元素
seq.assign (il) 将seq中的元素替换为初始化列表i1中的元素
seq.assign (n,t) 将seq中的元素替换为n个值为t的元素
注意:赋值相关运算会导致指向左边容器内部的迭代器、引用和指针失效。而swap操作将容器内容交换不会导致指向容器的迭代器、引用和指针失效(容器类型为array和string的情况除外)。
assign
在顺序容器(除array外)中可以assign成员赋值:
1 | list<string> names; |
由于其旧元素被替换,因此传递给assign的迭代器不能指向调用assign的容器。
第二种用法接受整型值和一个元素值:
1 | //等价于slist1.clear (); |
swap
1 | vector<string> svec1 (10); // 10个元素的vector |
除arraay,swap操作不对任何元素进行拷贝、删除、和插入,所以可以在常熟时间内完成,它只是交换了容器内部的数据结构(人话就是交换了头指针)。所以迭代器、引用、指针并不会失效,且所指向的元素也不会变,原本指向s1[1]的迭代器与s2交换后会指向s2[1]。但string调用swap会使迭代器、引用、指针失效。
array容器中使用swap会真正交换元素,所以时间会取决于元素数量,此外指针、引用、迭代器绑定的s1[1]现在还使s1[1],
容器大小操作
与大小相关的操作,size、empty、和max_size:返回容器所能容纳的最大元素数目。
关系运算符
除了无需关联容器外都支持关系运算符,但容器中的元素类型必须一样。比较容器实际上是逐对比较:
- 如果两个容器具有相同大小且所有元素都两两对应相等,则这两个容器相等;否则两个容器不等。
- 如果两个容器大小不同,但较小容器中每个元素都等于较大容器中的对应元素,则较小容器小于较大容器。
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
1 | vector<int> v1 = { 1,3,5,7,9,12 }; |
容器的关系运算符使用元素的关系运算符
容器的相等运算符实际上是使用元素的==运算符实现比较的,而其他关系运算符是使用元素的<运算符。如果元素类型不支持所需运算符,那么保存这种元素的容器就不能使用相应的关系运算。例如,我们在第7章中定义sales_data类型并未定义==-和<运算。
顺序容器操作
接下来介绍顺序容器的特有操作:
向顺序容器添加元素
当我们使用这些操作时,必须记得不同容器使用不同的策略来分配元素空间,而这些策略直接影响性能。在一个vector或string 的尾部之外的任何位置,或是一个deque的首尾之外的任何位置添加元素,都需要移动元素。而且,向一个vector或string添加元素可能引起整个对象存储空间的重新分配。重新分配一个对象的存储空间需要分配新的内存,并将元素从旧的空间移动到新的空间中。
使用push_back
1 | //从标准输入读取数据,将每个单词放到容器末尾string word; |
tip:当我们用对象初始化、或插入到容器时,实际上会先拷贝这个对象生成临时对象,再初始化或插入它。
在特定的位置添加元素
insert成员提供了更一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素。vector.deque、list和string都支持insert成员。forward_list提供了特殊版本的insert成员。
slist.insert(iter,"Hello! ");//将"Hello ! "添加到iter之前的位置
有些容器(如vector)不支持push_front但可以使用insert来插入到开始的位置。
svec.insert (svec.end(), 10,"Anna" ); //将10个Anna插入到尾部
同时还有接受迭代器的版本:
1 | vector<string> v = { "quasi","simba","frollo", "scar"}; |
不可以将一对指向自己的迭代器传入insert。
insert返回值
通过使用insert的返回值,可以在容器中一个特定位置反复插入元素:
1 | list<string> lst; |
insert返回的是第一个新加入元素的迭代器,如果不插入(即只有第一个参数),就返回传入的第一个参数。
使用emplace
新标准引入了三个新成员——emplace_front、emplace和 emplace_back,这些操作构造而不是拷贝元素。这些操作分别对应push_front、insert和push_back,允许我们将元素放置在容器头部、一个指定位置之前或容器尾部。
1 | //在c的末尾构造一个sales_data对象 |
它与push或insert的区别在于它可以直接调用元素类型的构造函数在容器中直接创建,而push需要创建临时的对象,然后压入容器,但传入参数必须匹配。
1 | // iter指向c中一个元素,其中保存了sales data元素 |
访问元素
包括array在内的每个顺序容器都有一个front成员函数,而除forward_list之外的所有顺序容器都有一个back成员函数。这两个操作分别返回首元素和尾元素的引用:
1 | //在解引用一个迭代器或调用front或back之前检查是否有元素 |
访问成员函数返回引用
const容器返回const引用,如果使用auto保存返回值,并希望改变元素的值,必须定义为引用类型:
auto &v = c.back();
下标操作和安全的随机访问
为保证使用下标访问不会越界,可以使用at,它会在下标越界时抛出异常:
1 | vector<string> svec; //空vector |
删除元素
pop函数
这些操作返回void,如果需要弹出元素的值,就必须在执行弹出前保存它:
1 | while (!ilist. empty()) { |
特殊的forward_list
因为单向链表删除或者添加一个元素会改变前一个元素,但单向链表没办法访问前驱,所以我们可以添加或者删除给定元素之后的元素,
1 | forward_ list<int> flst = {0,1,2,3,4,5,6,7,8,9}; |
改变容器大小
1 | list<int> ilist(10, 42) ; // 10个int:每个的值都是42 |
容器操作可能使迭代器失效
由于向迭代器添加元素和从迭代器删除元素的代码可能会使迭代器失效,因此必须保证每次改变容器的操作之后都正确地重新定位迭代器。这个建议对vector、string和deque尤为重要。
编写改变容器的循环程序
添加/删除vector、string 或deque元素的循环程序必须考虑迭代器、引用和指针可能失效的问题。程序必须保证每个循环步中都更新迭代器、引用或指针。如果循环中调用的是insert或erase,那么更新迭代器很容易。这些操作都返回迭代器,我们可以用来更新:
1 | //傻瓜循环,删除偶数元素,复制每个奇数元素 |
不要保存end返回的迭代器
因为插入删除操作会使end迭代器失效。
vector对象如何增长
vector和string在添加元素时如果大小其容量,会开辟一块比预定容量更大的地方。
管理容量的成员函数
reserve并不改变容器中元素的数量,它仅影响vector预先分配多大的内存空间,不过只有当需要的内存超过当前容量时,才会改变容量。一旦调用,会至少分配与需求一样大或更大的空间。
capacity和size
每个 vector实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。
额外的string操作
构造string的其他方法
1 | const char *cp = "Hello world!!! "; //以空字符结束的数组 |
substr操作
1 | string s("hello world"); |
返回一个string,包含s中从pos 开始的n个字符的拷贝。pos的默认值为0。n的默认值为s.size () - pos,即拷贝从pos开始的所有字符,开始位置超过大小,会抛出一个异常,结束位置超出大小,会默认拷贝string的末尾。
改变string的其他方法
除了接受迭代器的insert和 erase版本外,string还提供了接受下标的版本。下标指出了开始删除的位置,或是insert到给定值之前的位置:
1 | s.insert(s.size(), 5,'!'); //在s末尾插入5个感叹号 |
标准库string类型还提供了接受C风格字符数组的insert和 assign版本。例如,我们可以将以空字符结尾的字符数组insert 到或assign给一个string:
1 | const char *cp = "stately, plump Buck"; |
我们也可以指定将来自其他string或子字符串的字符插入到当前string中或赋予当前string:
1 | string s = "some string", s2 = "some other string"; |
append和replace函数
append操作是在string末尾进行插入操作的一种简写形式:
1 | string s ("C++ Primer"),s2 = s; //将s 和s2初始化为"C++ Primer" |
replace操作是调用erase和insert的一种简写形式:
1 | //将"4th"替换为"5th"的等价方法 |
此例中调用replace时,插入的文本恰好与删除的文本一样长。这不是必须的,可以插入一个更长或更短的string:
1 | s.replace(11,3,"Fifth"); // s == "C++ Primer Fifth Ed." |
改变string的多种重载函数
assign和 append函数无须指定要替换string中哪个部分: assign总是替换string中的所有内容,append总是将新字符追加到string末尾。
replace函数提供了两种指定删除元素范围的方式。可以通过一个位置和一个长度来指定范围,也可以通过一个迭代器范围来指定。insert函数允许我们用两种方式指定插入点:用一个下标或一个迭代器。在两种情况下,新元素都会插入到给定下标(或迭代器)之前的位置。
可以用好几种方式来指定要添加到string 中的字符。新字符可以来自于另一个string,来自于一个字符指针(指向的字符数组),来自于一个花括号包围的字符列表,或者是一个字符和一个计数值。当字符来自于一个string或一个字符指针时,我们可以传递一个额外的参数来控制是拷贝部分还是全部字符。
string搜索操作
搜索操作:成功返回一个无符号的string::size_type值,失败返回npos(cont string::size_type)的static成员(他们应该是同一种类型,但变量名字不同),因此不建议使用int储存。
1 | string name ("AnnaBelle"); |
find操作对大小写有区分A与a会区分开来
1 | string numbers ( "0123456789"), name ( "r2d2") ; |
指定可选位置
find函数第二个参数为从哪里开始,默认为0,使用它可以在字符串中循环搜索子字符串的所有位置:
1 | string : : size_type pos = 0; |
逆向搜索
1 | string river ( "Mississippi" ); |
compare函数
string类型提供的与C标准库 的strcmp函数相似的函数,根据s等于、大小、小于返回0、整数、负数。
数值转换
一些函数以可实现string与数值的转换:
1 | int i = 42; |
要转换为数值的string中第一个非空白符必须是数值中可能出现的字符:string s2 = “pi = 3.14” ;
1 | //转换s中以数字开始的第一个子串,结果d = 3.14 |
这里将字符串种第一个出现数字的地方的子串传入函数,直到遇到不可能是数值的字符,然后将其转换。参数中第一个非空白符号必须是+、0、数字或是是小数点,且可以包含e、E、x、X等其他进制需要的字母。
如果string不能转换为一个数值,这些函数抛出一个invalid_argument异常(参见5.6节,第173页)。如果转换得到的数值无法用任何类型来表示,则抛出一个 out_of range异常。
容器适配器
本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物一样。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。例如,stack适配器接受一个顺序容器(除array或forward_list外),并使其操作起来像一个stack一样。
定义一个适配器
首先我们可以用一个容器初始化另一个容器:
1 | deque<int> deq = {1,2,3,4}; |
我们还可以尖括号内重载构造函数,就可以创建一个适配器:
1 | //在vector上实现的空栈 |
所有适配器都要求有添加和删除以及访问尾元素能力,所以不能用array、forward_list构造。
栈适配器
栈默认基于deque实现,所以可以省略第二个参数:
1 | stack<int> intstack; //空栈 |
虽然是基于deque实现,但我们不能使用底层容器的操作,所以只可以用push,而不可以使用push_back。
队列适配器
priority_queue 允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。饭店按照客人预定时间而不是到来时间的早晚来为他们安排座位,就是一个优先队列的例子。默认情况下,标准库在元素类型上使用<运算符来确定相对优先级。后面会学习如何重载这个默认设置。