第九章(顺序容器)
本章要点,
- 顺序容器,元素在顺序容器中的位置和其加入容器时的顺序相互一致。
- 关联容器(分为有序和无序),关联容器中元素的位置由元素相互关联的关键字值决定。后序介绍特有操作。
- 所有的容器都享有公共的接口,不同的容器按不同的方式对它进行扩展。公共接口使得,我们基于某种容器学习的内容也都适用于其他的容器。每一种容器,都提供了不同的性能和功能的权衡。
- 容器是什么?容器就是一些特定类型对象的集合。
- 三中容器适配器,分别为容器操作定义了不同的接口,来与容器的类型适配。
/1.顺序容器概述
1).顺序容器在以下方面中都有不同的性能折中:
- 容器添加或者删除元素的代价
- 非顺序访问容器元素的代价
顺序容器类型 | 性能描述 |
---|---|
vector | 可变大小(顺序栈)。支持随机访问。在尾部之外的位置插入或者删除元素可能很慢 |
deque | 双向队列(顺序队列)。支持随机访问。在头尾位置插入或者删除速度很快 |
list | 双向链表。只支持双向顺序访问。在任何位置插入或者删除元素都很快。 |
forward_list | 单向链表。只支持单向顺序访问。在链表的任何位置进行插入或者删除都很快 |
array | 固定大小的数组。支持随机访问。不能添加或者删除元素(只能覆盖) |
string | 与vector相似的容器。但是专门保存字符。在尾部插入或者删除很快 |
2).分析。
- 除了
array
是固定大小之外,其他容器都提供了高效,灵活的内存管理。可以添加删除,扩张,收缩容器。 - 容器对元素的存储策略对容器操作的效率有着固有,重大的影响。以及决定着容器是否支持某一种操作。
容器类型 | 存储方式及其影响 |
---|---|
string,vector | 连续的内存空间。……当添加一个元素,有时候需要分配额外的空间,此时,每一个元素都需要移动到新的存储空间中去。 |
list,forward_list | 容器的额外内存开销很大。指针域,头尾指针。 |
deque | 其实也是顺序存储的,但是大小可变,实现可能比较困难。 |
3).注意,forward_list
和array
是新增的类型。
- 与内置数组相比较,array是一种更加安全,容易使用的数组类型。
forward_list
的设计目标是达到和最好的手写单项链表数据结构一样的性能。所以它不支持size
。原因就是保存大小会有额外的开销。而对于其他的容器,size
是一个常量级别的操作。- 新标准库的容器性能几乎肯定与精心优化过的同类数据数据结构一样好(同常更好)。现代c++程序应该使用的是标准库容器,而不是原始的数据结构。例如内置数组等。
4).如何选择合适的容器。
- 一般使用
vector
,除非你有更好的理由选择其他的容器。 - 程序中有很多小的元素,而且空间的开销很重偶要。不要使用
list
,或者forward_list
。 - 要求随机访问元素。
vector
或者deque
。 - 要在中间插入或者删除元素,
list
,或者forward_list
。 - 只在头尾插入,
deque
。 - 如果程序只有在读取数据时才需要在容器中间位置进行插入元素。而随后需要随机访问元素。那么,确定是否需要在中间插入,如果只是排序,直接插入,然后
sort()
可以避免中间插入。如果确实需要,那么输入时使用list,输入完成再将内容拷贝到vector
中。 - 如果既需要随机访问,又需要中间插入,取决于哪一种操作占主导地位。需要,进行测试。(
forward_list
,list
,vector
,deque
) - 如果是在不确定使用什么容器,程序中只使用
vector
和list
的公共操作:
- 使用迭代器。
- 不使用下标操作。
- 避免随机访问。
练习,9.1
vector
加sort
,实现插入排序,等价于list
。- 快速排序频繁地访问元素,需要随机访问比较合理。使用
vector
。
/2.容器库概述
1).容器的操作,
- 所有容器均适用(本小节介绍的)
- 仅仅适用于顺序容器(本章接下来小节介绍的),或者关联容器,或者无序容器
- 只适用于一小部分容器
2).一般来说,一个容器都定义在一个头文件中,文件名称和类型名相同。容器均为模板类,意味着需要提供额外的信息(string
??)。对于大多数容器,需要提供额外的元素类型信息,list<Sales_data>,deque<double>
3).顺序容器几乎可以保存保存任意类型的数据。但是一些容器操作对元素的数据类型有特殊的要求。例如,一些类如果没有默认构造函数,那么我们不能用只传递一个容器大小的实参来实现构造这个容器(容器的构造函数)。
4).注意,一些容器的操作还对元素类型有限制,以下是部分的容器操作。
类型别名 | 含义 |
---|---|
iterator | 容器类的迭代器类型 |
const_iterator | 可以读取元素,但是不能修改元素的迭代器类型 |
size_type | 无符号整数类型,容器的最大容量都可以包含在内 |
different_type | 带符号的整数类型,足够保存任意两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,和value_type& 含义是一样的 |
const_reference | 元素的左值类型,(即const value_type& ) |
构造函数 | 含义 |
---|---|
C c; | 默认构造函数,构造空的容器c |
C c1 (c2) | 构造c2 的拷贝c1 |
C c(b,e) | 构造c ,将迭代器中b(begin) 和e(end) 指定的的范围内的元素拷贝到c 中,(array 不支持) |
C c{a,b,c……} | 列表初始化容器 |
赋值和swap | 含义 |
---|---|
c1 = c2 | 将c1 中的元素替换为c2 中的元素 |
c1 = {a,b,c,d……} | 不适用于array |
a.swap(b) | 交换,容器中的内容 |
swap(a,b) | 与上一致 |
大小 | 含义 |
---|---|
c.size() | forward_list 不支持 |
c.max_size() | c 中可以保存的最大元素数目 |
c.empty |
添加或者删除元素(不支持array ) | 含义(在不同容器中,这些操作的接口都不同) |
---|---|
c.insert(args) | 插入 |
c.emplace(inits) | 使用inits 构造一个c 中的元素 |
c.erase(args) | 删除args 中指定的元素 |
c.clear() | 删除c 中的所有元素,返回void |
关系运算符 | 相关信息 |
---|---|
==,!= | 所有的容器都支持 |
<,<=,>,>= | 无序关联容器不支持 |
获取迭代器 | 相关信息 |
---|---|
c.begin(),c.end() | |
c.cbegin(),c.cend() | 返回的是const_iterator |
反向容器的额外成员(forward_list不支持) | 描述 |
---|---|
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不可修改元素的迭代器 |
c.rbegin(),c.rend() | |
c.crbegin(),c.crend() | 返回的是const_reverse_iterator |
//1.迭代器
1).与容器一样,迭代器有着公共的接口,如果一个迭代器提供某一个操作,那么所有提供相同操作的迭代器对这个操作的实现方式都是一样的。例如,所有迭代器都允许访问容器中的元素,并且都是通过解引用运算符号实现的。(或者说运算符号的含义都是一样的,前提是有这个操作)
2).所有容器支持的操作。
操作 | 含义 |
---|---|
*iter | |
iter->men | |
++iter | |
–iter | 对于forward_list 不适用 |
iter1 == iter2 | |
iter1 != iter2 |
3).只能用于,string,vector,deque,array
(原因就是因为它们内存是连续的。)
算术运算 | 含义 |
---|---|
iter + n | n 为整数,并且,iter 是指向容器中的元素或者是尾后,否则是无效的 |
iter-n | |
iter += n | |
iter1 -= n | |
iter1 - iter2 | |
<,<=,=>,> | 注意,上面的表格没有指出 |
4).迭代器的范围,两个迭代器之前的范围。代表两个迭代器左闭右开之间的元素。(编译器不会强制左边小于等于右边的要求,但是这是责任)
5).要求左闭右开是因为可安全地解引用。
//2.容器的类型成员
1).表中已经指出。强调几个
类型别名 | 作用 |
---|---|
value_type | 返回的是容器中的元素类型 |
reference/const_reference | 得到的是容器元素的引用/常量引用 |
2).注意,使用时一定要记得前面带上类名。
{
list<string>::iterator iter;
vector<int>::difference count;
}
练习9.8
{
list<string>::value_type value;
list<string>::reference value;
}
//3.begin和end函数成员
1).列表初始化,list<string> a = {"hello","world"};
;善用auto it1 = a.begin();
。
2).注意,
- 不以
c
开头的函数(begin,rbegin,end,rend,
)都是被重载的。当非常量对象时,返回的是iterator
,当时常量是,返回的是const_iterator
。 - 以
c
开头的返回的是const_iterator
,即便是使用auto
。 - 其他使用
auto
,返回的是否是const
依赖于容器的类型。 - 只读不写,使用
cbegin,cend
。
练习,注意const vector<int>和vector<int> const
的区别,前者是vector
是const
,不能修改vector
;后者是vector
里面的成员是const
,而vector
的大小是可以改变的。
//4.容器定义和初始化
1).初始化的方式。
方式 | 结果 |
---|---|
C c; | 默认构造函数,如果c 是一个array 那么执行默认的初始化,否则c 是一个空的容器 |
C c1(c2);C c1 = c2; | c1 初始化为c2 的拷贝。c1 和c2 必须是相同的类型,(对于array ,它们的大小还必须一样) |
C c{a,b,c,……};C c = {a,b,c}; | 类型相容即可。对于array ,列表中的元素数量必须小于或者等于array 的大小。不择的按值初始化 |
C c(b,e); | c初始化为b和e两个迭代器之间的元素。类型要相容即可。array 不适用 |
只有顺序容器不包括array 的构造函数才可以接受大小参数(即关联容器不适用) | |
C seq(n) | 进行值初始化,构造函数是explicit ,当元素类型是内置类型或者是有默认构造函数的类类型是才可以这样做,如果类类型没有默认构造函数,切记必须显式地指定元素初始值。 |
C seq(n,t) |
2).注意。容器类型和容器元素类型。
- 拷贝时,容器类型和元素类型都要求
可以匹配(必须相同)。 - 使用迭代器范围时,容器类型没有要求相同。元素类型只要可以匹配就可以。
{
list<string> a = {"hello","world"};
vector<const char*> b = {"ni","hao"};
list<string> c(a);//正确
vector<string> d(a);//错误,一个是链表,一个是顺序表
vector<string> e(b);//错误,元素类型不匹配
forward_list<string> f(b.begin(),b.end());//正确。注意这里是迭代器范围,左闭右开
}
4).array
的大小也是类型的一部分。当定义一个array
时,要包括元素的类型,以及大小。
{
//array不支持普通的构造函数。它的构造函数
//都会确定容器的大小,显式或者隐式
//需要用户指定大小,最好情况下也是容易出错的。
array<int,42>
array<string,12>
array<int,42>::size_type s;//正确
array<int>::size_type s1;//错误,不是一个类型
}
5).与内置数组的比较,
- 都需要显式地指定大小,这容易出错。
- 内置数组不支持拷贝,但是array支持
{
int a[10];
int b([10] = a;//错误
array<int,10> a;
array<int,10> b = a;//正确,类型一样,注意大小也是类型的一部分
}
练习:9.11
- 如果容器为空,暂未分配空间
- 迭代器范围(迭代器是
const
还是普通的都没有区别,只是拷贝而已)和列表初始化,都只要求元素的类型是相容的 - 而拷贝初始化,必须(容器和元素类型)都是一样的。
//5.赋值和swap
1).赋值方式
赋值方式 | 相关描述 |
---|---|
c1 = c2 | c1,c2必须有一样的类型,注意到array 的类型包括大小。 |
c = {a,b,c……} | array 不适用,因为大小不一定一样。大小也是类型的一部分。 |
swap(c1,c2);c1.swap(c2) | c1和c2必须有相同的类型,注意容器的类型的含义,元素类型和容器类型甚至还有容器大小。swap 通常比拷贝快得多。 |
assign 操作不适用于array 和关联容器,原因同上。 | |
seq.assign(b,e) | 将seq中的元素替换为迭代器b和e所表示范围的元素。注意迭代器不能指向seq |
seq.assign(il) | 将初始化列表li中的值替换seq |
seq.assign(n,t) | 换为n个t。 |
2).注意,
assign
只要元素类型是相容的就可以。- 因为直接赋值要求类型(容器和元素,甚至大小(
array
))是一样的。 - 一样的由于是拷贝而已,
assign
的迭代器是否是const
没有什么区别。 - 以下是等价的。
{
a.clear();
a.insert(a.begin(),10,"hi");
list<string> a(1);//只有一个空的string元素
a.assign(10,"hi");
}
- 除了
array
外,交换容器的内容操作保证很快,元素本身不动(不对任何元素进行拷贝,删除或者插入操作。),只是交换两个容器内部的数据结构。例如,对两个vector<string>
的容器进行交换,它们的元素并没有交换,指向它们的迭代器,引用和指针在交换之后都不会交换。它们指向的元素也没有改变。而是在交换之后,它们成为另一个vector
的组成部分。 - 但是注意,对于
string
容器,调用swap
会导致迭代器,引用,指针失效。 - 交换两个
array
会真正地交换它们的元素,因此所需要的时间与它们的数目成正比。 - 统一使用非成员版本的swap,是一个好习惯。它在泛型编程中非常地重要。
//6.容器大小的操作
1).一个例外,forward_list
仅仅不支持size
(其他两个是支持的)。其他的容器都支持以下函数。
size
empty
max_size
,返回的是一个大于或者等于该类型容器所能容纳的最大(最多)元素的值
//7.关系运算符号
1).关系运算符号,两边的运算对象是必须同容器类型,同元素类型的。
2).比较两个容器的大小,其实是对元素进行逐一比较。这一点和string
的比较很一样。
3).容器的比较,实际上就是元素的比较,所以如果我们的元素类型没有定义相应的操作,我们是不可以进行比较的。例如,Sales_data是没有定义==和<的,所以我们如果进行比较,那么就是错误的。
练习,9.16,比较vector<int>和list<int>
的大小,
- 可以逐一比较,
- 也可以先将其中一个转换为另一个的容器类型,再利用==进行判断就可以。
9.17,c1 < c2
的要求
- c1和c2的类型一定要相同。
- 它们的元素类型定义了<
/.3顺序容器操作
1).顺序容器和关联容器的不同在于两者组织元素的方式是不一样的。导致元素的存储,访问,添加以及删除也是不一样的。
2).以下介绍,顺序容器特有的操作。以上介绍的是所有的容器都支持的操作。
//1.向顺序容器中添加元素
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(args) | emplace 是需要构造类的必要信息。**使用(,),以逗号间隔就可以。**返回void |
c.push_front(t);/c.emplace_front(args) | 同 |
c.insert(p,t);/c.emplace(p,args)) | p可以是begin到end中的任何一个位置,在迭代器p之前插入,返回的是指向新添加的元素的迭代器。 |
c.insert(p,n,t) | 插入n个t,返回的所有t中最小的一个迭代器,如果n为0,返回的是p |
c.insert(p,b,e) | 迭代器范围的版本,迭代器范围不能指向该容器。返回的是最小的迭代器,方式就是直接将b到e之间搬到p所指向的元素之前(b在前e在后)。如果b=e为0,返回的是p |
c.insert(p,li) | li是一个{}括起来的值的列表。返回的是最小的迭代器,也是一样的直接将li顺序不变的整体搬进来。与迭代器范围是一样的。列表为空那么返回的是p |
2).向一个vector,或者string或者deque
插入元素会使得所有的指向容器的迭代器,引用,指针失效。
3).当我们使用这些操作时,一定要记得,不同是容器使用不同的策略来分配空间,而这些策略直接影响性能。就是顺序存储和链表存储的差异。
4).注意以下。
- 对于
string
使用word.push_back('a');
等价于word += 'a';
. - 我们利用一个对象(内置类型)对一个容器进行拷贝初始化,或者将对象插入到一个容器中时,都是对象的副本,与传值调用是一样的,容器里面的修改不会,引起对象本身的变化
insert
是在元素之前插入,所以即便是不支持push_front
,也可以使用insert
进行操作。
{
vector<string> svec;
list<string> slist;
slist.insert(slist.begin(),"hello");
slist.push_front("hello");//这两句话是等价的。
svec.insert(svec.begin(),"hello");//尽管这样可以做到
//在前面插入,但是这样的性能是很低的。
}
- 理解
insert
的返回值。
{
list<string> slist;
auto iter = slist.begin();
while (cin >> word)
iter = slist.insert(iter,word);
//以下是等价的
//slist.push_front(word);
}
- 理解
emplace
的与push
和insert
的不一样。emplace
是构造而不是拷贝元素。
{
Sales_data a("书本号码",34,2.97);
//例如,假定元素类型是Sales_data,
c.emplace_back("书本号码",34,2.97);
//等价于
c.push_back(a);
c.push_back(Sales_data("书本号码",34,2.97));
//一个是直接传递参数给元素类型的构造函数,
//在容器管理的内存空间中直接创建对象。
//一个是直接传递构造好的对象,创建一个局部的对象,并将其压入容器当中去。
}
- 既然是类的构造函数,我们可以根据类的构造函数进行传参。
{
c.emplace();//使用的是类Sales_data的默认构造函数
c.emplace(iter,"书本号码");//使用的是Sales_data(string)构造函数
c.emplace_front("书本号码",34,2.98);//使用的是三个参数的构造函数。
}
练习,
- 判断奇偶性使用位与运算,最低位为1则是奇数否则就是偶数,因此,
num & 1 == true
为奇数,num & 1 == false
为偶数。 - 解引用并进行插入就可。
- 进行插入
vector和string和deque
的迭代器会失效,所以,一旦进行了修改,迭代器也要相应地进行修改。例如,利用insert
的返回值配合增减操作。
//2.访问元素
1).如果容器中没有元素,那么访问的结果将会是未定义的。
at
和下标操作值适合string,vector,deque,array
back
不适用于,forward_list
- 对一个空的容器调用
back
或者front
将会是一个严重的错误,相当于是下标的越界。
操作 | 含义 |
---|---|
c.back() | 返回c的尾元素的引用,如果c是空的,该函数行为将会是未定义的。 |
c.front() | |
s[n] | 返回的下标为n的的元素的引用,n是一个无符号的整数,如果n >= c.s.size() ,函数将会返回未定义 |
s.at(n) | 如果下标越界就会抛出out_of_range 的异常。 |
2).注意,
- 对于
forward_list
迭代器不可以递减。 *(--c.end())
和c.back()
效果是一样的*(c.begin())
和c.front()
效果也是一样的- 注意使用
back
或者front
或者对迭代器解引用前,对容器是否为空进行检查。 - 返回的都是引用。如果容器是
const
的那么返回的是const
的引用。 - 如果我们希望得到的是引用,而不是拷贝。需要记住加上&
{
if (!c.empty())
{
c.back() = 42;
auto &i = c.back();
i = 1024;
auto k = c.back();
k = 1000;
//最终c.back()的值为1024;
}
}
- 下标运算符号并不会进行下标是否合法的检查。
练习9.24,注意,如果是at越界,那么程序会异常退出的,因为我们没有设计try
处理异常。
//3.删除元素
1).删除操作表。
array
不适用。forward_list
有特殊版本的erase
forward_list
不支持pop_back
,vector和string
不支持以及pop_front
操作 | 相关描述 |
---|---|
c.pop_back() | 返回void ,如果c为空函数的行为将会是未定义的。 |
c.pop_front() | |
c.erase§ | 删除p所指向的元素,返回被删除元素之后的迭代器的位置(这样总是合法的),如果p是尾后迭代器函数的行为将会是未定义的 |
c.erase(b,e) | 迭代器范围,左闭右开。两个迭代器相等,或者都指向尾后迭代器也是合法的。返回的是e |
c.clear() | 删除所有的元素,返回的是void |
- 删除
deque
中除了首位位置之外的元素都会使得迭代器,引用,指针失效 - 指向
vector
和string
中的删除点之后的迭代器,引用和指针会失效。 - 删除元素的成员函数不会检查,参数的合法性,所以程序员一定要保证它们是存在的。
练习,
- 对于将数组进行进行拷贝到容器的问题,我们可以将指针看成是迭代器,例如我们可以将,
arr
和arr+10
作为迭代器范围进行assign
。
//4.特殊的forward_list操作
1).forward_list
有特殊版本的添加和删除操作。它是一个单向的链表,所以进行操作时需要它的前驱,因为它的前驱的后继是需要改变的。但是一个单向链表中获取一个前驱是不可能的。所以需要,因此添加或者删除一个元素我们是通过改变后继这个元素来实现的。(数据结构考题)
{
//大致是这样的,c语言版本。
struct link{
dataType data;
struct link* next;
};
//p为需要删除的元素。
p->data = p->next->data;
q = p->next;
p->next = q->next;
free(q);
}
2).由于实现单向链表的操作与其他的实现不一样。所以forward_list
定义了自己的删除和插入操作。
名称 | 相关描述 |
---|---|
lst.before_begin();/lst.cbefore_begin() | 返回首元素之前的位置的迭代器。不可以解引用,和尾后的迭代器是一个性质的。 |
lst.insert_after(p,t) | 在迭代器p之后插入元素。 |
lst.insert_after(p,n,t) | n表示数量 |
lst.insert_after(p,b,e) | 迭代器范围,也是不能执行它本身 |
lst.insert_after(p,il) | il是一个花括号的列表。如果p为尾后迭代器,那么是函数行为未定义的,为空则返回p。返回的是指向最后一个插入元素的迭代器。 |
emplace_after(p,args) | 如果p是尾后迭代器,那么函数的行为就是未定义的。p之后插入,返回的是新插入的元素的迭代器。 |
lst.erase_after§ | 删除的是p之后的那个元素,返回的是执行被删元素之后的元素的迭代器,如果不存在这样的元素,返回的是尾后迭代器。如果p指向的是尾元素或者尾后迭代器,这样的函数将会是未定义的。 |
lst.erase_after(b,e) | **b之后一直到e之前的元素。**返回的就是e |
{
forward_list<int> flst = {0,1,2,3,4,5,6};
auto prev = flst.before_begin();
auto curr = flst.begin();
while (curr != flst.end())//flst的end不会变化。
{
if(*curr % 2)
curr = flst.erase(prev);//prev还是指向curr的前一个位置
else
{
prev = curr;
++curr;
}
}
}
//5.改变容器的大小
1).array
不支持resize
。
操作名称 | 相关描述 |
---|---|
c.resize(n) | 改变c的大小为n。如果n < c.size() ,多的元素被丢弃。反之不足的元素进行值初始化 |
c.resize(n,t) | 系添加的元素都是t |
{
list<int> ilist(10,42);
ilist.resize(15);//末尾多了5个为0的元素
ilist.resize(25,-1);//末尾继续加10个为-1的元素
ilist。resize(5);//只剩下5个位42的元素
}
2).注意。
- 如果容器中的类型是类类型,我们要么保证类类型有默认构造函数,或者我们为它提供初始值。
- 如果
resize
是缩小容器的大小,那么指向被删除元素的指针,迭代器,引用都会失效 - 对于
vector,string,deque
进行resize
,可能导致迭代器,指针,引用失效。
//6.容器操作可能会导致迭代器失效
1).添加元素的情况。
|容器类型|插入位置|指针,引用,迭代器的有效性|
|vector或者是string
|存储空间被重新分配|全部失效|
|vector或者是string
|储存空间没有被重新分配|指向插入位置之前的元素的指针,迭代器,引用仍然有效,但是指向插入位置之后元素的指针,引用,迭代器将会失效。|
|deque
|插入到处首尾位置之外的任何位置,|全部失效|
|deque
|插入到首位位置|迭代器会失效。但是指向存在元素的指针,引用不会失效。|
|list或者forward_list
|任何位置|全部有效,包括尾后和首前|
2).删除元素的情况。
|容器类型|删除位置|指针,引用,迭代器的有效性|
|vector或者是string
|任何位置|指向删除位置之前的元素的指针,迭代器,引用仍然有效,但是指向删除位置之后元素的指针,引用,迭代器将会失效。|
|deque
|首尾位置之外的任何位置,|全部失效,意味着,位置全部发生变化,具体是如何实现的?|
|deque
|首位位置|如果是尾元素,尾后迭代器会失效。但是指向其他元素的迭代器,指针,引用不会失效。如果是首元素,则全部有效(包括尾后)。|
|list或者forward_list
|任何位置|全部有效,包括尾后和首前|
3).几点说明。
- 使用失效的指针,引用,迭代器很可能跟使用未初始化的指针发生一样的后果。
- 删除
deque
的尾元素,删除vector和string
任何位置的元素,都会导致尾后迭代器失效。 - 对于
list和forward_list
,迭代器,指针和引用时刻保证有效。 - 程序设计时,一定要注意,对于
string,vector,deque
重新定位迭代器,指针,引用。 - 删除时,对于被删除的元素,指针,引用,迭代器,必然会失效。因为元素被销毁了。
4).程序设计时要注意的问题。
- 标准库中
end()
操作是很快的,因为vector,string
以及deque
的操作经常会导致尾后迭代器失效。 - 很多标准库实现上,如果我们使用失效的
end
来做判断,while (begin != end);
将会导致无线的循环。修改为,while (begin != c.end());
即可。
练习9.31
list,forward_list
支持自增自减运算,只是不支持算术运算。- 支持大小的比较。
练习9.32
- 函数实参的求值顺序是不确定的。
/4.vector对象是如何增长的
1).vector
的内存分配测略使得,它的扩张操作,通常比list
和deque
更加地快。
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个元素的存储空间 |
3).几点说明。
reserve
并不会改变容器中元素的数量,他仅仅影响vector
预先分配多大的内存空间。- 只有当需要的内存空间超过当前的容量时,
reserve
调用才会改变vector
的容量。 - 并且,如果需求大小大于当前容量时,
reserve
至少会分配与需求一样的内存空间(可能会更大)。 - 如果内存的需求小于等于当前容量时,
reserve
什么也不做,它并不会退回内存空间。 - 值得注意的时,
resize
修改的是容器中的元素的数目,而不是容器的容量,更不可能使用resize
来缩小容器预留的存储空间。 - 尽管新标准中,我们可以调用
shrink_to_fit
成员函数来退回不需要的空间,但是具体的实现可以选择忽略这一请求。调用它,也并不能保证一定退回不需要的空间。
4).关于,capacity
和size
- 空
vector
的size
为0,标准库实现的时候,capacity
也为0; - 当我们添加元素时,
size
等于元素的数目,capacity
至少为size
大小。具体大小看标准库的实现方式。 - 实际上,如果我们没有超过
vector
的容量,vector
就不能重新分配空间。while (c.size() != c.capacity());
vector
的实现采用的是,每一次需要分配新的内存空间时。将当前的容量翻倍。
5).每一个vector
实现都可以选择自己的内存分配策略,但是必须遵守的是,只有不得已的情况下,才能重新分配空间。
- 只有在执行
insert
操作时,size
才会和capacity
相等。 - 调用
resize
或者reserve
时给定的大小超过当前的capacity
,vector
才可能会重新配分内存空间。额外多少空间,具体看实现方式。 - 不管是什么策略,在一个为空的
vector
中连续进行n此的push_back
后,所需要的时间不能超过n的常倍。
练习,9.37
- 对于
list
,size
和capacity
总是相等的。 - 对于
array
,capacity
是不可变的。
练习,9.40
- 关于
resize
是否会改变capacity
取决于编译器。
/5.额外的string操作
1).用途。
string
和c风格字符数组之间的转换- 增加了用下标代替迭代器的版本
//1.构造string的其他方法
1).从string对象看。
方式 | 相关信息 |
---|---|
string s1; | 空字符串 |
string s1(s2) | 副本,s2可以是c风格字符数组的指针,可以是cons ,因为只是拷贝 |
string s1 = s2 | 副本 |
string s1(“value”) | 不包括字面值后面的空字符 |
string s = “value” | 同上 |
string s(n,‘c’) |
2).从顺序容器角度看。(实际上string不是容器)
方式 | 相关描述 |
---|---|
string s{‘a’,‘b’,‘c’} | |
string s = {‘a’,‘b’,‘c’} | 可以有等号。 |
string s(b,e) | 迭代器范围 |
string s(n) | ??? |
3).其他构造函数。
n,len2,pos2,
都是无符号的。
方式 | 相关描述 |
---|---|
string s(cp,n) | 将cp指向的数组的前n个字符进行拷贝。数组至少有n个字符 |
string s(s2,pos2) | s是string s2 从下标pos2 开始的字符拷贝。如果pos2 > s2.size() ,函数的行为将会是未定义的 |
string s(s2,pos2,len2) | pos2的规定同上,但是不管len2 的值为多少,构造函数至多拷贝到s.size() |
4).几点说明。
- 当我们从一个
const char*
创建一个string
时,指针指向的数组必须以空字符结束,拷贝操作遇到空字符时停止。 - 如果我们还传递一个计数值,可选的,数组就不必以空字符结束。
- 如果我们没有计数值也没有空字符结尾,未定义?。或者超过范围,会抛出
out_of_range
的错误,异常终止。 - 注意,
(cp+6,5)
代表从cp[6]
开始拷贝5个元素,cp这里转化为指针了。 string
为参数时,如果pos > size
一样的,会抛出out_of_range
的错误。
5).substr
操作。
s.substr(pos,n)
,返回一个string
,包含s中从pos
开始的n个字符的拷贝。pos
的默认值是0,n的默认值是size-pos
,即是从pos
开始的全部字符。- 如果开始的
pos
是不合法的,会抛出out_of_range
的异常。 - 如果n大于范围,一样的会进行调整,得到从pos开始的全部字符串。
练习,9.41关于string
的拷贝等。
- 如果是c风格的字符串。可以使用
s = cp;
/s(cp);
s(cp,n)
;要求就是一定要以空字符结尾。
- 如果是
string
s = s1;
/s(s1)
s = (s1,pos,n);
- 如果是其他的容器。
s = (b,e);
vector
提供了一个data
成员函数,返回的是容器内存空间的首地址。那么对于vector<char>
到string
的转换
- 可以这么做,
s = (v.data(),v.size());
- 是把这个看成c风格的字符串还是?
//2.改变string的其他方法(insert,assign,erase)
1).除了接受迭代器版本的insert
以及erase
之外,string
的还有,下标版本的。
{
s.insert(s.size(),5,'!');
//在s的末尾插入5个!
//注意这里依然是,在前插入
s.erase(s.size() - 5,5);
//删除s最后的五个元素
}
2).标准库string
类型还定义提供了接受c风格字符串数组的insert
和assign
版本。
{
const char *cp = "hello world!";
s.assign(cp,5);
//s == "hello";
s.insert(s.size(),cp+5);
//s == "hello world!"
}
3).几点说明。
assign
的要求拷贝的字符一定要小于数组中的字符数。不包括结尾的空字符。- 这里的字符串需要以空字符结尾?
4).我们还可以,指定来自其他的string
或者子字符串插入到当前的string
或者赋予当前的string
{
string s = "some string";
string s1 = "some other string";
s.insert(0,s1);
//将s1插入到s的0位置之前的地方
s.insert(0,s1,0,s1.size());
//在s[0]位置之前插入s1中从s2[0]开始的s1.size()个字符。
}
5).小节。
insert
和erase
加入和下标的方式assign
则是加入和c风格字符串的方式。
6).append
和replace
函数
append
是在末尾进行插入操作的一种简写形式。
{
string s("hello world!"),sl = s;
s.insert(s.size(),"abc");
sl.append("abc");
//以上是等价的
}
replace
是调用erase
和insert
的一种简写形式。replace
的第三个参数并不要与第二个参数是一样长的。
{
s.erase(12,3);
//从当前下标位置开始删除3个内容。注意下标从0开始。
s.insert(12,"def");
//可以是字符串。
//插入方式就是直接整体插入。搬
//等价于
s.replace(12,3,"def");
//含义就是,从下标13开始删除3个元素,然后再从13位置之前插入'def'。
}
7).操作表。
修改string的操作 | 相关的描述 | 返回值 |
---|---|---|
s.insert(pos,args) | 在pos 之前插入args ,请注意,pos是下标,而且注意,下标从零开始。返回的是s的引用。注意args的4个形式 | 引用,下同 |
s.erase(pos,len) | 在pos 位置开始,依次删除pos+1到pos+len-1这些元素。如果被省略,删除从pos 开始(包括pos )直到末尾的所有字符。 | |
s.assign(args) | 替换指定字符。 | |
s.append(args) | ||
s.replace(range,args) | range 的组织形式有,迭代器范围或者下标加上长度。 |
8).args
的组织形式。
args
可以是以下形式之一,append
和assign
可以使用任何的形式。str
不可以和s相同。迭代器不可以指向s
|形式|相关描述|
|-|-|
|str|字符串|
|str,pos,len|str
中,从pos
开始至多len
个字符|
|cp,len|从cp
指向的字符数组前最多len个字符。|
|cp|cp
必须以空字符结尾|
|n,c|n个字符c|
|b,e|迭代器b和c指定的范围内的字符。|
|{}|以逗号分隔|
9).replace
和insert
所允许的形式,与range
以及pos
的形式有关。(以下列出的是不可以的)
replace | replace | insert | insert |
---|---|---|---|
(pos,len,args) | (b,e,args) | (pos,args) | (iter,args) |
b,e | str,pos,len | cp | str |
{} | b,e | str,pos,len | |
{} | cp,len | ||
cp |
10).接口就是函数。有共同的接口->重载函数。
11).字符串来源可以是
string
- 字符串字面量
- 字符串指针
- {}
- 字符加个数
12).对于string
和c风格的字符串还可以限定字符串的范围。
//3.string的搜索操作
1).搜索操作。
- 如果搜索得到,返回的是一个
string::size_type
的无符号整型数。并且指向的是字符串的第一个字符的位置。 - 如果没有匹配到,返回的是一个
string::npos
的static
成员。标准库将npos
定义为一个const string::size_type
类型,并且初始化未-1。我们知道upos
是一个无符号的整数。所以此初始值代表着,nops
代表着,任何string
对象可能的最大的大小。 - 用一个
int
或者其他带符号的整型来接受find
的返回值都是不恰当的。 - 搜索是对下标敏感的。
操作名称 | 相关描述 |
---|---|
s.find(args) | 查找args 第一次出现的位置 |
s.rfind(args) | 查找args 最后一次出现的位置 |
s.find_first_of(args) | 查找args 中任意一个字符第一次出现的位置 |
s.find_last_of(args) | 最后一次出现的位置 |
s.find_first_not_of(args) | 第一个args 中没有的s 中的字符。 |
s.find_last_not_of(args) | 查找最后一个args 中没有的s 中的字符 |
2).args
的形式。
形式 | 相关的描述 |
---|---|
c,pos | pos 指定了从哪一个位置开始搜索,默认为零,下同 |
s,pos | |
cp.pos | cp 指向的字符串需要以空字符结尾 |
cp,pos,n | 从s中的pos 开始查找指针cp指向的数组的前n个字符。pos 和n 是没有默认值的。 |
//4.compare函数
1).比较结果。
比较结果 | 返回值 |
---|---|
s > 指定字符串 | 正数 |
< | 负数 |
== | 0 |
2).参数形式。
s.compare(args)
|args|相关描述|
|-|-|
|sl||
|pos,n,sl|将s从pos开始的n个字符和sl进行比较|
|pos1,n1,sl,pos2,n2||
|cp||
|pos,n,cp|cp指向的的是以空字符结尾的字符串|
|pos,n1,cp,n2|从cp指向的地址开始进行比较。|
//5.数值转换
1).标准库中引入很多的函数,实现数值数据和标准库string
之间的转换。
- 要转换数值的
string
中的第一个非空白字符必须是,数值中可能出现的字符。例如,d = stod(s.substr(s.find_find_of("+-.1234567890")));
string
参数中,第一个非空字符必须是+,-,或者数字。
- 可以是以0x、0X开头的十六进制数
- 转换为浮点值的函数,可以是**小数点.**开头,并且以e或者E来表示指数部分。
- 对于转换为整型的字符串。根据进制的不同,例如十六进制的。
- 如果
string
不可以转换为一个数值,那么函数将会抛出一个invalid_argument
的异常。如果转换得到的数值无法用任何类型类表示,那么将会抛出一个out_of_range
的异常。
2).转换函数。
操作 | 相关描述 |
---|---|
to_string(val) | 是一组重载的函数。val可以是任何算术类型。每个浮点型或者int或者更大的类型都有相应的版本。小整型会被提升 |
stoi(s,p,b) | 返回int类型的数值。b表示的是进制。默认是10。p是size_t 下标 |
stol,stoll,stoul,stoull, | |
stof(s,p) | p的作用是一样的 |
stod,stold |
{
string str = "1010";
int a = stoi(str, 0, 2);//p表示位置,2表示的是进行时str中的进制。
cout << a << endl;
return 0;
}
/6.容器适配器
1).三种容器适配器,以及它们可以作为基础的顺序容器。
|适配器类型|可以作为基础的顺序容器|原因|
|stack|除了array
和forward_list
|push_back
,pop_back
,back
|
|queue|list
,deque
|push_back
,pop_front
,back
,front
|
|priority_queue|vector
,deque
|front
,push_back
,pop_back
,随机访问|
2).所有适配器都支持的操作。
操作名称 | 相关描述 |
---|---|
size_type | 一种类型,表示适配器的大小 |
value_type | 元素类型 |
container | 实现适配器的底层容器 |
A a | 创建空的适配器 |
A a© | 带有容器c的拷贝的适配器 |
关系运算符 | 全支持,返回的是底层容器的比较结果 |
a.empty() | |
a.size() | |
swap(a,b) | 交换适配器的内容,要求,类型包括底层容器类型,全部一样。 |
a.swap(b) |
3).我们可以重载默认的底层容器。
stack
和queue
的默认值是deque
priority
的默认值是vector
- 重载形式。
stack<int,vector>
4).几点说明。
- 因为所有适配器要求,可以添加或者删除,访问尾元素的能力,所以
array
以及forward_list
是不可以作为底层容器的 - 每一个栈都有自己的操作。虽然是底层实现,但是我们不可以在适配器中使用底层实现的操作。例如在
stack
中使用push_back
是错误的。
5).stack
适配器。
操作 | 相关描述 |
---|---|
s.pop() | 删除栈顶元素,但是不会返回值 |
s.push(item) | 压入栈顶。通过拷贝或者移动item而来 |
s.emplace(args) | 由args 进行构造 |
s.top() | 返回栈顶 |
6).queue
、priority
适配器。
操作 | 相关描述 |
---|---|
q.pop() | 返回的是queue 的首元素,或者priority——queue 的最高优先级元素。但是不会删除该元素。 |
q.front() | 只适用于queue ,返回的是首元素,但是不会删除该元素 |
q.back() | 只适用于queue ,返回尾元素。 |
q.top() | 返回最高优先级的元素,但是不会删除该元素。只适用于priority_queue |
q.push(item) | 在priority_queue 的适当的位置或者queue 的末尾创建一个元素 |
q.push(args) |