顺序容器(下)——跟我一起从C到C++(第十一期)

10 顺序容器

承接顺序容器(上)——跟我一起从C到C++(第十期)

10.4 管理迭代器

10.4.1 容器操作可能使迭代器失效

向容器中添加元素或者从容器中删除元素可能使容器元素的指针、引用或者迭代器失效。一个失效的指针、引用或者迭代器不再能表示任何元素。使用就会造成严重的程序设计错误,可能引起与使用未初始化指针一样的问题。
向容器添加元素后:

  • 如果容器是vectorstring,且存储空间被重新分配,则指向容器的迭代器、指针、引用都会失效。如果存储空间未重新分配,指向插入位置之前的元素的迭代器、指针和引用仍然有效,但指向插入位置之后的元素的迭代器、指针和引用将会失效。
  • 对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用失效。如果在首尾位置添加元素,迭代器会失效,但指向存在的元素的引用和指针不会失效。
  • 对于listforward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针、引用仍然有效。

当删除一个元素后:

  • 当我们删除元素时,尾后迭代器总会失效。
  • 对于listforward_list,指向容器的迭代器(包括尾后迭代器和首前迭代器)、指针、引用仍然有效。
  • 对于deque,如果在首尾之外的任何位置删除元素,那么指向被指向被删除元素外的其他元素的迭代器、指针和引用都会失效。如果删除的是deque的尾元素,则尾后迭代器也会失效,但其他的迭代器、指针和引用不受影响;如果是删除首元素,这些也不会受影响。
  • 对于vectorstring,指向被删除元素之外的元素的迭代器、引用和指针仍有效。

10.4.2 改变容器的循环

添加或者删除元素的时候,循环程序必须考虑迭代器、引用、指针可能失效的问题。程序必须保证每个循环步骤中都更新迭代器、引用或者指针。如果循环中调用的是inserterase,那么更新迭代器会很容易。这些操作都会返回迭代器,我们可以用来更新:

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空间的动态增长

vectorstring的情况类似,这里就用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

当我们加入元素,vectorsizecapacity都会增加:

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来要求dequevectorstringcapacity()减少到size()相同大小。这意味着要求容器归还超出当前大小的多余内存。

每个vector实现都可以选择自己的内存分配策略。但是必须遵守一条原则:只有迫不得己的时候才可以分配新的内存空间。

10.6 额外的string操作

除了顺序容器共同的操作之外,string类型还提供了一些额外的操作。这些操作中大部分要么提供string类和C风格字符数组之间的相互转换,要么是增加了允许我们用下标替代迭代器版本。

10.6.1 构造string的其他方法

nlen2pos2都是无符号值。

操作功能
string s(cp,n)scp指向的数组中前n个字符的拷贝
string s(s2,pos2)sstring s2从下标pos2开始的字符的拷贝。如果pos2>s2.size(),则构造函数的行为未定义
string s(s2,pos2,len2)sstring s2从下标pos2开始len2个字符的拷贝。如果pos2>s2.size(),则构造函数的行为未定义

这些构造函数参数cp接受一个string或者一个const char *参数。

10.6.2 substr操作

操作功能
substr(pos,n)返回一个string。包含s中从pos开始的n个字符的拷贝。pos默认值为0n的默认值为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类型支持顺序容器的赋值运算符、assigninserterase操作。除此之外,它还定义了额外版本的inserterase
yes,除了接受迭代器版本的inserterasestring还提供接受下标版本的版本:

s.insert(s.size(), 5, '!');//在s末尾插入5个感叹号  
s.erase(s.size() - 5, 5);//在s删除最后5个字符

因为要与C风格字符串打交道,string类型提供了接受C风格字符串的insertassign

const char *cp = "Stately, plump buck!";  
s.assign(cp, 7);//Stately  
s.insert(s.size(), cp+7);//Stately, plump buck!

而且,string类型还定义了两个额外的appendreplace

append操作是在string末尾进行插入的一种简写形式:

s.insert(s.size(), "world"); 

可以变成

s.append("world");

replace操作是在eraseinsert的一种简写形式:

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::nposstatic成员。

  • 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,posspos位置找字符cpos默认为0
s2,posspos位置字符串s2pos默认为0
cp,posspos位置C风格字符串指针cppos默认为0
cp,pos,nspos位置C风格字符串指针cp的前n个字符,posn无默认值

10.6.5 compare函数

string类型的compare函数很类似C标准库里的strcmp函数。
根据s是等于、大于或者小于参数指定的字符串,s.compare返回0、正数或者负数。

操作功能
s.compare(s2)比较ss2
s.compare(pos1,n1,s2)s中从pos1开始的n1个字符与s2比较
s.compare(pos1,n1,s2,pos2,n2)s中从pos1开始的n1个字符与s2pos2开始的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风格字符串cppos2开始的n2个字符比较

10.6.6 数值转换

字符串常常包含数值,我们可以通过一些函数进行转换:

操作功能
to_string(val)一组重载函数。返回数值valstring表示(小整形会被提升)
stoi(s,p,b)
stol(s,p,b)
stoul(s,p,b)
stoll(s,p,b)
stoull(s,p,b)
返回s的起始字串的数值,返回类型分别是intlongunsigned longlong longb表示转换所用的基数,默认值为10psize_t指针,用来保存第一个非数值的字符的下标,p默认为0
stof(s,p)
stod(s,p)
stould(s,p)
返回s的起始字串的数值,返回类型分别是floatdoublelong doublepsize_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 容器适配器

除了顺序容器外,标准库还定义了三个顺序容器适配器:stackqueuepriority_queue
适配器是标准库中的一个通用概念。容器、迭代器和函数都有适配器。
一个适配器就是一种机制,使其行为看起来像一种不同的类型。例如,stack适配器接受一个顺序容器(除arrayforward_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)
交换ab的内容,ab必须有相同类型,包括底层容器类型也必须相同

10.7.1 定义一个适配器

我们可以用一个容器来初始化一个新的适配器,

deque<int> deq;  
stack<int> stk{deq};

默认情况下,stackqueue是基于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_backpop_back操作,也就是除了arrayforward_list都可以用来构造stack
  • queue要求backpush_backfrontpush_front,因此它可以在list或deque之上构造,但不能建立在vector
  • priority_queue要求push_backfrontpop_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不能使用顶层容器dequepush_back,而是自己的push

10.7.3 队列适配器

queuepriority_queue适配器定义在头文件<queue>
其中,queue默认基于deque实现,而priority_queue默认基于vector实现。
当然,queue也可以用listvector实现,而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等等,但不能用 listSTL里面默认用的是vector),Functional 就是比较的方式,当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
一般就是:

//升序队列
priority_queue <int,vector<int>,greater<int> > q;
//降序队列
priority_queue <int,vector<int>,less<int> >q;

//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)

更多的用法,以后会在专文总提到,这里就让我们先有个印象就行。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值