C++ STL 学习之【string】

简介: STL 是 C++ 的重要组成部分,由六大部分构成:伪函数、空间配置器、算法、容器、迭代器 和 配接器,其中各种各样的 容器 可以很好的辅助我们写程序,比如今天要介绍的 string,有了它之后,我们对字符串的操作就能变得行云流水

✨个人主页: Yohifo

🎉所属专栏: C++修行之路

🎊每篇一句: 图片来源


The key is to keep company only with people who uplift you, whose presence calls forth your best.


关键是只与那些提升你的人在一起,他们的存在唤起了你最好的一面。


0c1a855c7187118d1742fd4f6aeeac7.png


📘前言


STL 是 C++ 的重要组成部分,由六大部分构成:伪函数、空间配置器、算法、容器、迭代器 和 配接器,其中各种各样的 容器 可以很好的辅助我们写程序,比如今天要介绍的 string,有了它之后,我们对字符串的操作就能变得行云流水


1eac670322298a6fc8d834b81815965.png


注意: string 诞生于 STL 之前,因此存在部分接口冗余的情况


📘正文


本文介绍的是 string 部分常用接口


📖basic_string


string 是 basic_string模板 的一份实例,因为字符串多种多样,所以 string 也有各种各样的版本


string 常规字符串类,即每个字符占位 1byte

wstring 宽字符串类,用来处理较长字符串,Winows下占位 2byte,而 Linux下占位 4byte

u16string 匹配 UTF-16 编码标准,指定字符占位 2byte(C++11)

u32string 匹配 UTF-32 编码标准,规定字符占位 4byte(C++11)

世界上有各种各样的语言,其字符长度大多不一样,因此需要使用不同的 string 来匹配输出自己国家的字符

b45319fe0d5ce3f2bf851beee41cea9.png


📖编码理解


我们这里介绍的是 string 类,它匹配 UTF-8 标准,而此标准又兼容了 ASCII 码,因此比较常用


ASCII 是美国信息标准交换代码,仅仅通过 1byte 就能满足其字符需求

ebbd33f93de81b02b927ecf7510d657.png

UTF-8 的特点是能根据不同范围的字符匹配使用不同的标准,因为ASCII 都是 0xxxxxxx 的形式,当识别到其他字符时,会匹配使用对应标准,比如当识别到汉字时,会使用 GBK 编码标准来进行输出(Windows)

f31bd83fd71705d532eaa6c60cbad42.png

后续随着万国码 Unicode 的诞生,提出了能适用更多语言的编码标准,即 UTF-16 和 UTF-32 ,而 basic_string 中的 u16string 和 u32string 这两个类就是用来匹配编码标准的


注: 这两个类是在 C++11 标准中制定的

0764dde96a381f93ef1aafd81ec1dec.png

我们的 string 其实就是 basic_string <char> 的别名



📖构造函数相关


现在正式进入 string 类的学习,先从默认成员函数—构造函数入手


注意: string 包含于 iostream 头文件中,并且还需要展开 std 命名空间


🖋️无参(默认)构造函数


#include<iostream>
using namespace std;
int main()
{
  string s; //此时调用的是无参构造函数
  return 0;
}



调用无参构造函数时,默认将对象初始化为空串,即只包含 '\0' 的字符串


668350e1124340a6efd77f570860b98.png


🖋️带参构造函数


我们也可以指定 string 对象中的内容


int main()
{
  string s("Hello String!");  //指定内容
  //string s = "Hello String!"; //下面这种写法也是完全可以的
  return 0;
}

7d1535f1794eb16f024f6084c980319.png



string 也支持将对象构造为 n 个字符 c


int main()
{
  string s(10, 'w');  //构造10个w字符
  return 0;
}

fe0a26156eab3bec847e297bb75325f.png


最后再来看看 string 类的 拷贝构造 函数


int main()
{
  string s1("Hello");
  string s2(s1);  //将 s1 的内容构造给 s2
  //string s2 = s1; //这种写法也是可以的
  return 0;
}


bc329c7304b76f8c553cb179910d8a5.png


📖容量操作相关


我们可以把 string 类看作一个专门用来处理字符的顺序表,因为它有字符指针、容量、长度等信息,我们也可以进行手动扩容等操作


🖋️获取数据


获取 string 对象中指向字符串的指针 _str


C++兼容C,在某些场景下需要使用指向字符串的指针,因此 string 类中提供了这个接口


int main()
{
  string s("hello");
  cout << s.c_str() << endl;  //获取对象s中的字符串指针
  return 0;
}

