三、String迭代器
1.迭代器介绍
迭代器是一个像指针一样的东西
我们知道,string类的底层是这样的一个顺序表。这样就可来进行增删查改
而迭代器就是增加了一种访问的方式,这里要注意,迭代器只是像指针的一样的东西,而不一定是指针,我们可以暂且理解为指针。
我们来分析下面的代码,注意迭代器的使用必须是string::iterator。
这段代码中,begin和end可以指向的的是起始的位置,和末位置的后一个位置,由于末位置是最后一个字符,所以end指向的是'\0'字符
int main() { string s1("hello world"); cout << s1 << endl; string::iterator it = s1.begin(); while (it != s1.end()) { cout << *it << ' '; ++it; } cout << endl; return 0; }
注意的是iterator是一个像指针的类型,有可能是指针,有可能不是指针
如下代码所示,它可以实现类似指针的功能,修改所指向的空间,也就是说迭代器可以读写数据
int main() { string s1("hello world"); cout << s1 << endl; string::iterator it = s1.begin(); while (it != s1.end()) { (*it)--; it++; } it = s1.begin(); while (it != s1.end()) { cout << *it << ' '; ++it; } cout << endl; return 0; }
2.string中迭代器的作用
我们知道,使用operator[]运算符也可以实现读写,使用迭代器也可以实现读写。在string中我们起始更喜欢使用运算符重载来读写,因为比较简洁。那么使用迭代器有什么意义呢?
我们知道,我们有时候在读的时候,也喜欢使用范围for来解决。
for(auto& c : s1) { c++; } for (auto c : s1) { cout << c << ' '; } cout << endl;
范围for实际上底层就是使用迭代器来实现的,底层直接替换为迭代器
所以说实际上并没有什么范围for,它只是一层伪装罢了,没有迭代器就不支持范围for,一个类只要支持迭代器就支持范围for,我们已经知道数组和string都是支持范围for的,而栈是没有迭代器的,所以自然不支持范围for,数组支持迭代器是因为指针就是天然的迭代器。
迭代器的一大好处就是任何容器都支持迭代器,并且用法是类似的
我们可以看下面的程序,虽然我们可能暂时不会写出来,但是还是能读懂的
#include<vector> #include<list> int main() { vector<int> v; v.push_back(10); v.push_back(20); v.push_back(30); v.push_back(40); vector<int>::iterator vit = v.begin(); while (vit != v.end()) { cout << *vit << ' '; vit++; } cout << endl; list<int> lt; lt.push_back(50); lt.push_back(60); lt.push_back(70); lt.push_back(80); list<int>::iterator lilt = lt.begin(); while (lilt != lt.end()) { cout << *lilt << ' '; lilt++; } cout << endl; return 0; }
对于链表和树形结构就无法使用operator[]了。但是迭代器仍然可以使用。
总结:iterator提供了一种统一的方式访问和修改容器的数据
3.迭代器跟容器结合
我们容器中的数据都是私有的。我们无法访问的,但是如果想要实现算法的话,就必须访问数据,为了访问数据就需要使用迭代器了。因为iterator提供了统一的访问方式和修改容器的数据。
我们知道数组的逆置和链表的逆置是不一样的,但是库里面提供了统一的算法接口。
算法的库名称为<algorithm>
我们可以来看一下库里面的reverse函数,可以看到他是需要传入两个迭代器的,这个迭代器的区间为左闭右开,刚好就是我们迭代器的begin和end
我们来应用一下
#include<vector> #include<list> #include<algorithm> int main() { vector<int> v; v.push_back(10); v.push_back(20); v.push_back(30); v.push_back(40); vector<int>::iterator vit = v.begin(); while (vit != v.end()) { cout << *vit << ' '; vit++; } cout << endl; list<int> lt; lt.push_back(50); lt.push_back(60); lt.push_back(70); lt.push_back(80); list<int>::iterator lilt = lt.begin(); while (lilt != lt.end()) { cout << *lilt << ' '; lilt++; } cout << endl; reverse(lt.begin(), lt.end()); reverse(v.begin(), v.end()); cout << "reverse_vector:"; vit = v.begin(); while (vit != v.end()) { cout << *vit << ' '; vit++; } cout << endl; cout << "reverse_list:"; lilt = lt.begin(); while (lilt != lt.end()) { cout << *lilt << ' '; lilt++; } cout << endl; return 0; }
运行结果为
需要注意的是,我们的顺序表和链表虽然看上去用的是同一个函数,但其实并不是同一个函数,它调用的是函数模板。用函数模板自动生成一个函数。
我们在上面提到过,范围for起始就是迭代器。所以我们可以使用范围for来进行遍历一下数据
如果我们需要排序的话,可以直接用sort,而不需要使用qsort了,可以看到sort有两个函数重载,我们现在只考虑第一个函数重载,顺序表可以直接使用第一个进行排序,链表不能用第一个。可以看到,只需要传左闭右开的迭代器就可以进行排序了。
总结:算法可以通过迭代器,去处理容器中的数据
4.反向迭代器
有时候我们还需要反向遍历容器中的数据,这时候就需要我们使用反向迭代器了,反向迭代器的类型是在迭代器前面加上reverse,同样的调用begin和end也变成了rbegin和rend,并且和正向迭代器一样,是左闭右开的
int main() { string s1("hello world"); string::reverse_iterator rit = s1.rbegin(); while (rit != s1.rend()) { cout << *rit << ' '; rit++; } cout << endl; return 0; }
我们有时候会觉得迭代器的类型太过于繁琐,我们会直接使用auto来进行代替
和正向迭代器类似,反向迭代器也可以直接修改容器的数据
5.const修饰的迭代器
当我们写出如下代码的时候,我们发现报错了
这里其实涉及到一个权限放大的问题
我们为了使得传参的代价小一点,不去调用拷贝构造,故而使用了引用,但又因为我们函数内并未涉及到修改s的值,所以我们加上了const修饰,但是这样我们就发现,报错了。这是因为s是const对象,它必须调用const修饰的迭代器
我们查看库里面的,确实又一个const修饰的迭代器
所以我们将迭代器的类型进行修改就可以解决问题了
普通的迭代器可以进行读写,而const修饰的迭代器只能读不能写
在这里为了避免犯错误,我们可以使用auto来代替类型
同样的,反向迭代器遇到const修饰的对象,也需要使用const修饰的迭代器类型
四、String容量相关的成员函数
1.size和length
size是数据个数,length是长度,这两个在string的结果是一致的
这两个的功能是一样的,但是却有两个函数这其实就与string的历史有一些关系,string是在STL产生之前的。当时使用的是length,因为与c语言中的strlen是一致的。但是后来为了在STL中提供统一的接口,就采用了size了。因为对于树形结构等,用lenth这个单词并不合适。而使用size是比较合适的
2.max_size
这个函数可以返回最大的数据个数。
但是要小心,在不同的编译器上,这个函数可能会产生不一样的结果
这是因为STL是一个规范,它有很多版本,比如PJ版本SGI版本。他们的底层实现大同小异
3.capacity
这个函数顾名思义,是计算当前的容量的,同样的,这个函数也要小心,不同的编译器运行结果有可能不同
这里是本应该是16,但是最后一个是\0,故少了一位。容量就是15了
我们可以在vs上分析一下扩容的过程,如下代码所示,可以看到大概是呈1.5倍的速度扩容的
int main() { string s("hello world"); cout << s.max_size() << endl; cout << s.capacity() << endl; size_t old = s.capacity(); for (size_t i = 0; i < 100; i++) { s += 'x'; if (old != s.capacity()) { cout << "扩容" << s.capacity() << endl; old = s.capacity(); } } return 0; }
而在Linux环境下,是呈2倍的速度进行扩容的
4.clear
clear的作用是清理字符串的内容,使其成为空字符串
我们可以注意到,size会被改变,但是capacity不会被改变
5.reserve
这里我们首先需要注意的是:不要与reverse这个单词搞混了
reverse: 这个单词的意思是反转
reserve: 这个单词的意思是保留
reserved的功能是请求一个容量的改变
那么我们应该如何使用呢?在前面使用capacity的时候,我们可以观测到string一个对象的容量的改变,而如果我们对对象使用这个成员函数,那么就可以直接改变容量,这样的好处就在于不需要扩容了
如下代码所示
int main() { string s; s.reserve(100); cout << s.max_size() << endl; cout << s.capacity() << endl; size_t old = s.capacity(); for (size_t i = 0; i < 100; i++) { s += 'x'; if (old != s.capacity()) { cout << "扩容" << s.capacity() << endl; old = s.capacity(); } } cout << s.size() << endl; cout << s.capacity() << endl; s.clear(); cout << s.size() << endl; cout << s.capacity() << endl; return 0; }
我们可以观测到实际上是要比100大一些的
但是在Linux下,开的容量正好是100,在此不做演示了
所以这个函数具体开多大的空间还取决于编译器。但总的来说,来的容量必须比100要大。不能比100少
那么当比原来string对象的容量要小的时候,会发生什么情况呢,根据文档的描述,可能会缩容。
但是在实际中,缩小容量与否取决于编译器
下面是vs2022的结果,并没有缩小容量。
但是需要注意的是,当我们将这个reserve放到clear后面的时候,缩小了。但是并没有完全缩
而在Linux环境下, 确确实实缩小了容量,即要缩到多少,就缩小到多少。但是有个前提是跟clear有关系。
6.resize
对于这个函数他的作用是填值加开空间,他的作用与resever类似,但是比resever要多了一个填值的过程
下面是他们两个的对比
还需要注意的是,他的填值是由自己来决定的,如果我们不带上这个字符,默认填0
还需要注意的是,如果n比我们当前的数据要多,这自然是我们最期望的状态。但倘若n比我们的数据个数要小,这样的话,我们就会删一些数据
也就是说,他是一定会使得size减少,但是容量不会缩容的
7.shrink_to_fit
这个函数的作用是,收缩至合适,即将capacity调整至size
但是这个函数也是不具有约束力的,即不一定会收缩至和size一样,可能会比size大一些