【C++】STL之string类概述-2

简介: 【C++】STL之string类概述

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,这是为何呢?

image.png

  • 这其实是因为string的封装得过多了,因此我们在进行观看的时候需要一直点到最里面才可以,继而发现了我们的\0

image.png

  • 那既然我们可以通过【下标 + [ ]】的形式去访问字符串中的每一个元素,那在访问的同时是否可以进行修改呢?这当然是可以的,马上来试试👇
for (int i = 0; i < s.size(); i++)
{
  s[i]++;
}
  • 打印一下可以看到,每个元素 + 1之后再去遍历打印的时候就有了不同的结果

image.png

  • 不仅如此,我们还可以单独使用,将每个元素++之后我们再把s[0]--,那么在打印的时候看到的结果即为[a]

image.png

从上面的种种我们可以看到这个operator[]使得字符串可以让我们像数组一样去使用,做增、删、查、改很方便

  • 但是呢,上面这种string[]的形式和下面这样对字符数组的访问是有本质区别的
string s("abcdef");
char s2[] = "hello world";
s[1]++;   // -> operator[](1)++
s2[1]++;  // -> *(s2 + 1)++
  • 这一块我们可以通过汇编来进行查看,发现s[1]++在底层是转换为operator[]的形式;但是对于s[2]++却是在做一些解引用的操作,这一块看不懂也没关系,但在学习了C语言操作符后我们要知道对于[]来说其实就是一种解引用的形式

image.png

【温馨提示】:只能用于string + vector + deque,但是链表、树不能用,链表各个结点的空间不连续

  • 对于【vector】和【deque】,我在后续都会讲解到,它们都是STL中的容器,而且在内存中与string一样都是连续的,因此我们可以像访问数组一样去访问里面的元素。但是呢,像【链表】、【树】这样的结果,它们的一个个结点在空间中都是都是离散的,无法做到像数组那样去连续访问

image.png

② at

当然,除了下标 + []的形式,我们还可以通过【at】的形式去做一个访问

  • 一样通过查看文档来观测一下,也是具有两个重载,一个是普通对象,一个则是const对象

image.png

void TestAt()
{
  string str("abcdef");
  for (int i = 0; i < str.size(); i++)
  {
    cout << str.at(i) << " ";
  }
  cout << endl;
}
  • 可以看到可以在边遍历的时候边修改字符串中的值

image.png

我们再来看看这个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具有常性,是无法修改的

image.png

对于oparator[]at()来所,还要再做一个对比,即它们在处理异常这一块

可以看到对于上面的oparator[]来说若是产生了一个越界访问的话就直接报出【断言错误】了

image.png

然后看到对于at()来说虽然也是弹出了警告框,但是呢这个叫做【抛异常】,之后我们在学习C++异常的时候会讲到的

image.png

  • 此时我们应该再去仔细地看一看文档,里面说到在检查出所传入的pos位置有问题时,就会报出out_of_range的异常,这也就印证了上面的现象

image.png

  • 那对于异常而言都是可以去捕获的,那就是采用try...catch的形式。此时我们再运行的话就可以发现此异常被捕获了,而且打印出了异常的信息

image.png

③ 迭代器

那接下去呢,我就要来讲讲【迭代器】了,它是我们在学习STL的过程中很重要的一部分内容,让我们对容器的理解能够更上一层楼

  • 在这之前呢,我们认识两个最常见的接口函数,即为beginend
  • begin获取一个字符的迭代器
  • end获取最后一个字符下一个位置的迭代器

image.png

image.png

  • 迭代器是是另一种访问string中内容的方式,图示如下,我们使用一个it去保存这个字符串【begin】处的位置,那么在其不断进行后移的过程中,就是在遍历这个字符串,当其到达最后的【end】处时,也就遍历完了,此刻便会停了下来

image.png

  • 好,我们一起来看看这段代码
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++;
}
  • 来观察下运行结果就发生也没问题,可以进行修改

image.png

  • 因此我们看迭代器的这种方式,其实和指针非常得类似,不过呢不能完全这样说,所以你可以说  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容器来说,也是可以使用迭代器去做一个遍历的

image.png

  • 不仅如此list也是可以使用迭代器来进行访问的

image.png💬 好,上面仅仅作为拓展,如果读者不懂得话也没关系,下一文就会学习到

【小结】:

好,对上面的内容做一个小结。iterator提供一种统一的方式访问和修改容器

还记得我们在初步认识STL的时候讲到的STL的六大组件,除了【容器】之外最重要的就是【算法】,这里我先简单地介绍几个算法并演示一下

  • 首先就是我们使用到最多的【reverse】函数,字面意思:颠倒元素

image.png

  • 观察其参数我们可以发现,传入两个迭代器即可,那刚好就是我们前面所学的【begin】和【end】
reverse(s.begin(), s.end());
  • 一起来看一下结果就可以发现确实string字符串内的字符都发生了一个翻转,但是有一个头文件#include <algorithm>可不要忘记了哦

image.png

  • 同样,对于vector容器来说也是同样适用

image.png


  • 好,再来说一个【sort】,也很明了,就是对区间内的元素去做一个排序的操作,此时我们可以看到两个重载形式,第一个就是正常传入区间迭代器,而第二个重载形式则是可以传递【仿函数】,它也是STL的六大组件之一,我们在后续也会进行学习,这里先提一句
  • 如果你有看过 C语言回调函数 的话就可以很清晰地看出来是它们很类似,这里不做展开

