文章目录
10 顺序容器
10.4 管理迭代器
10.4.1 容器操作可能使迭代器失效
向容器中添加元素或者从容器中删除元素可能使容器元素的指针、引用或者迭代器失效。一个失效的指针、引用或者迭代器不再能表示任何元素。使用就会造成严重的程序设计错误,可能引起与使用未初始化指针一样的问题。
向容器添加元素后:
- 如果容器是
vector
或string
,且存储空间被重新分配,则指向容器的迭代器、指针、引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍然有效,但指向插入位置之后的元素的迭代器、指针和引用将会失效。 - 对于
deque
,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。 - 对于
list
和forward_list
,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针、引用仍然有效。
当删除一个元素后:
- 当我们删除元素时,尾后迭代器总会失效。
- 对于
list
和forward_list
,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针、引用仍然有效。 - 对于
deque
,如果在首尾之外的任何位置删除元素,那么指向被指向被删除元素外的其他元素的迭代器、指针和引用都会失效。如果删除的是deque
的尾元素,则尾后迭代器也会失效,但其他的迭代器、指针和引用不受影响;如果是删除首元素,这些也不会受影响。 - 对于
vector
或string
,指向被删除元素之外的元素的迭代器、引用和指针仍有效。
10.4.2 改变容器的循环
添加或者删除元素的时候,循环程序必须考虑迭代器、引用、指针可能失效的问题。程序必须保证每个循环步骤中都更新迭代器、引用或者指针。如果循环中调用的是insert
或erase
,那么更新迭代器会很容易。这些操作都会返回迭代器,我们可以用来更新:
vector<int> v = {0, 1, 2, 3, 4, 5};
auto iter = v.begin();
while (iter != v.end())
{
if (*iter % 2)
{
iter = v.insert(iter, *iter);
iter += 2;
}
else
iter = v.erase(iter);
}
这里还要讲一个习惯——不要保存end()
返回的迭代器。
通常C++标准库中的end()
处理都很快速,我们不用试图“优化”这个循环,反则当我们在循环中进行一些操作使得保存end()
中的迭代器失效了,那么这个迭代器讲不再指向容器中的任何元素或是尾后元素了。
所以你会发现,程序员们一般:
auto iter = v.begin();
while (iter != v.end())
而不是
auto iter = v.begin();
auto last = v.end();
while (iter != last)
10.5 vector对象是如何增长的
参考:
vector空间的动态增长
vector
和string
的情况类似,这里就用vector
进行描述。
当添加元素时,如果vector
空间大小不足,则会以原大小的两倍另外配置一块较大的新空间,然后将原空间内容拷贝过来,在新空间的内容末尾添加元素,并释放原空间。vector
的空间动态增加大小,并不是在原空间之后的相邻地址增加新空间,因为vector
的空间是线性连续分配的,不能保证原空间之后有可供配置的空间。因此,对vector
的任何操作,一旦引起空间的重新配置,指向原vector
的所有迭代器就会失效。
10.5.1 管理容量
vector
提供了一些成员函数,允许我们与它的实现中内存分配部分互动。capacity
操作告诉我们容器不扩张内存空间的情况下可以容纳多少个元素。reserve
操作告诉我们通知容器它应该准备多少个元素。
size()
函数返回的是已用空间大小, capacity()
返回的是总空间大小, capacity()-size()
则是剩余的可用空间大小。当size()
和capacity()
相等,说明vector
目前的空间已被用完,如果再添加新元素,则会引起vector
空间的动态增长。
由于动态增长会引起重新分配内存空间、拷贝原空间、释放原空间,这些过程会降低程序效率。因此,可以使用 reserve(n)
预先分配一块较大的指定大小的内存空间,这样当指定大小的内存空间未使用完时,是不会重新分配内存空间的,这样便提升了效率。只有当n>capacity()
时,调用reserve(n)
才会改变vector
容量。
resize()
成员函数只改变元素的数目,不改变vector
的容量。
vector<int> v;
cout << "size: " << v.size()
<< " capacity: " << v.capacity() << endl;
输出:size: 0 capacity: 0
当我们加入元素,vector
的size
和capacity
都会增加:
v.push_back(5);
cout << "size: " << v.size()
<< " capacity: " << v.capacity() << endl;
输出:size: 1 capacity: 1
capacity
的增长至少与size
一样大,具体分配多少额外空间则视标准库具体实现而定:
for (vector<int>::size_type i = 0; i != 24; ++i)
v.push_back(i);
cout << "size: " << v.size()
<< " capacity: " << v.capacity() << endl;
输出:size: 24 capacity: 32
我们也可以用reserve
设置预分配空间
vector<int> v;
v.reserve(50);
cout << "size: " << v.size()
<< " capacity: " << v.capacity() << endl;
输出:size: 0 capacity: 50
当这新分配的空间50
也被用完了,就再拿给程序分配,分配的具体值又得依赖于标准库的具体实现了。
这里还有一种操作shrink_to_fit
来要求deque
、vector
或string
将capacity()
减少到size()
相同大小。这意味着要求容器归还超出当前大小的多余内存。
每个vector实现都可以选择自己的内存分配策略。但是必须遵守一条原则:只有迫不得己的时候才可以分配新的内存空间。
10.6 额外的string操作
除了顺序容器共同的操作之外,string
类型还提供了一些额外的操作。这些操作中大部分要么提供string
类和C风格字符数组之间的相互转换,要么是增加了允许我们用下标替代迭代器版本。
10.6.1 构造string的其他方法
n
、len2
、pos2
都是无符号值。
操作 | 功能 |
---|---|
string s(cp,n) | s 是cp 指向的数组中前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() ,则构造函数的行为未定义 |
这些构造函数参数cp
接受一个string
或者一个const char *
参数。
10.6.2 substr操作
操作 | 功能 |
---|---|
substr(pos,n) | 返回一个string 。包含s 中从pos 开始的n 个字符的拷贝。pos 默认值为0 。n 的默认值为s.size()-pos ,即拷贝从pos 开始的全部字符 |
substr
操作返回一个string
,它的原始string的一部分或者全部的拷贝。可以传递给substr
一个可选的开始位置和计数值:
string s("hello world");
string s2 = s.substr(0, 5);//hello
string s3 = s.substr(6);//world
string s4 = s.substr(6, 11);//world
string s5 = s.substr(12);//抛出一个out_of_range异常
10.6.3 改变string的其他方法
string
类型支持顺序容器的赋值运算符、assign
、insert
和erase
操作。除此之外,它还定义了额外版本的insert
和erase
。
yes,除了接受迭代器版本的insert
和erase
,string
还提供接受下标版本的版本:
s.insert(s.size(), 5, '!');//在s末尾插入5个感叹号
s.erase(s.size() - 5, 5);//在s删除最后5个字符
因为要与C风格字符串打交道,string
类型提供了接受C风格字符串的insert
和assign
:
const char *cp = "Stately, plump buck!";
s.assign(cp, 7);//Stately
s.insert(s.size(), cp+7);//Stately, plump buck!
而且,string类型还定义了两个额外的append
和replace
append
操作是在string
末尾进行插入的一种简写形式:
s.insert(s.size(), "world");
可以变成
s.append("world");
而replace
操作是在erase
和insert
的一种简写形式:
string s("hello 5 world");
s.erase(6, 1);
s.insert(6, "4");//hello 4 world
可以变成
s.replace(6, 1, "4");//hello 4 world
而且,调用replace
操作时,与插入文本与删除文本不一样长时,也可以生效:
s.replace(6, 1, "4th");//hello 4th world
10.6.4 搜索操作
string
类提供了6种不同的搜索函数,
每个搜索操作都会返回一个string::size_type
值,表示匹配发生位置的下标。
如果搜索失败,则会返回一个名为string::npos
的static
成员。
- find
find
函数完成最简单的搜索,它查找参数定义的字符串,若找到,则返回第一个匹配位置的下标,否则就是npos
。
string s("united state");
auto pos = s.find("united");// pos == 0
- find_first_not_of
find_first_not_of
搜索第一个不在参数中的字符:
string numbers("0123456789");
string s("0377114p3");
auto pos = s.find_first_not_of(numbers);//7
- 指定在哪里开始搜索
find_first_not_of
会返回参数中任何一个字符第一次出现的位置。
string numbers("0123456789");
string s("r2d2");
string::size_type pos = 0;
while ((pos = s.find_first_not_of(numbers, pos)) != string::npos)
{
cout << "found number at index: " << pos
<< " element is " << s[pos] << endl;
++pos;
}
输出:
found number at index: 0 element is r
found number at index: 2 element is d
- 逆向搜索
find
由左往右搜索,而rfind
由右往左搜索:
string s("r2d2");
auto pos = s.find("2");//1
auto rpos = s.rfind("2");//3
类似的,find_last
的函数的功能与find_first
的函数相似,只是它们返回最后一个而不是第一个匹配。
总结一下就是:
搜索操作返回指定的字符出现的下标,否则就是string::npos
操作 | 功能 |
---|---|
s.find(args) | 查找args 第一次出现的位置 |
s.rfind(args) | 查找args 最后一次出现的位置 |
s.find_first_of(args) | 查找args 中任何一个字符第一次出现的位置 |
s.find_last_of(args) | 查找args 中任何一个字符最后一次出现的位置 |
s.find_first_not_of(args) | 查找第一个不在args 中的字符的位置 |
s.find_last_not_of(args) | 查找最后一个不在args 中的字符的位置 |
args 形式 | 含义 |
---|---|
c,pos | 从s 的pos 位置找字符c ,pos 默认为0 |
s2,pos | 从s 的pos 位置字符串s2 ,pos 默认为0 |
cp,pos | 从s 的pos 位置C风格字符串指针cp ,pos 默认为0 |
cp,pos,n | 从s 的pos 位置C风格字符串指针cp 的前n 个字符,pos 和n 无默认值 |
10.6.5 compare函数
string
类型的compare
函数很类似C标准库里的strcmp
函数。
根据s
是等于、大于或者小于参数指定的字符串,s.compare
返回0
、正数或者负数。
操作 | 功能 |
---|---|
s.compare(s2) | 比较s 和s2 |
s.compare(pos1,n1,s2) | 将s 中从pos1 开始的n1 个字符与s2 比较 |
s.compare(pos1,n1,s2,pos2,n2) | 将s 中从pos1 开始的n1 个字符与s2 从pos2 开始的n2 个字符比较 |
s.compare(cp) | 比较s 和C风格字符串cp |
s.compare(pos1,n1,cp) | 将s 中从pos1 开始的n1 个字符与C风格字符串cp 比较 |
s.compare(pos1,n1,cp,pos2,n2) | 将s 中从pos1 开始的n1 个字符与C风格字符串cp 从pos2 开始的n2 个字符比较 |
10.6.6 数值转换
字符串常常包含数值,我们可以通过一些函数进行转换:
操作 | 功能 |
---|---|
to_string(val) | 一组重载函数。返回数值val 的string 表示(小整形会被提升) |
stoi(s,p,b) stol(s,p,b) stoul(s,p,b) stoll(s,p,b) stoull(s,p,b) | 返回s 的起始字串的数值,返回类型分别是int 、long 、unsigned long 、long long 。b 表示转换所用的基数,默认值为10 。p 是size_t 指针,用来保存第一个非数值的字符的下标,p 默认为0 。 |
stof(s,p) stod(s,p) stould(s,p) | 返回s 的起始字串的数值,返回类型分别是float 、double 、long double 。p 是size_t 指针,用来保存第一个非数值的字符的下标,p 默认为0 。 |
sto
系列的函数必须把数值放在首位。
比如,
字符串和整型数值互转:
int i = 42;
string s = to_string(i);//int -> string
double d = stod(s);//string -> double
字符串提取浮点数:
string s("pi = 3.14");
double d = stod(s.substr(s.find_first_of("+-.1234567890")));
重点说一下这个例子,它先利用find_first_of
找到第一次含+-.1234567890
的字符串然后返回所在下标,并使用substr
将前面不能转换的地方分离,然后进行stod
转换。
10.7 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack
、queue
和priority_queue
。
适配器是标准库中的一个通用概念。容器、迭代器和函数都有适配器。
一个适配器就是一种机制,使其行为看起来像一种不同的类型。例如,stack
适配器接受一个顺序容器(除array
或forward_list
外)并使其操作像一个stack
一样。
所有的容器适配器都有如下操作:
操作 | 功能 |
---|---|
size_type | 一种类型,足以保存当前类型的最大对象的大小 |
value_type | 元素类型 |
container_type | 实现适配器的底层容器的类型 |
A a; | 创建一个名为a 的空适配器 |
A a(c); | 创建一个名为a 的适配器,带有容器c 的拷贝 |
== 、!= 、< 、> 、<= 、>= | 关系运算符 |
a.empty() | 若包含a 的任意元素,则返回false ;否则返回true |
a.size() | 返回a 包含的元素数目 |
a.swap(b) swap(a,b) | 交换a 和b 的内容,a 和b 必须有相同类型,包括底层容器类型也必须相同 |
10.7.1 定义一个适配器
我们可以用一个容器来初始化一个新的适配器,
deque<int> deq;
stack<int> stk{deq};
默认情况下,stack
和queue
是基于deque
实现的,priority_queue
是在vector
之上实现的。我们可以在创建一个适配器时将一个命名的顺序容器作为第二个类型参数,来重载默认容器模型:
stack<string, vector<string>> str_stk;
//用vector<string>构造的类型为string的适配器str_stk
vector<string> svec;
stack<string, vector<string>> str_stk2(svec);
对于一个给定的适配器,可以使用哪些容器是受到限制的,所有适配器都要求容器具有添加和删除元素的功能。
因此,适配器不能构造在array
之上。
类似的,我们也不能用forward_list
来构造适配器,因为所有适配器都要有添加、删除以及访问尾元素的能力。
- stack只要求
push_back
、pop_back
操作,也就是除了array
和forward_list
都可以用来构造stack
。 queue
要求back
、push_back
、front
和push_front
,因此它可以在list或deque之上构造,但不能建立在vector
。priority_queue
要求push_back
、front
和pop_front
,而且还要求有随机访问能力,因此它可以在vector
或deque之上构造,但不能建立在list
。
10.7.2 栈适配器
stack
类型定义在头文件<stack>
stack
类型基于deque
实现,但也可以用vector
或者list
构造。
操作 | 功能 |
---|---|
s.pop() | 删除栈顶元素,但不能返回该元素值 |
s.push(item) | 压入栈 |
s.emplace(args) | 压入栈 |
s.top() | 返回栈顶元素,但不弹出栈 |
例如:
stack<int> s;
for (size_t ix = 0; ix != 10; ++ix)
s.push(ix);
while (!s.empty())
{
int val = s.top();
cout << val << " ";
s.pop();
}
输出:
9 8 7 6 5 4 3 2 1 0
总所周知,栈stack
是先进后出的。
而且s
不能使用顶层容器deque
的push_back
,而是自己的push
。
10.7.3 队列适配器
queue
和priority_queue
适配器定义在头文件<queue>
。
其中,queue
默认基于deque
实现,而priority_queue
默认基于vector
实现。
当然,queue
也可以用list
或vector
实现,而priority_queue
也可以用deque
实现。
操作 | 功能 |
---|---|
q.pop() | 删除队列首元素,但不能返回该元素值 |
q.push(item) | 在队列末尾插入一个元素 |
q.emplace(args) | 在队列末尾插入一个元素 |
q.front() | 返回首元素,但不删除该元素 |
q.back() | 返回尾元素,但不删除该元素(只适用于queue ) |
q.top() | 返回最高优先级元素,但不删除该元素(只适用于priority_queue ) |
总所周知,队列queue
是先进先出的(FIFO,进入的是队尾,出去的是队首,就好比排队)
不过,这里多了一个priority_queue
,允许我们为队列中的元素建立优先级。新加入的元素会排在所有优先级比它低的已有元素之前。就比如饭店按照客人预定的时间而不是到来的时间的早晚为他们安排座位。
定义:priority_queue<Type, Container, Functional>
Type
就是数据类型,Container
就是容器类型(Container
必须是用数组实现的容器,比如vector
,deque
等等,但不能用 list
。STL
里面默认用的是vector
),Functional
就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
一般就是:
//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
更多的用法,以后会在专文总提到,这里就让我们先有个印象就行。