5c46b69396cf1afff76dd494d5ffd82.png


此时直接打印内容的原因是当指针指向对象为常量字符串时,编译器会直接打印内容

我们可以通过强转来观察函数 c_str()


cout << (void*)s.c_str() << endl; //此时指针非常量字符指针

6b00c9afa23eea80cb979c636443b85.png


通过函数 capapcity() 和 size() 获取当前对象的容量和大小


int main()
{
  string s(200, 'H'); //直接构造200个字符H
  cout << "The string capacity is " << s.capacity() << endl;
  cout << "The string size is " << s.size() << endl;
  //cout << "The string size is " << s.length() << endl;  //这种方式也能获取大小
  return 0;
}

2006bcaf22ef8e148cbd782d7686f10.png

length() 函数能起到和 size() 函数完全一样的效果, 那为什么会有两个函数呢?


string 诞生于 STL 之前,当时的设计的获取大小函数为 length()

后来当 string 并入 STL 后,委员会为了统一化,就在 string 类中添加了一个 size() 函数,因为其他容器中获取大小的函数都是 size()

为了确保向前兼容性,不能直接删除 length(),这里推荐使用 size()

🖋️扩容空间


new 出来的空间不支持像 realloc 一样直接扩容,而是需要通过函数扩容


realloc 大多数情况下都是异地扩容,即 开辟-拷贝-销毁-更改指向

而 reserve() 函数实现的就是异地扩容

int main()
{
  string s(30, 'H');  //初始化大小为10
  cout << "The default capacity " << s.capacity() << endl;
  s.reserve(300); //扩容为300
  cout << "The expansion capacity " << s.capacity() << endl;
  return 0;
}

9e6c02e3a8435806e4ac98f539d9509.png


VS 中的容量都会稍微多一点


假若我们不手动扩容,string 也会像顺序表一样,识别到容量不够时,自动扩容


VS中 string 的扩容策略


默认给一个大小为 15 的数组存储数据,当数组够用时,都是用的数组

当数组容量不够时,改用指针,先 2倍 扩容至 30,后续字符都是存在指针中

之后的扩容操作,都是以 1.5倍 进行扩容

会多开辟一些空间

Linux中 string 的扩容策略


默认大小为 0 的空间

当第一次扩容时,会先扩至 1

扩容时每次都是 2倍 扩容法,比较清晰

不会多开空间

int main() 
{
  string s;
  int capacity = s.capacity();
  cout << "The default capacity " << capacity << endl;
  int n = 0;
  while (n <= 100)
  {
  //尾插字符
  s += 'a';
  if (capacity != s.capacity())
  {
    capacity = s.capacity();
    cout << "The new capacity " << capacity << endl;
  }
  n++;
  }
  return 0;
}




至于 Windows 中为何如此复杂?首先是 STL 版本不同,其次string 在实际使用中,都用不了太大的空间,因此 VS 就直接索性给了一个默认大小为 15 的数组,后续有需要再进行扩容

895924ef917feb4f0f9f304576d24fb.png

频繁扩容会导致内存碎片问题,VS在这里的处理方法是比较合理的


小技巧: 在使用 string 时,可以先提前计算好需要的空间,然后通过 reserve 直接提前扩好,避免因自动扩容而导致的内存碎片问题


🖋️调整长度


除了可以扩容外,我们还可以改变 size


int main()
{
  string s(50, 'W');  //当前的 size 为50
  cout << "The default size " << s.size() << endl;
  cout << "The default capacity " << s.capacity() << endl;
  cout << endl;
  s.resize(30); //改变 size 为30
  //s.resize(100, 'Z'); //还可以这样写,更改后50块空间为 Z
  cout << "The new size " << s.size() << endl;
  cout << "The new capacity " << s.capacity() << endl;
  return 0;
}

6da97f92556f96f54f3052d97951ccf.png

resize() 有两种情况:


调整后空间比原空间大,此时相当于扩容 reserve(),不过 resize() 还有一个初始化的功能,即将参数2设为指定字符,如果没有指定就默认为 \0

调整后空间比原空间小,此时将 _size 调整至目标空间,而 _capapcity 不变,此时我们也无法访问到 _size 之外的数据

resize() 并不会缩容,因为缩容的代价比较大,需要先开辟新空间,然后拷贝,释放原空间,才能完成缩容,因此 resize() 在处理时,若新空间比原空间小,是不会改变 _capaciy 的


📖遍历字符相关