image.png

  • 立马,我们来看看如何去进行使用,也是传递【begin】和【end】即可
sort(s.begin(), s.end());
  • 通过运行结果我们可以看到再 通过sort进行排序后原本的乱串变成了有序串

image.png💬 其余容器的这里就不演示了,读者可自己下去试试看,总结一下:算法可以通过迭代器去处理容器中的数据

好,讲完正向迭代器,我们再来说说【反向迭代器】

  • 首先我们要来了解一下新的两个接口【rbegin】和【rend】

image.pngimage.png

  • 还是结合具体图示来观察一下,对于【rbegin】来说指向的是最后一个字符的位置,对于【rend】来说它指向的是第一个字符的前一个位置,

image.png

  • 好,我们来看一下具体该如何去使用,其实和 正向遍历 非常相似,只是这个迭代器我们要换一下,通过它们二者的返回值其实就可以看得出出来
reverse_iterator rbegin();
reverse_iterator rend();

展示一下代码

string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
  cout << *rit << " ";
  rit++;
}

再来看看结果

image.png

好,讲完了正向和反向迭代器后,我们再来拓展地讲一些东西

  • 仔细地观察一下这四个接口函数,发现除了 iterator reverse_iterator 之外,还有const_iteratorconst_reverse_iterator,那后面的这两个我们要如何去使用呢?

image.png

  • 我们这里将迭代器遍历封装为函数,采取引用传值减少拷贝构造,那还需要加上const做修饰防止权限放大
void Func(const string& s)
{
  string::iterator it = s.begin();
  while (it != s.end())
  {
    cout << *it << " ";
    it++;
  }
}
  • 但是呢当我在编译之后却发现出了问题,说是无法去进行一个转换

image.png

  • 那我们用用上面看到的两个新的迭代器试试,发现确实不会有问题了,原因就是在于对象s是属于const对象,那么它在调用【begin】的时候返回的就是const迭代器,是【只读】那此时我们若是使用普通迭代去接收的话就是【可读可写】,也算是一个权限放大的问题

image.png

  • 我们再来试试反向迭代器,可以发现也是具有同样的问题

image.png

  • 此时只有将迭代器换成const_reverse_iterator才可以,但你是否觉得这样写过于复杂了呢?
string::const_reverse_iterator rit = s.rbegin();
  • 如果有同学了解C++11的关键字auto的话就可以清楚其可以完成自动类型转换的功能,不需要我们去关心具体的类型,这个关键字我在下面讲到【范围for】的时候还会再提到的,读者可以自行先了解一下
auto rit = s.rbegin();
  • 那么这个迭代器是否真的能做到【只读】呢,我们去修改一下即可,发现确实是呈现一种只读的效果

image.png【总结】:

  • 好,最后来总结一下我们上面所学习的四种迭代器,分别是image.png

④ 范围for

好,我花了很大的篇幅在介绍迭代器之后,我们再来讲讲范围for,这个是C++11才出来的,现在被广泛地使用

  • 很简单,我们马上来看看具体的代码,它就是一种语法糖的,这里的auto就是我们上面所说到过的【自动类型推导】,那这里如果我们不用auto的话直接使用char也是可以的
void TestRangeFor()
{
  string s("abcdef");
  for (auto ch : s)
  {
    cout << ch << " ";
  }
  cout << endl;
}
  • 来讲讲它的原理,通过汇编我们可以看到,范围for的底层实现还是【迭代器】,所以我们可以说在它在遍历的时候相当于是将*it的数据给到当前的ch,和迭代器的本质还是类似的

image.png

  • 那这个范围for既然的底层实现既然都是迭代器的话,是否也可以像迭代器那样在遍历的时候去做一个修改呢?这当然是可以的喽~
  • 但是呢,下面这样就不可以啦,因为这样的话ch在遍历的时候每次只会是当前字符的一份拷贝,那么在循环遍历结束后ch每一次的变化是不会导致字符串s发生变化的

image.png

  • 那我们只需要让ch和字符串每一个字符所属同一块空间即可,那这个时候就使用我们所学习的【引用】即可

image.png【注意事项】:

  • 好,接下去我们几个注意事项

一个类如果不支持迭代器就不支持范围for,因为范围for的底层使用的也是迭代器

  • 不是所有的类都支持迭代器的,例如我们之后要学习的stack类,它就是不支持的

image.png

  • 可以看到,不支持迭代器,也是不支持范围for的

image.png

只能正着遍历,但是不能倒着

  • 不仅如此,范围for也是不支持像迭代器那样倒着遍历的,这个无法演示,读者可以自行思考一下🤔

⑤ front 和 back

然后再来拓展两个C++11中新接口,看这个字面其实就可以看出【front】取到的是字符串的首字符,而【back】取到的则是字符串的尾字符

image.png

void TestBackAndFront()
{
  string str("abcdef");
  cout << str.front() << " " << str.back() << endl;
}
  • 可以看到,确实取到了字符【a】和字符【f】

image.png

相关文章
|
6天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
21 2
|
12天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
40 5
|
19天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
49 4
|
20天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
46 4
|
2月前
|
安全 Java 测试技术
Java零基础-StringBuffer 类详解
【10月更文挑战第9天】Java零基础教学篇,手把手实践教学!
34 2
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
22 1
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
28 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
25 4
|
2月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
2月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)