3)string类对象的访问及遍历操作
函数名称 | 功能说明 |
operator[] (重点) | 返回pos位置的字符,const string类对象调用 |
begin + end | begin获取第一个字符的迭代器 + end获取最后一个字符下一个位置的迭代器 |
rbegin + rend | rbegin获取最后一个字符的迭代器 + rend获取第一个字符前一个位置的迭代器 |
范围for | C++11支持更简洁的范围for的新遍历方式 |
① operator[]
char& operator[] (size_t pos); const char& operator[] (size_t pos) const;
首先我们来说说这个
operator[]
,相信学习过 类和对象的运算符重载 的同学一定不陌生
- 之前我们都是像下面这样去访问string字符串的,那现在有了这个
operator[]
,我们就可以使用【下标 + [ ]】的形式去访问字符串中的每一个元素
void TestOperator() { string s("abcdef"); cout << s << endl; }
- 那就是像下面这样去做一个访问即可,要使用到的是我们前面所学的【size】这个接口,获取到字符串的大小
for (int i = 0; i < s.size(); i++) { cout << s[i] << " "; }
我们知道,其实这个
string
类的对象会在堆区中开辟出一块数组空间来存放相对应的字符,最后为了和C语言保持一致会在最后面加上一个\0
,那为何这里在打印的时候没有看到呢?
- 通过调试来进行观察,我们可以发现其在遍历的过程中并没有遇到
\0
,这是为何呢?
- 这其实是因为string的封装得过多了,因此我们在进行观看的时候需要一直点到最里面才可以,继而发现了我们的
\0
- 那既然我们可以通过【下标 + [ ]】的形式去访问字符串中的每一个元素,那在访问的同时是否可以进行修改呢?这当然是可以的,马上来试试👇
for (int i = 0; i < s.size(); i++) { s[i]++; }
- 打印一下可以看到,每个元素 + 1之后再去遍历打印的时候就有了不同的结果
- 不仅如此,我们还可以单独使用,将每个元素++之后我们再把
s[0]--
,那么在打印的时候看到的结果即为[a]
从上面的种种我们可以看到这个
operator[]
使得字符串可以让我们像数组一样去使用,做增、删、查、改很方便
- 但是呢,上面这种string
[]
的形式和下面这样对字符数组的访问是有本质区别的
string s("abcdef"); char s2[] = "hello world"; s[1]++; // -> operator[](1)++ s2[1]++; // -> *(s2 + 1)++
- 这一块我们可以通过汇编来进行查看,发现
s[1]++
在底层是转换为operator[]
的形式;但是对于s[2]++
却是在做一些解引用的操作,这一块看不懂也没关系,但在学习了C语言操作符后我们要知道对于[]
来说其实就是一种解引用的形式
【温馨提示】:只能用于string
+ vector
+ deque
,但是链表、树不能用,链表各个结点的空间不连续
- 对于【vector】和【deque】,我在后续都会讲解到,它们都是STL中的容器,而且在内存中与
string
一样都是连续的,因此我们可以像访问数组一样去访问里面的元素。但是呢,像【链表】、【树】这样的结果,它们的一个个结点在空间中都是都是离散的,无法做到像数组那样去连续访问
② at
当然,除了下标 +
[]
的形式,我们还可以通过【at】的形式去做一个访问
- 一样通过查看文档来观测一下,也是具有两个重载,一个是普通对象,一个则是const对象
void TestAt() { string str("abcdef"); for (int i = 0; i < str.size(); i++) { cout << str.at(i) << " "; } cout << endl; }
- 可以看到可以在边遍历的时候边修改字符串中的值
我们再来看看这个const对象
const char& at (size_t pos) const;
void func(const string& s) { for (int i = 0; i < s.size(); i++) { s.at(i)++; cout << s.at(i) << " "; } }
- 可以观察到,此时我们再去修改这个字符串中的内容时就会出问题了,原因就在于这个对象
s
具有常性,是无法修改的
对于
oparator[]
和at()
来所,还要再做一个对比,即它们在处理异常这一块
可以看到对于上面的oparator[]
来说若是产生了一个越界访问的话就直接报出【断言错误】了
然后看到对于at()
来说虽然也是弹出了警告框,但是呢这个叫做【抛异常】,之后我们在学习C++异常的时候会讲到的
- 此时我们应该再去仔细地看一看文档,里面说到在检查出所传入的
pos
位置有问题时,就会报出out_of_range
的异常,这也就印证了上面的现象
- 那对于异常而言都是可以去捕获的,那就是采用
try...catch
的形式。此时我们再运行的话就可以发现此异常被捕获了,而且打印出了异常的信息
③ 迭代器
那接下去呢,我就要来讲讲【迭代器】了,它是我们在学习STL的过程中很重要的一部分内容,让我们对容器的理解能够更上一层楼
- 在这之前呢,我们认识两个最常见的接口函数,即为
begin
和end
- begin获取一个字符的迭代器
- end获取最后一个字符下一个位置的迭代器
- 迭代器是是另一种访问string中内容的方式,图示如下,我们使用一个
it
去保存这个字符串【begin】处的位置,那么在其不断进行后移的过程中,就是在遍历这个字符串,当其到达最后的【end】处时,也就遍历完了,此刻便会停了下来
- 好,我们一起来看看这段代码
void TestIterator() { string s("abcdef"); string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; it++; } cout << endl; }
- 除此之外,迭代器也可以像数组那样在遍历的时候修改内部的元素,
it
取到的是每个元素的位置,那么对于*it
来说即为每个元素
string::iterator it = s.begin(); while (it != s.end()) { (*it)++; it++; }
- 来观察下运行结果就发生也没问题,可以进行修改
- 因此我们看迭代器的这种方式,其实和指针非常得类似,不过呢不能完全这样说,所以你可以说
iterator
是像指针一样的类型,有可能是指针,有可能不是指针
这边再拓展一点,上面说到迭代器在我们学习STL的过程中起着很大的作用,原因就在于其他的容器都可以使用这种形式来进行遍历
void TestIterator2() { vector<int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push_back(4); vector<int>::iterator vit = v.begin(); while (vit != v.end()) { cout << *vit << " "; vit++; } }
- 可以看到,对于
vector
容器来说,也是可以使用迭代器去做一个遍历的
- 不仅如此
list
也是可以使用迭代器来进行访问的
💬 好,上面仅仅作为拓展,如果读者不懂得话也没关系,下一文就会学习到
【小结】:
好,对上面的内容做一个小结。iterator提供一种统一的方式访问和修改容器
还记得我们在初步认识STL的时候讲到的STL的六大组件,除了【容器】之外最重要的就是【算法】,这里我先简单地介绍几个算法并演示一下
- 首先就是我们使用到最多的【reverse】函数,字面意思:颠倒元素
- 观察其参数我们可以发现,传入两个迭代器即可,那刚好就是我们前面所学的【begin】和【end】
reverse(s.begin(), s.end());
- 一起来看一下结果就可以发现确实string字符串内的字符都发生了一个翻转,但是有一个头文件
#include <algorithm>
可不要忘记了哦
- 同样,对于
vector
容器来说也是同样适用
- 好,再来说一个【sort】,也很明了,就是对区间内的元素去做一个排序的操作,此时我们可以看到两个重载形式,第一个就是正常传入区间迭代器,而第二个重载形式则是可以传递【仿函数】,它也是STL的六大组件之一,我们在后续也会进行学习,这里先提一句
- 如果你有看过 C语言回调函数 的话就可以很清晰地看出来是它们很类似,这里不做展开
- 立马,我们来看看如何去进行使用,也是传递【begin】和【end】即可
sort(s.begin(), s.end());
- 通过运行结果我们可以看到再 通过
sort
进行排序后原本的乱串变成了有序串
💬 其余容器的这里就不演示了,读者可自己下去试试看,总结一下:算法可以通过迭代器去处理容器中的数据
好,讲完正向迭代器,我们再来说说【反向迭代器】
- 首先我们要来了解一下新的两个接口【rbegin】和【rend】
- 还是结合具体图示来观察一下,对于【rbegin】来说指向的是最后一个字符的位置,对于【rend】来说它指向的是第一个字符的前一个位置,
- 好,我们来看一下具体该如何去使用,其实和 正向遍历 非常相似,只是这个迭代器我们要换一下,通过它们二者的返回值其实就可以看得出出来
reverse_iterator rbegin(); reverse_iterator rend();
展示一下代码
string::reverse_iterator rit = s.rbegin(); while (rit != s.rend()) { cout << *rit << " "; rit++; }
再来看看结果
好,讲完了正向和反向迭代器后,我们再来拓展地讲一些东西
- 仔细地观察一下这四个接口函数,发现除了
iterator
和reverse_iterator
之外,还有const_iterator
和const_reverse_iterator
,那后面的这两个我们要如何去使用呢?
- 我们这里将迭代器遍历封装为函数,采取引用传值减少拷贝构造,那还需要加上
const
做修饰防止权限放大
void Func(const string& s) { string::iterator it = s.begin(); while (it != s.end()) { cout << *it << " "; it++; } }
- 但是呢当我在编译之后却发现出了问题,说是无法去进行一个转换
- 那我们用用上面看到的两个新的迭代器试试,发现确实不会有问题了,原因就是在于对象
s
是属于const对象,那么它在调用【begin】的时候返回的就是const迭代器,是【只读】那此时我们若是使用普通迭代去接收的话就是【可读可写】,也算是一个权限放大的问题
- 我们再来试试反向迭代器,可以发现也是具有同样的问题
- 此时只有将迭代器换成
const_reverse_iterator
才可以,但你是否觉得这样写过于复杂了呢?
string::const_reverse_iterator rit = s.rbegin();
- 如果有同学了解C++11的关键字
auto
的话就可以清楚其可以完成自动类型转换的功能,不需要我们去关心具体的类型,这个关键字我在下面讲到【范围for】的时候还会再提到的,读者可以自行先了解一下
auto rit = s.rbegin();
- 那么这个迭代器是否真的能做到【只读】呢,我们去修改一下即可,发现确实是呈现一种只读的效果
【总结】:
- 好,最后来总结一下我们上面所学习的四种迭代器,分别是
④ 范围for
好,我花了很大的篇幅在介绍迭代器之后,我们再来讲讲范围for,这个是C++11才出来的,现在被广泛地使用
- 很简单,我们马上来看看具体的代码,它就是一种语法糖的,这里的
auto
就是我们上面所说到过的【自动类型推导】,那这里如果我们不用auto
的话直接使用char
也是可以的
void TestRangeFor() { string s("abcdef"); for (auto ch : s) { cout << ch << " "; } cout << endl; }
- 来讲讲它的原理,通过汇编我们可以看到,范围for的底层实现还是【迭代器】,所以我们可以说在它在遍历的时候相当于是将
*it
的数据给到当前的ch,和迭代器的本质还是类似的
- 那这个范围for既然的底层实现既然都是迭代器的话,是否也可以像迭代器那样在遍历的时候去做一个修改呢?这当然是可以的喽~
- 但是呢,下面这样就不可以啦,因为这样的话
ch
在遍历的时候每次只会是当前字符的一份拷贝,那么在循环遍历结束后ch
每一次的变化是不会导致字符串s发生变化的
- 那我们只需要让ch和字符串每一个字符所属同一块空间即可,那这个时候就使用我们所学习的【引用】即可
【注意事项】:
- 好,接下去我们几个注意事项
一个类如果不支持迭代器就不支持范围for,因为范围for的底层使用的也是迭代器
- 不是所有的类都支持迭代器的,例如我们之后要学习的
stack
类,它就是不支持的
- 可以看到,不支持迭代器,也是不支持范围for的
只能正着遍历,但是不能倒着
- 不仅如此,范围for也是不支持像迭代器那样倒着遍历的,这个无法演示,读者可以自行思考一下🤔
⑤ front 和 back
然后再来拓展两个C++11中新接口,看这个字面其实就可以看出【front】取到的是字符串的首字符,而【back】取到的则是字符串的尾字符
void TestBackAndFront() { string str("abcdef"); cout << str.front() << " " << str.back() << endl; }
- 可以看到,确实取到了字符【a】和字符【f】