字符串当然少不了遍历操作,主要有三种遍历方式:下标、at()、迭代器,因为 下标 和 at() 区别不大,所以可以一起介绍,而 迭代器 是一个很重要的东西,后续容器学习中都会出现它的影子


🖋️下标访问

首先来看看 下标访问,实现原理很简单:运算符重载 operator[]


int main()
{
  string s("chatGPT");
  size_t pos = 0; //下标
  while (pos < s.size())
  {
  //直接像数组一样通过下标访问字符
  cout << "The " << pos + 1 << " char is " << s[pos] << endl;
  pos++;
  }
  return 0;
}

509f6f06d956af3bfe7eaaa3a5839ef.png


当我们出现越界行为时,下标访问是直接通过 assert 报错的


下面再来看看 at()


cout << "The " << pos + 1 << " char is " << s.at(pos) << endl;


运行结果与 operator[] 一致,其实这两种方法的实现原理都一样,不过处理问题的方法不一样


当出现越界访问,at() 是抛出异常,而非直接断言报错


总的来说,at() 用的比较少,我们一般都是使用 operator[] 来进行下标的随机访问


🖋️迭代器


下面来看看迭代器 iterator 遍历字符串


int main()
{
  string s("chatGPT");
  //创建迭代器
  string::iterator it = s.begin();  //此时的it相当于指向第一个字符的指针
  //auto it = s.begin();  //可以利用 auto 自动识别类型
  while (it != s.end())
  {
  cout << *it;
  it++;
  }
  cout << endl;
  return 0;
}

d271dfab870052572ad4dafd5f8b2b2.png


注: begin() 获取第一个字符,end() 获取最后一个字符的下一个字符,即 '\0'


除了可以正向遍历外,我们还可以通过反向迭代器 reverse_iterator 进行反向遍历


int main()
{
  string s("chatGPT");
  //创建反向迭代器
  string::reverse_iterator rit = s.rbegin();  //此时的rit相当于指向最后一个字符的指针
  //auto it = s.begin();
  while (rit != s.rend())
  {
  cout << *rit;
  rit++;
  }
  cout << endl;
  return 0;
}


注: rbegin() 获取最后一个字符,rend() 获取第一个字符的前一个字符


迭代遍历区间都是左闭右开


除了上面两种普通迭代器外,还有两个 const 修饰的迭代器,用来遍历常量字符串


const_iterator 正向遍历常量字符串

const_reverse_iterator 反向遍历常量字符串

注意:


迭代器名 const_iterator 中的 const 并非是 const 操作符,而是与普通迭代器构成重载

迭代器不太适合遍历顺序表,适合用来遍历链表

所谓的 范围for 其实就是在调用迭代器进行遍历

📖字符修改相关


现在来谈谈字符修改相关接口


🖋️尾插字符/字符串

尾插字符/字符串有三种方式:


push_back() 尾插字符

append() 尾插字符/字符串

operator+= 尾插字符/字符串

先来看看 push_back()


int main()
{
  string s = "Hello ";
  //尾插字符
  s.push_back('X');
  cout << s << endl;
  return 0;
}

e61b4e5243216754840e6b67c2dd4bb.png


push_back() 就像是顺序表的尾插,一次只能插入一个字符


再来看看 append()


int main()
{
  string s = "Hello ";
  //尾插字符
  s.append(3, 'X'); //需要指定待插入的字符数
  s.append(" YYY"); //或者直接插入字符,都是可以的
  cout << s << endl;
  return 0;
}


int main()
{
  string s = "Hello ";
  //尾插字符
  s += 'C'; //直接和字符拼接
  s += "SDN"; //和字符串拼接也是可以的
  cout << s << endl;
  return 0;
}

append() 还有很多其他用法,感兴趣的可以去查看官方文档

2f3f91a79e70db8b5f0d84bfc0f3db4.png

最后再来看看 operator+= ,这个是使用频率最高的,因为比较方便




在日常使用中,对于字符串尾插这件事,我们通常都是使用 operator+=


🖋️任意位置插入字符/字符串


string 支持在任意位置插入字符/字符串


int main()
{
  string s("cccccc");
  cout << "Begin insert:" << s << endl;
  s.insert(2, 1, 'A');  //在第 n 个位置插入 m 个字符 c
  s.insert(4, "BBB"); //在第 n 个位置插入字符串
  cout << "After insert:" << s << endl;
  return 0;
}

38501d79fe966e1bad9fdb0db9f1f53.png


insert() 的用法同样还有很多,可以自行查看官方文档


🖋️删除字符/字符串


有任意位置插入,当然就有任意位置删除 erase()


