《C++primer》 第九章_顺序容器 读书笔记
《C++Primer 第五版》
——读书随笔集
第九章
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。
9.1 顺序容器概述
下表列出了标准库中的顺序容器,所有顺序容器都提供快速顺序访问元素的能力。但是这些容器在以下方面都有不同的性能折中:
- 向容器添加或从容器中删除元素的代价。
- 非顺序访问容器中元素的代价。
vector | 可变大小数组。支持快速随机访问。但是在尾部之外的位置插入或删除元素可能很慢。 |
---|---|
deque | 双端队列。支持快速随机访问。在头尾位置插入或删除速度很快。 |
list | 双向链表。只支持双向顺序访问。在list中任何位置插入或删除元素都很快。 |
forward_list | 单项链表。只支持单向顺序访问。在链表中任何位置进行插入或者删除元素都很快。 |
array | 固定大小数组。支持快速随机访问。不能删除或添加元素 |
string | 与vector相似的容器,但专门用来保存字符。随机访问快,在尾部插入或删除速度快。 |
除了固定大小的array外,其他容器都提供高效、灵活的内存管理。
string和vector
- 元素存储在连续的内存空间中,由于是连续存储的,所以由元素的下标来计算其地址是非常快速的。
- 在这两种容器的中间位置添加或删除元素将会十分耗时:在一次插入或删除操作之后,需要移动插入、删除元素位置之后的所有元素来保持连续存储。
- 添加一个元素时有可能还需要分配额外的存储空间,在这种情况下,每个元素都必须移动到新的存储空间。
list和forward_list
- 这两个容器的设计目的是令容器任何位置添加或删除操作都很快。
- 这两个容器不支持元素的随机访问,为了访问一个元素,只能遍历整个容器。
- 与
vector
、deque
和array
相比,这两个容器的额外内存开销也很大。
deque
deque
支持快速的随机访问。- 在
deque
的中间位置添加或删除元素的代价(可能)很高。但是在deque
的两端添加或删除元素都很快,与list
或forward_list
添加删除元素的速度相当。
forward_list 和 array 是C++ 标准新增加的类型
- 与内置数组相比,
array
是一种更安全、更容易使用的数组类型。 array
的大小是固定的,因此array
不支持添加和删除元素以及改变容器大小的操作。forward_list
的设计目标是达到与最好的手写的单向链表数据结构相当的性能,forward_list
没有size
操作,因为保存或计算其大小就会比手写链表多出额外的开销。而对于其它容器而言,size
保证是一个快速的常量时间操作。
新标准库容器的性能几乎肯定与最精心优化过的同类数据结构一样好,通常会更好。
确定使用哪种容器
- 通常
vector
是最好的选择,除非有更好的理由选择其他容器。 - 如果程序中有很多小的元素且额外开销很重要,则不能使用
list
或forward_list
。 - 如果程序要求随机访问元素,应使用
vector
或deque
。 - 如果程序要求在容器中间插入或删除元素,应使用
list
或forward_list
。 - 如果程序需要在头尾插入或删除元素,但不会在中间位置进行插入或删除操作,则使用
deque
- 如果程序只有在读取时才需要在容器中间插入元素,随后需要随机访问元素,则:
- 首先,确定是否真的需要在容器中间位置添加元素,当处理输入数据时,通常可以很容易地向
vector
中追加元素,然后再调用标准库sort
函数来重拍容器中的元素,从而避免在容器中间位置添加元素。 - 如果必须在中间位置插入元素,考虑在输入阶段就是要list,一旦输入完成,将
list
中的内容拷贝到一个vector
中。
- 首先,确定是否真的需要在容器中间位置添加元素,当处理输入数据时,通常可以很容易地向
- 如果程序既需要随机访问元素,又需要在容器中间位置插入元素,此时取决于在
list
或forward_list
中访问元素与vector
或deque
中插入、删除元素的相对性能。
9.2容器库概述
容器类型上的操作形成了一种层次:
- 某些操作是所有容器类型都提供的。
- 另一些操作针对顺序容器、关联容器或无序容器。
- 还有一些操作只适用于一小部分容器。
一般每个容器都定义在一个头文件之中,文件名与类型名相同,即 deque
定义在头文件 deque
中,list
定义在 list
中,依次类推。
容器都是模板类,对大多数,但不是所有容器,还需要提供额外的元素类型信息。
对容器可以保存的元素类型的限制
顺序容器几乎可以保存任意类型的元素,特别是,可以定义一个容器,其元素类型是另一容器。
vector<vector<string>> lines;
交旧的编译器可能需要在两个尖括号之间键入空格:
vector<vector<string> > lines;
顺序容器构造函数的一个版本是接受容器大小参数,它使用了元素类型的默认构造函数,但某些类没有默认的构造函数,可以定义一个保存这种类型对象的容器,但构造这种容器时不能只传递一个元素数目参数:
//假设 noDefault 是一个没有默认构造函数的类型
vector<noDefault> v1(10, init); //正确,提供了元素初始化器
vector<noDefault> v1(10); //错误,必须提供一个元素初始化器
类型别名 | 说明 |
---|---|
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
difference_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 | 列表初始化c |
赋值与swap | 说明 |
---|---|
c1 = c2 | 将c1中的元素替换为c2中的元素 |
c1 = | 将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 |
9.2.1 迭代器
-
迭代器范围
一个迭代器的范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者尾元素之后的位置,这两个迭代器通常被称为
begin
和end
。这种元素范围称为左闭右开区间,即[begin,end)
。两个迭代器构成范围迭代器的要求:
- 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置。
- 可以通过反复递增
begin
来到达end
,也就是说,end
不应该在begin
之前。
-
使用左闭合范围蕴含的编程假定
假定
begin
和end
构成一个合法的迭代器范围:- 如果
begin
和end
相等,则范围为空。 - 如果
begin
和end
不相等,则范围至少包含一个元素,且begin指向范围中的第一个元素。 - 可以对
begin
递增若干次,使得begin == end
。
因此可以采用一个循环来处理一个元素范围:
while(begin != end){ *begin = val; ++begin; }
- 如果
9.2.2 容器类型成员
大多数容器还提供反向迭代器,反向迭代器就是一种反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义都是颠倒的,例如,对一个反向迭代器执行 ++
,会得到上一个元素。
-
如果需要使用元素类型,则可以使用容器的
value_type
。 -
如果需要元素类型的一个引用,可以使用
reference
或const_reference
。
9.2.3 begin和end成员
begin
和 end
操作生成指向容器第一个元素和尾后元素之后的位置的迭代器。这两个迭代器最常用的就是形成一个包含容器所有元素的迭代器范围。
r
开头的版本返回反向迭代器。
c
开头的版本返回 const
迭代器。
list<sting> a = { "Milton","Shakespeare","Austen" };
auto it1 = a.begin(); //list<string>::iterator
auto it2 = a.rbegin(); //list<string>::reverse_iterator
auto it3 = a.cbegin(); //list<string>::const_iterator
auto it4 = a.crbegin(); //list<string>::const_reverse_iterator
auto
与 begin
或 end
结合使用时,获取的迭代器类型依赖于容器类型:
//显示指定类型
list<string>::iterator it5 = a.begin();
list<string>::const_iterator it6 = a.begin();
auto it7 = a.begin(); //仅当a是const时,it7是const_iterator
auto it8 = a.cbegin(); //it8 是const_iterator
不需要写访问时,应该使用 cbegin
和 cend
。
9.2.4容器定义和初始化
每个容器类型都定义了一个默认构造函数,除了 array 之外,其他容器的默认构造函数都会创建一个指定类型的空容器,且都可以接受指定容器大小和元素初始值的参数。
-
将一个容器初始化为另一容器的拷贝
将一个新容器创建为另一个容器的拷贝方法有两种:
- 直接拷贝整个容器。
- 拷贝由一个迭代器对指定的元素范围(array除外)。
为了创建一个容器为另一个容器的拷贝,两个容器的类型及其元素类型必须匹配。
传递迭代器参数来拷贝一个范围时,就不要求容器类型相同,而且新、旧容器中的元素类型也可以不同,只要能将要拷贝的元素转换为要初始化的容器的元素类型即可。
list<sting> authors = { "Milton","Shakespeare","Austen" }; 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()); //假设it表示articles的一个元素,可以拷贝一个指定的范围 forward_list<string> words2(articles.begin(), it));
-
列表初始化
list<sting> authors = { "Milton","Shakespeare","Austen" }; vector<const char*> articles = {"a","an","the"};
对于除
array
之外的容器类型,列表初始化还隐含的指定了容器的大小,容器将包含与初始值一样多的元素。 -
与顺序容器大小相关的构造函数
除了与关联容器相同的构造函数之外,顺序容器(array除外)还提供了另一个构造函数,它接受一个容器大小和一个元素初始值(可选),如果不提供初始元素值,则标准会创建一个值初始化:
vector<int> ivec(10, -1); list<string> svec(10,"hi"); forward_list<int> ivec(10); //10个元素,每个元素都是0 deque<string> svec(10); //10个元素,每个元素都是空的string
- 如果元素类型是内置类型或者具有默认构造函数的类型,可以只为构造函数提供一个容器大小。
- 如果元素类型没有默认的构造函数,除了提供容器大小参数外,还必须指定一个显示的元素初始值。
- 只有顺序容器的构造函数接受大小参数,关联容器并不支持。
-
标准库 array 具有固定大小
当定义一个
array
时,除了需要指定元素类型,还需要指定容器的大小。array<int,42>; //保存42个int的数组 array<string,42>; //保存42个string的数组
为了使用
array
类型,必须同时指定元素类型和大小:array<int,42>::size_type i; //数组类型包括元素的类型和大小 array<int>::size_type j; //错误,array<int>不是一个类型
一个默认构造的
array
是空的:它包含了与其大小一样多的元素,元素被执行默认初始化。如果对
array
进行列表初始化,初始值的数目必须等于或者小于array
的大小。如果初始值数目小于array
的大小,则它们被用来初始化array
靠前的元素,剩余的元素执行值初始化。如果元素类型是一个类类型,那么该类必须有默认的构造函数,以使值初始化能够进行。array<int,10> ia1; //10个默认初始化的int array<int,10> ia2 = {0,1,2,3,4,5,6,7,8,9}; //列表初始化 array<int,10> ia3; = {42}; //ia3[0] = 42,其它值为0
内置数组不能执行拷贝或赋值,但是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; //正确,只要数组类型匹配即可
9.2.5 赋值和swap
赋值运算符将其左边容器中的全部元素替换为右边容器中的元素的拷贝:
c1 = c2; //c1的 内容替换为c2中的元素拷贝
c1 = {a,b,c} //赋值后,c1的大小为3
第一个赋值运算后,左边容器将与右边容器相等,如果两个容器的原来大小不同,赋值运算后的两者的大小都与右边容器的原大小相同。
array
类型允许赋值,赋值符号左右两边的运算对象必须具有相同的类型。
array<int,10> a1 = {0,1,2,3,4,5,6,7,8,9};
array<int,10> a1 = {0}; //所有元素的值都是0
a1 = a2;
a2 = {0}; //错误,不能将一个花括号列表赋予数组
由于右边运算对象的大小可能与左边运算对象的大小不同,因此 array
不支持 assign
,也不允许用花括号包围的值列表进行赋值。
-
使用assign(仅顺序容器)
顺序容器(array 除外)还定义了一个
assign
成员,允许我们从一个不同但类型相容的类型赋值,或者从容器的一个子序列赋值。assign
操作用参数所指定的元素替换左边容器中的所有元素:list<string> names; vector<const char*> oldstyle; names = oldstyle; //错误,容器类型不匹配 names.assign(oldstyle.cbegin(),oldstyle.cend()); //正确,可以将const char* 转换成string
由于就的元素被代替,因此传递给
assign
的迭代器不能指向调用assign
的容器。assign 的第二个版本接受一个整型值和一个元素,它用指定书目且具有相同给定值的元素替换容器中的原有元素:
list<string> slist1(1); //1个元素,指定为空的string slist1.assign(10,"hi"); //10个元素,每个都是 "hi"--
-
使用swap
swap 操作交换两个相同类型容器的内容,调用swap之后,两个容器中的元素将会交换:
vector<string> svec1(10); vector<string> svec2(24); swap(svec1,svec2); //svec1将包含24个元素,svec2将包含10个元素
交换两个容器内容的操作保证会很快,元素本身并未交换,
swap
只是交换了两个容器的内部数据结构。 除
array
外,swap
不对任何元素进行拷贝、删除或插入操作,因此能够保证在常数时间内完成。 元素不会被移动的事实意味着,除
string
之外,指向容器的迭代器、引用和指针在swap
操作后不会失效。它们仍然指向swap
之前所指向的那些元素,但是在swap
之后,这些元素已经属于不同的容器了假定iter
在swap
之前指向svec1[3]
的string
,那么swap
之后它指向svec2[3]
的元素。与其他容器不同,对一个
string
调用swap
会导致迭代器、引用和指针失效。 与其他容器不同,
swap
两个array
会真正交换它们的元素,因此,交换两个array
所需的时间与array
中元素的数目成正比。对于array
,在swap
操作之后,指针、引用和迭代器所绑定的元素保持不变,但元素值已经与另一个array
中对象的元素值进行了交换。
在新标准库中,容器既提供成员函数版本的 swap
,也提供了非成员版本的 swap
。统一使用非成员版本的swap
是一个好习 惯。
9.2.6 容器大小操作
每个容器类型都有三个与大小相关的操作:
- 成员函数
size
返回容器中元素的数目。 empty
当size
为0时返回true
,否则返回false
。max_size
返回一个等于或大于该类型容器所能容纳的最大元素数的值。
forward_list
支持 max_size
和 empty
,但不支持 size
。
9.2.7 关系运算符
- 只有当其元素类型也定义了相应的比较运算符时,才可以使用关系运算符来比较两个容器。
- 每个容器都支持 相等运算符(==)和不等运算符(!=)。
- 除了无序关联容器外所有的容器都支持关系运算符 (>、>=、<、<=)。
- 关系运算符左右两边的运算对象必须是相同的容器类型,且必须保存相同类型的元素。
- 比较两个容器实际上是进行元素的逐对比较,这些运算符的工作方式与string的关系运算符类似:
- 如果两个容器具有相同的大小且所有的元素对应相等,则两个容器相等,否则两个容器不相等。
- 如果两个容器大小不同,但较小的容器中每个元素都等于较大容器中对应的元素,则较小的容器小于较大的容器。
- 如果两个容器都不是另一个容器的前缀子序列,则它们的比较结果取决于第一个不相等的元素的比较结果。
9.3顺序容器操作
9.3.1 向顺序容器中添加容器
除了 array
之外,所有的标准库容器都提供了灵活的内存管理,在运行时可以动态增加或删除元素来改变容器的大小。
-
使用push_back
容器元素是拷贝,容器中的元素与提供值对象之间没有任何关联。对容器中元素的任何改变都不会影响到原始对象。
-
使用push_front
除了push_back,list,forward_list和deque容器还支持名为push_front的类似操作。
vector
不支持push_front。 -
在容器中特定的位置插入元素
insert
成员提供了更一般的操作,允许我们在容器中的任何位置插入元素。vector
,deque
,list
,和string
都支持insert成员,forward_list
提供了特殊版本的insert成员。insert
函数接受一个迭代器作为其第一个参数,迭代器指出了在容器中什么位置放置新元素。它可以指向容器的任何位置,包括容器尾部之后的下一个位置。insert
函数将元素插入到迭代器所指定的位置之前。slit.insert(iter,"hello"); //将hello插入到iter之前的位置
由于某些容器不支持
push_front
操作,但它们对于insert
操作并无类似的限制,因此可以将元素插入到容器的开始位置,而不必担心容器是否支持push_front
。vector<string> svec; list<string> slist; // 等价于调用slist.push_front("hello") slist.insert(slist.begin(),"hello"); //vector 不支持push_front,但是可以在begin()之前加入元素 svec.insert(svec.begin(),"hello");
把元素插入到
vector
、deque
、string
中的任何位置都是合法的,然而,这样做可能很耗时。 -
插入范围内元素
insert
函数除了接受一个迭代器参数外,还可以接受一个元素数目和一个值,它将指定数量的元素添加到指定位置之前,这些元素都按照给定的值初始化。svec.insert(svec.end(),10,"Anna");
这行代码将10个元素插入到
svec
的末尾,并将所有的元素都初始化"Anna"
。接受一个迭代器或一个初始化列表的insert版本将给定范围中的元素插入到指定的位置之前:
vector<string> v = {"quasi","simba","frollo","scar"}; //将v的最后两个元素添加到slist的开始位置 slist.insert(slist.begin(),v.edn()-2,v.end()); slist.insert(slist.end(),{"these","words","will","go","at","the","end"}); //运行时错误,迭代器表示要拷贝的范围,不能指向与目的位置相同的容器 slist.insert(slist.begin(),slist.begin(),slist.end());
如果传递给
insert
一对迭代器,它们不能指向添加元素的目标容器。如果范围为空,不插入任何元素,
insert
操作会将第一个参数返回。 -
使用insert的返回值
通过使用 insert 的返回值,可以在容器中一个特定位置反复插入元素:
list<string> lst; auto iter = lst.begin(); //将iter初始化为lst.begin() while(cin >> word) iter = lst.insert(iter,word); //等价于调用push_front
-
使用emplace操作
emplace_front
,emplace
,emplace_back
这些操作不是构造而是拷贝元素,这些操作分别对应push_front
,insert
,push_back
,允许将元素放置在容器头部,一个指定位置之前或容器尾部。调用
push
或insert
函数是将元素类型的对象传递给它们,这些对象被拷贝到容器中。调用
emplace
时是将参数传递给元素类型的构造函数,emplace
成员使用这些参数在容器管理的内存中直接构造元素。//在c的末尾构造一个 Sales_data 对象 //使用三参数的 Sales_data 构造函数 c.emplace_back("9-999-999-9",25,12,99); //没有接受三个参数的push_back版本 c.push_back("9-999-999-9",25,12,99); //创建一个临时的 Sales_data 对象,并将其传递给push_back c.push_back(Sales_data("9-999-999-9",25,12,99));
emplace 函数的参数根据元素类型而变化,参数必须与元素类型的构造函数相匹配。
c.emplace_back(); //使用Sales_data的默认构造函数 c.emplace(iter,"9-999-999-9"); //使用Sales_data(string)的构造函数 c.emplace_front("9-999-999-9",25,12,99); //使用接受一个ISBN,COUNT,PRICE版本的构造函数
9.3.2 访问元素
包括 array
在内的每个顺序容器都有一个 front
成员,而除 forward_list
之外的所有容器都有一个 back
成员函数,这两个操作分别返回首元素和尾元素的引用。
//在解引用一个迭代器或调用front或back之前需要检查是否有元素
if (!c.empty())
{
// val 和 val2 是c中第一个元素值的拷贝
auto val = *c.begin(), val2 = c.front();
//val3 和 val4 是c中最后一个元素值的拷贝
auto last = c.end();
auto val3 = *(--last); //不能递减forward_list迭代器
auto val4 = c.back(); //forward_list不支持
}
迭代器 end
指向的是容器尾元素之后的元素,为了获取尾元素,必须首先递减此迭代器。
在调用 front
和 back
(解引用begin 或 end 返回的迭代器)之前,要确保 c
非空,如果容器为空,if
操作的行为将是未定义的。
-
访问成员函数返回的是引用
在容器中访问元素的成员函数即 front、back、下标和at返回的都是引用。
如果容器是一个 const对象,则返回值是const的引用。如果容器不是const的,则返回值是普通的引用,可以用来改变元素的值。
if (!c.empty()) { c.front() = 42; //将42赋值给c中的第一个元素 auto &v = c.back(); //获取指向最后一个元素的引用 v = 1024; //改变c中的元素 auto v2 = c.back(); //v2不是一个引用,它是c.back() 的一个拷贝 v2 = 0; //未改变c中的元素 }
-
下标操作和安全的随机访问
提供快速随机访问的容器
string
、vector
、deque
、array
也都提供下标运算符。使用越界的下标是一种严重的错误,并且编译器并不检查这种错误。
如果希望确保下标是合法的,可以使用
at
成员函数,使用at
成员函数,如果下标越界,at
会抛出一个out_of_range
异常。vector<string> svec; cout<<svec[0]; //运行时错误,svec中没有元素 cout<<svec.at(0); //抛出一个out_of_range异常
9.3.3 删除元素
与添加元素的多种方式类似,(非array)容器也有多种删除元素的方式。
这些操作会改变容器的大小,所以不适用于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中删除点之后位置的迭代器,引用和指针都会失效。
删除元素的成员函数并不检查其参数。在删除元素前,程序员必须确保他们是存在的。
9.3.4 特殊的forward_list操作
为了理解forward_list为什么有特殊版本的添加和删除操作,考虑当我们从一个单向链表中删除一个元素时会发生什么。当添加或删除一个元素时,删除或添加的元素之前的那个元素的后继会发生变化。为了添加或删除一个元素,我们需要访问其前驱,以便改变前驱改变前驱的链接。但是,forward_list是单向链表。在一个单向链表中,没有简单的方法来获取一个元素的前驱,出于这个原因,在一个forward_list中添加或删除元素的操作是通过改变给定元素之后的元素来完成的。这样,我们总是可以访问到被添加或删除元素所影响的元素。
由于这些操作与其他容器上的操作有实现方式不同,forward_list并未定义insert、emplace和erase,而是定义了名为insert_after、emplace_after和erase_after的操作。为了支持这些操作,forward_list也定义了before_begin,它返回一个首前迭代器。这个迭代器允许我们在链表首元素之前并不存在的元素“之后”添加或删除元素(亦即在链表首元素之前添加删除元素)。
lst.before_begin() | 返回指向链表首元素之前并不存在的元素的迭代器,此迭代器不能解引用。 |
---|---|
lst.cbefore_begin() | 返回一个const_iterator |
lst.insert_after(p,t) | 在迭代器p之后的位置插入元素。t是一个对象 |
lst.insert_after(p,n,t) | n是数量 |
lst.insert_after(p,b,e) | b和e是表示范围的一对迭代器(b和e不能指向lst内) |
lst.insert_after(p,il) | il是一个花括号列表。返回一个指向最后一个插入最后一个元素的迭代器。如果范围为空,则返回p,若p为尾后迭代器,则函数行为未定义。 |
emplace_after(p,args) | 使用args在p指定的位置之后创建一个元素,返回一个指向这个新元素的迭代器。若p为尾后迭代器,则函数的行为未定义 |
lst.erase_after(p) | 删除p指向的位置之后的元素。 |
lst.erase_after(b,e) | 删除从b之后直到(但不包含)e之间的元素,返回一个指向被删除元素之后元素的迭代器,若不存在这样的元素,则返回尾后迭代。 |
当在forward_list中添加或删除元素时,我们必须关注两个迭代器——一个指向我们要处理的元素,另一个指向其前驱。例如,我们从list中删除奇数元素的循环程序,将其改为从forward_list中删除元素:
forward_list<int> flst={0,1,2,3,4,5,6,7,8,9};
auto prev=flst.before_begin(); //表示flst的“首前元素”
auto curr=flst.begin(); //表示flst中的第一个元素
while(curr!=flst.end())
{
if(*curr%2)
curr=flst.erase_after(prev);// 删除它并移动curr
else
{
prev=curr; //移动迭代器curr,指向下一个元素,prev指向curr之前的元素
++curr;
}
}
此例中,curr表示我们要处理的元素,prev表示curr的前驱。调用begin来初始化curr,这样第一步循环就会检查第一个元素是否是奇数。我们用before_begin来初始化prev,它返回指向curr之前不存在的元素的迭代器。
当找到奇数元素后,我们将prev传递给erase_after,此调用将prev之后的元素删除,即,删除curr指向的元素。然后我们将curr置为erase_after的返回值,使得curr指向序列中下一个元素,prev保持不变,仍指向(新)curr之前的元素。如果curr指向的元素不是奇数,在else中我们将两个迭代器都向前移动。
9.3.5 改变容器大小
我们可以使用resize来增大或缩小容器,与往常一样,array不支持resize。
c.resize(n) | 调整c的大小为n个元素。若n<s.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,指向删除元素之前的迭代器,指针,引用仍有效。
注意:当删除元素时,尾迭代器总是会失效。
-
编写改变容器的循环程序
使用insert和erase函数的返回值来更新迭代器,保证迭代器始终有效。
//删除偶数元素,复制每个奇数元素 vector<int> v = {0,1,2,3,4,5,6,7,8,9}; auto iter = v.begin(); while(iter != v.end()) { if(*iter %2) {iter = v.insert(iter,*iter);//复制奇数元素 iter+=2;//跳过复制的元素和原来的奇数,往后挪两个位置 } else { iter = iter.erase(iter);//删除偶数元素并且指向下一个位置, } }
-
不要保存end返回的迭代器
当我们添加或删除vector或string的元素后,或者在deque首元素以外的任何位置添加删除元素后,原来end返回的迭代器总会失效。
如果在一个循环中插入或删除deque,string或vector中的元素,不要缓存end返回的迭代器,很有可能是已经失效的迭代器。
9.4 vector对象是如何增长的
我们都知道vector对象是动态存储的,从这一点看有点像链表,可以动态的增加或减少元素。我们也知道链表中是有指针变量,专门用于存储上一个和下一个元素的地址。正是因为这两个指针的存在,我们才能做到动态的存储数据,即不用像数组那样必须事先申请好空间。链表的缺点就是不能够快速的随机访问其中元素,必须通过指针层层查找。
但是,vector既可以实现动态存储数据,而且支持快速随机访问(用下标或者指针访问元素)。对于能够用下标查找的数据类型,其存储方式必定是连续的,即每个元素紧挨着前一个元素存储。
这样对于vector来说,就会出现一个问题,我在初始定义vector的时候该给其申请多少内存空间?如果申请的很小,那么我来了新的数据改存放在哪里?如果申请的很大,我用不完,那岂不是很浪费内存空间?
当然对于vector这种标准库类型,通常我们只关心如何使用它,而不关心其实如何实现的。不过对于其在存储空间的实现方式还是了解一下比较好。
我们知道容器中元素连续存储,且容器大小是可变的,考虑向vector中添加元素会发生什么?如果没有空间容纳新的元素,容器不可能简单的将其添加到内存的其他位置,因为vector的元素必须连续存储。因此容器必须分配新的空间来保存已用的元素和新的元素。将已有元素从旧位置移动到新空间,然后添加新元素,释放旧空间。如果说我们每添加一个新的元素就执行一次这样的操作,显然性能会慢到我们不可接受。
为了避上面的代价,标准库采用了可以减少容器空间重新分配的策略。当不得不获取新的内存空间时,vector的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,从而用来保存更多新的元素。这样,就不需要每次添加新的元素都重新分配容器的内存空间了。
vector类型提供了一些成员函数,允许我们与它的现实中内存分配部分互动。
c.capacity() 不重新分配内存空间的话,c可以保存多少元素
c.reserve(n) 分配至少能容纳n个元素的内存空间
c.shrink_to_fit() 将capacity()减少为size()相同大小。size()为vector已经保存元素个数。
如果没有空间容纳新元素:
- 容器必须分配新的内存空间来保存已有元素和新元素,将已有元素从旧位置移动到新空间中,然后添加新元素,释放旧存储空间。
- vector和string的实现通常会分配比新的空间需求更大的内存空间。容器预留这些空间作为备用,可以用来保存更多的新元素。
管理容器的成员函数
- capacity操作告诉我们容器在不扩张内存空间的情况下可以容纳多少个元素。
- reverse操作允许我们通知容器它应该准备保存多少个元素。
capacity和size
- size是指它已经保存的元素的数目;
- capacity是在不分配新的内存空间的前提下它最多可以保存多少个元素。
int main()
{
vector<int> a;
for(int i = 0; i < 24; ++ i) a.push_back(i);
cout << a.size() << endl; //输出size为24
cout << a.capacity() << endl; //输出capacity为32,capacity大于等于size
return 0;
}
Note:每个vector实现都可以选择自己的内存分配策略。但是必须遵守的一条原则是:只有当迫不得已时才可以分配新的内存空间。
9.5 额外的string操作
详细见C++文档——字符串库——std::basic_string
9.6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack
、queue
、priority_queue
。
适配器是标准库中的一个通用概念,容器、迭代器和函数都有适配器。本质上,一个适配器是一种机制,能使某种事物的行为看起来像另外一种事物。一个容器适配器接受一种已有的容器类型,使其行为看起来像一种不同的类型。
-
定义一个适配器
每个适配器都定义两个构造函数:
- 默认的构造函数创建一个空的对象。
- 接受一个容器的构造函数拷贝该容器来初始化适配器。
默认情况下,
stack
和queue
是基于deque
实现的,priority_queue
是在vector
之上实现的。可以在创建一个适配器时将一个命名的顺序容器作为第二个参数来重载默认容器类型。
//在vector上实现的空栈 stack<string, vector<string>> str_stk; //str_stk2在vector上实现,初始化时保存svec的拷贝 stack<string, vector<string>> str_stk2(svec);
对于一个给定的适配器,可以使用哪些容器是有限制的。
- 所有容器都要有添加和删除元素的能力,因此,适配器不能构建在
array
之上。类似的,也不能用forward_list
来构造容器适配器,因为所有适配器都要求容器具有添加、删除以及访问尾元素的能力。 stack
只要求push_back
、pop_back
和back
操作,因此可以使用除array
和forward_list
之外的任何容器类型来构造。queue
要求back
、push_back
、front
、push_front
,因此它可以构建在list
和deque
之上,但不能基于vector
构造。priority_queue
除了front
、push_back
、pop_back
操作之外还要求随机访问能力,因此可以构造于vector
或queue
之上,但不能基于list
构造。
每个容器适配器都基于底层容器类型的操作定义自己的特殊操作,使用时只可以使用适配器操作,而不能使用底层容器类型的操作。
-
栈适配器
stack<int> intStack; for (size_t ix = 0; ix != 10; ++ix) intStack.push(ix); while (!intStack.empty()) { int value = intStack.top(); intStack.pop(); }
-
队列适配器
queue
和priority_queue
适配器定义在queue
头文件中。
priority_queue
允许在队列中的元素创立优先级。新加入的元素会安排在所有优先级比它低的已有元素之前。默认情况下,标准库在元素类型上使用<
运算符来确定相对优先级。