【C++从0到王者】第九站:String基本介绍及使用(中)

简介: 【C++从0到王者】第九站:String基本介绍及使用

三、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大一些

相关文章
|
14天前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
16天前
|
C++
模拟实现c++中的string
模拟实现c++中的string
|
4月前
|
C语言 C++ 容器
【c++丨STL】string模拟实现(附源码)
本文详细介绍了如何模拟实现C++ STL中的`string`类,包括其构造函数、拷贝构造、赋值重载、析构函数等基本功能,以及字符串的插入、删除、查找、比较等操作。文章还展示了如何实现输入输出流操作符,使自定义的`string`类能够方便地与`cin`和`cout`配合使用。通过这些实现,读者不仅能加深对`string`类的理解,还能提升对C++编程技巧的掌握。
167 5
|
4月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
112 2
|
5月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
48 1
|
5月前
|
C++ 容器
|
5月前
|
C语言 C++
深度剖析C++string(中)
深度剖析C++string(中)
76 0
|
5月前
|
存储 编译器 程序员
深度剖析C++string(上篇)(2)
深度剖析C++string(上篇)(2)
61 0
|
5月前
|
存储 Linux C语言
深度剖析C++string(上篇)(1)
深度剖析C++string(上篇)(1)
45 0
|
5月前
|
C++