int main()
{
  string s("ABCDEFG");
  cout << "Begin erase:" << s << endl;
  //任意位置删除
  s.erase(3, 1);  //从pos3开始,删除第1个字符
  cout << "After erase 1 char:" << s << endl;
  s.erase(2, 4);  //从pos2开始,删除4个字符
  cout << "After erase 4 char:" << s << endl;
  s.erase();  //默认全删
  cout << "After erase all:" << s << endl;
  return 0;
}

039df53318e16be23595b5273f8cd7d.png

注意: erase() 是一个全缺省参数,参数1为 0 ,表示默认从 pos0 开始,参数2为 npos,这是无符号整型中的 -1 ,为无符号整型最大值,意思就是如果不写参数2,默认就全删完了

942b2641a0d298abb9830b9ec73c057.png


来看看 npos

91cd86007f72f62f45e13ac2062e41f.png

它的值是 4294967295,没有字符串长达 42亿 多,因此可以用来当作默认长度值


🖋️查找字符/字符串位置


string 类中提供了查找字符/字符串的函数 find()


int main()
{
  string s("My name is KiKi");
  //查找,返回的是目标字符/字符串第一次出现的下标
  cout << "Find 1 char pos: " << s.find('n') << endl; //找字符,默认从pos0开始
  cout << "Find str pos: " << s.find("KiKi", 5) << endl;  //找字符串,从pos5开始
  cout << "Find not exist str pos: " << s.find("KaKa", 10) << endl; //假设没找到
  return 0;
}


可以看到,当目标不存在时,返回的就是 npos


find() 还有几种形式:


rfind() 从后往前找

find_first_of(str, pos = 0) 从pos位置往后,找 str 中出现的任意字符

find_last_of(str, pos = npos) 从 npos 位置往前,找 str 中出现的任意字符

find_first_not_of() 反向查找

find_last_not_of() 反向查找

string 类的接口雀氏很多


🖋️截取字符串


我们可以截取字符串中的目标字符串 substr()


int main()
{
  string s("I am an iKun, love sing、jump、rap and basketball");
  //利用 find 和 substr 切割出 iKun
  cout << "The target is " << s.substr(s.find('i'), 4) << endl;
  return 0;
}



食不食油饼~

其实 substr() 通常用来截取网址中的域名


📖非成员函数


string类中还有很多定义在类外的非成员函数


🖋️流操作


我们可以直接对 string 对象使用流插入 operator<< 和流提取 operator>>


int main()
{
  string s;
  cin >> s;
  cout << s;
  return 0;
}



🖋️获取字符串


单纯的流插入是无法满足字符串插入需要的,因为字符串中往往都会包含 ' ',而 cin 会认为这是结束标志,进而不再读取字符,因此有专门的函数获取字符串 getline()


#include<string>
int main()
{
  string s;
  getline(cin, s);  //需要包含头文件 string
  cout << s;
  return 0;
}



注意: 需要包含头文件 string


🖋️比较函数


string 类中存在一系列的大小比较函数(18个),光是判断相等就有3个,其实没必要设计这么多函数,这可能也是 string 饱受别人吐槽的原因之一,大佬陈浩也写过相关文章吐槽

5f08fa5acc55ff375b9694895e72f09.png


427ae77665e255cfbd97e83d5c93380.png


原文出处:《STL中的string类怎么啦?》


📖相关试题


简单学完 string 类后,还是有很多试题值得我们去练习的,感兴趣的同学可以点击下面的链接直达题目仓库


string值得练习的题目


📘总结

以上就是本次关于 STL 之 string 的全部讲解了,string 类接口众多,但常用的也就那二三十个,其中大多数函数都有多个版本,如果还想了解更多关于 string 类细节的,可以阅读官方文档


如果你觉得本文写的还不错的话,可以留下一个小小的赞👍,你的支持是我分享的最大动力!


如果本文有不足或错误的地方,随时欢迎指出,我会在第一时间改正


目录
相关文章
|
3月前
|
编译器 C++ 容器
【c++丨STL】基于红黑树模拟实现set和map(附源码)
本文基于红黑树的实现,模拟了STL中的`set`和`map`容器。通过封装同一棵红黑树并进行适配修改,实现了两种容器的功能。主要步骤包括:1) 修改红黑树节点结构以支持不同数据类型;2) 使用仿函数适配键值比较逻辑;3) 实现双向迭代器支持遍历操作;4) 封装`insert`、`find`等接口,并为`map`实现`operator[]`。最终,通过测试代码验证了功能的正确性。此实现减少了代码冗余,展示了模板与仿函数的强大灵活性。
94 2
|
9天前
|
对象存储 C++ 容器
c++的string一键介绍
这篇文章旨在帮助读者回忆如何使用string,并提醒注意事项。它不是一篇详细的功能介绍,而是一篇润色文章。先展示重载函数,如果该函数一笔不可带过,就先展示英文原档(附带翻译),最后展示代码实现与举例可以直接去看英文文档,也可以看本篇文章,但是更建议去看英文原档。那么废话少说直接开始进行挨个介绍。
|
4月前
|
存储 安全 C语言
C++ String揭秘:写高效代码的关键
在C++编程中,字符串操作是不可避免的一部分。从简单的字符串拼接到复杂的文本处理,C++的string类为开发者提供了一种更高效、灵活且安全的方式来管理和操作字符串。本文将从基础操作入手,逐步揭开C++ string类的奥秘,帮助你深入理解其内部机制,并学会如何在实际开发中充分发挥其性能和优势。
|
3月前
|
存储 算法 C++
【c++丨STL】map/multimap的使用
本文详细介绍了STL关联式容器中的`map`和`multimap`的使用方法。`map`基于红黑树实现,内部元素按键自动升序排列,存储键值对,支持通过键访问或修改值;而`multimap`允许存在重复键。文章从构造函数、迭代器、容量接口、元素访问接口、增删操作到其他操作接口全面解析了`map`的功能,并通过实例演示了如何用`map`统计字符串数组中各元素的出现次数。最后对比了`map`与`set`的区别,强调了`map`在处理键值关系时的优势。
198 73
|
3月前
|
存储 算法 C++
【c++丨STL】set/multiset的使用
本文深入解析了STL中的`set`和`multiset`容器,二者均为关联式容器,底层基于红黑树实现。`set`支持唯一性元素存储并自动排序,适用于高效查找场景;`multiset`允许重复元素。两者均具备O(logN)的插入、删除与查找复杂度。文章详细介绍了构造函数、迭代器、容量接口、增删操作(如`insert`、`erase`)、查找统计(如`find`、`count`)及`multiset`特有的区间操作(如`lower_bound`、`upper_bound`、`equal_range`)。最后预告了`map`容器的学习,其作为键值对存储的关联式容器,同样基于红黑树,具有高效操作特性。
130 3
|
4月前
|
存储 缓存 C++
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
C++ 标准模板库(STL)提供了一组功能强大的容器类,用于存储和操作数据集合。不同的容器具有独特的特性和应用场景,因此选择合适的容器对于程序的性能和代码的可读性至关重要。对于刚接触 C++ 的开发者来说,了解这些容器的基础知识以及它们的特点是迈向高效编程的重要一步。本文将详细介绍 C++ 常用的容器,包括序列容器(`std::vector`、`std::array`、`std::list`、`std::deque`)、关联容器(`std::set`、`std::map`)和无序容器(`std::unordered_set`、`std::unordered_map`),全面解析它们的特点、用法
C++ 容器全面剖析:掌握 STL 的奥秘,从入门到高效编程
|
4月前
|
C++
模拟实现c++中的string
模拟实现c++中的string
|
4月前
|
存储 算法 C++
【c++丨STL】priority_queue(优先级队列)的使用与模拟实现
本文介绍了STL中的容器适配器`priority_queue`(优先级队列)。`priority_queue`根据严格的弱排序标准设计,确保其第一个元素始终是最大元素。它底层使用堆结构实现,支持大堆和小堆,默认为大堆。常用操作包括构造函数、`empty`、`size`、`top`、`push`、`pop`和`swap`等。我们还模拟实现了`priority_queue`,通过仿函数控制堆的类型,并调用封装容器的接口实现功能。最后,感谢大家的支持与关注。
175 1
|
5月前
|
C++ 容器
【c++丨STL】stack和queue的使用及模拟实现
本文介绍了STL中的两个重要容器适配器:栈(stack)和队列(queue)。容器适配器是在已有容器基础上添加新特性或功能的结构,如栈基于顺序表或链表限制操作实现。文章详细讲解了stack和queue的主要成员函数(empty、size、top/front/back、push/pop、swap),并提供了使用示例和模拟实现代码。通过这些内容,读者可以更好地理解这两种数据结构的工作原理及其实现方法。最后,作者鼓励读者点赞支持。 总结:本文深入浅出地讲解了STL中stack和queue的使用方法及其模拟实现,帮助读者掌握这两种容器适配器的特性和应用场景。
124 21
|
5月前
|
C++ 开发者
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
95 16