目录
一、前言
二、什么是 string ?
💦 string 类的基本概念
💦 string 类与 char * 的区别
💦 string 类的作用
💦 总结
三、string 的常用接口详解
💦string 类对象的默认成员函数
① 构造函数(初始化)
② 赋值重载(初始化)
💦string 类对象的访问及遍历操作
① operator[ ] (⭐)
② at
③ 迭代器 (⭐)
④ 范围for
💦string 类对象的常见容量操作
① size、length、capacity
② clear
③ empty
④ reserve (⭐)
⑤ resize (⭐)
⑥ 【reserve】和【resize】在使用中的易错点 (⭐)
💦string类对象的修改操作
① push_back
② append
③ operator+=(string) (⭐)
④ insert (⭐)
⑤ assign
⑥ erase (⭐)
⑦ replace
⑧ pop_back
⑨ swap
💦string类对象的其他字符串操作
① c_str
② substr (⭐)
③ find (⭐)
例题训练:分割url
④ rfind
⑤ find_first_of
⑥ find_last_of
⑦ find_first_not_of
⑧ find_last_not_of
💦string类对象的非成员函数重载
① operator+ ()
② relational operators
③ operator>>()
④ operator<<()
⑤ getline (⭐)
四、共勉
一、前言
最近在刷 leetcode 的时候,发现 string 都还没弄明白吗,但是 STL 的强大是众所周知滴,早晚都是要解决滴,因此专门写下这篇文章,以供自己复习和各位老铁使用,快速的回忆 string 的用法,让你找回自信,不用再竞赛的时候颜面尽失。
本次博客主要讲解 string 的常用接口,由于篇幅过长,string 的常考面试题,下一篇博客来阐述,请大家持续关注我O!!
二、什么是 string ?
在 C语言 中,字符串是以
\0
结尾的一些字符的集合,为了操作方便,C标准库中提供了一些str
系列的库函数,但是这些库函数与字符串是分离开的,不太符合OOP
(面向对象)的思想,而且底层空间需要用户自己管理,稍不留神可能还会越界访问。
于是 C++ 中就引入了String
类,它可以看做是一个管理字符串的数据结构。
💦 string 类的基本概念
string 就是字符串的意思,是 C++用来代替 char 数组 的数据结构。里面封装了一些常用的方法,方便我们地对其进行一些操作,而且string的空间大小是动态变化的,大大减小了不必要的花销
💦 string 类与 char * 的区别
C++ 中,
std::string
是用于处理字符串的标准库类。它提供了一系列成员函数和操作符,使得字符串的操作更加方便和灵活。
string与char*的区别:
-
char*
是一个指针 -
string
本质上是一个类,类的内部封装了char*
,即string
是一个char*
型的容器 -
string
管理char*
所分配的内存,不用担心复制越界和取值越界等
💦 string 类的作用
string类是一个用于处理字符串的类,它可以存储任意数目的字符。它可以用来表示文本或其他字符串数据,例如:文本文件内容、网络传输的数据、数据库中的文本列、用户输入或输出等等。
string类的作用有以下几个方面:
- 存储和处理文本数据:string类可以存储和操作任意数目的字符,可以处理文本数据,如搜索、替换、截取子串等操作。
- 方便字符串操作:使用string类可以方便地进行字符串操作,如连接(concatenate)两个字符串、删除(erase)字符串中的一些字符、复制(copy)字符串等。
- 兼容性:string类是C++标准库的一部分,可用于在不同的计算机和操作系统之间进行可靠的代码交换。
💦 总结
总结:
- string 是表示字符串的字符串类。
- 该类的接口与常规容器的接口基本相同,再添加了一些专门用来操作 string 的常规操作。
- string 在底层实际是:basic_string 模板类的别名。
- 实现为:typedef basic_string<char, char_traits, allocator> string;
- 不能操作多字节或者变长字符的序列。
⚠ 在使用string类时,必须包含#include
头文件以及using namespace std
三、string 的常用接口详解
对 string类 大体有个了解后我们就要去学着使用这个类里面的一些函数了
💦string 类对象的默认成员函数
函数名称 | 功能说明 |
constructor | 构造函数 |
destructor | 析构函数 |
operator= | 赋值重载 |
① 构造函数(初始化)
- 点进第一个成员函数
Constructor
就可以看到存在7个重载,其中我框出来的三个是重点,要着重记忆,其余的了解一下即可
- 我将上面的函数整理了一下,方便观看
string(); // 构造一个空字符串
string (const char* s); // 用C-string来构造string类对象
string (const char* s, size_t n); // 用C-string的前n个字符来构造string类对象
string (size_t n, char c); // 生成n个c字符的字符串
string (const string& str); // 利用原先的字符串做拷贝构造
// 拷贝str字符串中从pos位置开始的len个字符
string (const string& str, size_t pos, size_t len = npos);
- 然后我们到VS里演示一下
- 前面的几个都好理解,来讲一下最后的这一个,可以看到在函数形参这一块给出了一个缺省值叫做
npos
,可能有的同学不清楚这个npos
是什么东西
// 拷贝str字符串中从pos位置开始的len个字符
string (const string& str, size_t pos, size_t len = npos);
- 这个我们也可以到文档中来查看一下,发现它是【无符号整数的最大值】
- 那这个值究竟是多少呢,我们可以将其打印出来看看
💬 我们发现这是一个非常大的值,但是呢,无论这个数值是多少,其实影响都不大,因为我要说明的是即便我们不给出这个参数的话,编译器默认就是从当前的这个【pos】位置开始一直到字符串末尾
⚡ 继续通过文档来观察一下,substring
指的就是子串的意思,意思即为 从pos位置的len个长度去拷贝字符串的一部分(如果str字符串太短或者len为npos则直接到达字符串的末尾)
那现在的话你应该可以明白最后的这个参数我为何没有传递却拷贝到了后面的所有字符
② 赋值重载(初始化)
string& operator= (const string& str); // 将一个string对象赋值给到另一个
string& operator= (const char* s); // 将一个字符串赋值给到string对象
string& operator= (char c); // 将一个字符赋值给到string对象
- 同样,我们到VS中来做一个测试,可以看到这三种方式都可以构成一个 string 的对象,不过照这么来看后面的这两种似乎没多大用处,用的最多的还是第一种
💦string 类对象的访问及遍历操作
接口名称 | 接口说明 |
operator[ ] (⭐) | 返回pos位置的字符,const string类对象调用 |
at ( ) | 返回pos位置的字符,const string类对象调用 |
迭代器 (⭐) | begin() + end() 或者 rbegin() + rend() |
范围for | C++11支持更简洁的范围for的新遍历方式 |
注意: ⭐表示重点掌握
① operator[ ] (⭐)
首先我们来说说这个
operator[]
,相信学习过 运算符重载 的同学一定不陌生
char& operator[] (size_t pos);
const char& operator[] (size_t pos) const;
- 之前我们都是像下面这样去访问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[]
使得字符串可以让我们像数组一样去使用,做增、删、查、改很方便
② 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
的形式。此时我们再运行的话就可以发现此异常被捕获了,而且打印出了异常的信息
③ 迭代器 (⭐)
接口名称 | 使用说明 |
begin() | 返回指向第一个元素的迭代器 |
end() | 返回指向最后一个元素的下一个位置的迭代器 |
rbegin() | 返回指向最后一个元素的反向迭代器 |
rend() | 返回指向第一个元素的前一个位置的反向迭代器 |
那接下去呢,我就要来讲讲【迭代器】了,它是我们在学习STL的过程中很重要的一部分内容,让我们对容器的理解能够更上一层楼,如果想要了解更多可以来看这篇文章:迭代器
- 迭代器是是另一种访问string中内容的方式,图示如下,我们使用一个
it
去保存这个字符串【begin】处的位置,那么在其不断进行后移的过程中,就是在遍历这个字符串,当其到达最后的【end】处时,也就遍历完了,此刻便会停了下来
- 好,我们一起来看看这段代码
void TestIterator()
{
string s("abcdef");
string::iterator it = s.begin();
while (it != s.end())
{
cout << *it << " ";
it++;
}
cout << endl;
}
- 因此我们看迭代器的这种方式,其实和指针非常得类似,不过呢不能完全这样说,所以你可以说
iterator
是像指针一样的类型,有可能是指针,有可能不是指针
好,讲完正向迭代器,我们再来说说【反向迭代器】
- 首先我们要来了解一下新的两个接口【rbegin】和【rend】
- 还是结合具体图示来观察一下,对于【rbegin】来说指向的是最后一个字符的位置,对于【rend】来说它指向的是第一个字符的前一个位置,
- 好,我们来看一下具体该如何去使用,其实和 正向遍历 非常相似,只是这个迭代器我们要换一下,通过它们二者的返回值其实就可以看得出出来
reverse_iterator rbegin();
reverse_iterator rend();
- 展示一下代码
string::reverse_iterator rit = s.rbegin();
while (rit != s.rend())
{
cout << *rit << " ";
rit++;
}
- 再来看看结果
还记得我们在初步认识STL的时候讲到的STL的六大组件,除了【容器】之外最重要的就是【算法】,这里我先简单地介绍几个算法并演示一下
- 首先就是我们使用到最多的【reverse】函数,字面意思:颠倒元素
- 观察其参数我们可以发现,传入两个迭代器即可,那刚好就是我们前面所学的【begin】和【end】
reverse(s.begin(), s.end());
- 一起来看一下结果就可以发现确实string字符串内的字符都发生了一个翻转,但是有一个头文件
#include <algorithm>
可不要忘记了哦
好,再来说一个【sort】,也很明了,就是对区间内的元素去做一个排序的操作,此时我们可以看到两个重载形式,第一个就是正常传入区间迭代器,而第二个重载形式则是可以传递【仿函数】,它也是STL的六大组件之一,我们在后续也会进行学习,这里先提一句
- 立马,我们来看看如何去进行使用,也是传递【begin】和【end】即可
sort(s.begin(), s.end());
- 通过运行结果我们可以看到再 通过
sort
进行排序后原本的乱串变成了有序串
💬 其余容器的这里就不演示了,读者可自己下去试试看,总结一下:算法可以通过迭代器去处理容器中的数据
④ 范围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也是不支持像迭代器那样倒着遍历的,这个无法演示,读者可以自行思考一下🤔
💦string 类对象的常见容量操作
函数名称 | 功能说明 |
size | 返回字符串有效字符长度 |
length | 返回字符串有效字符长度 |
capacity | 返回空间总大小 |
clear | 清空有效字符 |
empty | 检测字符串是否为空串,是返回true,否则返回false |
reverse(⭐) | 为字符串预留空间 |
resize(⭐) | 将有效字符串的个数改成 n 个,多出的空间用字符c填充 |
注意: ⭐表示重点掌握
① size、length、capacity
- 首先要来讲的是【size】和【capacity】,这其实就和我们在顺序表中所介绍的意思差不多,前者表示当前字符串已经存放了多少数据,后者表示当前这个字符串可容纳的空间数
- 我们到VS下通过调试来观察一下,可以看到现在这个
str
没有任何的数据,而VS为我们开出了大小默认为15的空间,其实这里应该是16,只不过最后的\0
也占了一个大小
- 然后去构建出一个具体的字符串来进行观察,发现
size
的值发生了一个变化。不仅如此,我还打印了一下这个字符串的length
属性,观察到其和size
是一样的
- 然后我们点进到这两个函数的定义中可以发现它们的定义竟然是一样的
- 我们在文档的时候,一定会有一种很奇怪的感觉,在C++的容器里面为什么没有
string
呢?这里要追溯到STL的一个诞生历史,其实对于string
这个类,是不属于STL的,因为它是在STL之前就已经存在了的,属于C++标准库里的内容
- 像下面这样来看的话,STL是属于标准库中的一部分,string也是属于标准库的一部分,但是呢二者却没有什么直接的关联。在最早期string的长度接口是定义为【length】的,到后面STL出了之后才改为了【size】,但是这一块我们需要去考虑到一个历史追溯的问题,所以不能将这个接口给删除,因而我们才看到一模一样的两个接口实现
💬 后续我们在使用到时候直接用【size】即可,因为其他STL库中的接口使用的也是【size】
追究完这个【size】之后,我们再来谈谈这个【capacity】
- 我们使用的是下面这段代码进行测试
void TestCapacity()
{
string s;
size_t sz = s.capacity();
cout << "making s grow:\n";
for (int i = 0; i < 100; ++i)
{
s.push_back('c');
if (sz != s.capacity())
{
sz = s.capacity();
cout << "capacity changed: " << sz << '\n';
}
}
}
- 以下分别是在Windows下的运行结果,通过不断往这个
s
中添加字符来扩充他的容量
💬 可以看到Windows下的VS中,扩容的大小是呈现一个1.5倍大小的趋势
② clear
接下去呢,我们来讲【clear】
void TestClear()
{
string s("hello");
cout << "size: " << s.size() << endl;
cout << "capacity: " << s.capacity() << endl;
s.clear();
cout << "size: " << s.size() << endl;
cout << "capacity: " << s.capacity() << endl;
}
- 对于clear来说,它只会清空字符串中的【size】,但对于【capacity】来说是不会有什么变化的,如果你是这个接口的实现者一定不会去清空这个【capacity】容量的大小,万一后面又需要插入字符呢?此时又需要去进行一个扩容,是非常麻烦的
③ empty
- 接下去再来看看【empty】这个接口,很简单,我们复用一下上面这段代码,看一下这个字符串在清空前后的【empty】结果即可,于是就可以看到没被清空的时候返回
0
,代表false
不空,而被清空之后返回1
,代表true
为空
④ reserve (⭐)
接下去呢我们来讲讲这个【reserve】,可以提前为一个字符串开出指定的空间
- 这一块我们可以放在之前观察【capacity】的地方进行测试,可以发现在开出预留空间后就不会去执行下面的这一段扩容逻辑了,因为此时空间已经够了
所以我们可以得出结论:【reserve】,它的主要功能是 开空间,避免频繁扩容
⑤ resize (⭐)
讲完【reserve】后,我们再来讲讲【resize】,它们很类似,但也有不同之处
void resize (size_t n);
void resize (size_t n, char c); // 初始化数据为n个c字符
void TestResize()
{
string s("abcdef");
// 开空间
s.reserve(100);
cout << s.size() << endl;
cout << s.capacity() << endl;
cout << "---------------------" << endl;
// 开空间 + 填值初始化
s.resize(200);
cout << s.size() << endl;
cout << s.capacity() << endl;
}
- 通过对它们两个做一个对比可以发现对于【reserve】而言仅仅只是对
capacity
起作用,size
就是初始化的字符串个数;
- 但是对于【resize】而言
size
和capacity
会一起发生改变,前者变为传递的参数n,后者则遵循VS下的扩容规则,比给出的参数多一些
一样,我们通过解读文档的形式来看看这个函数还有哪些内容
- 如果 n > 当前的字符串长度,则在末尾插入所需数量的字符以达到n的大小来扩展当前内容
- 如果n < 当前的字符串长度,则会去进行一个删除的操作,删除第n个字符之后的字符
第一点我们刚才已经验证过了,接下去我们再来试试第二点,看看是否真会去做一个删除的操作
- 可以看到,若是传递了 < n的参数进去的话,再去打印这个字符串就会发生一个截断的现象
上面我们所使用的都是第一个只传递n,不过下面还有一个重载形式可以【初始化数据为n个c字符】,看到最后这里也有说到如果这个
c
是特殊的话,就会去以这个c
字符去填充后面多开空间的部分
- 可以看到若是我们没有去制定后面这个字符
c
的话,默认会以'\0'
进行填充的,那如果我们制定出字符呢?
- 此时我指定了后面的字符
c
,此刻后面填充的就是我们所指定的那个字符
s.resize(100, 'a');
⑥ 【reserve】和【resize】在使用中的易错点 (⭐)
- 接下去请读者观察一下下面这段代码,然后看看其中有什么问题?
void teststring()
{
string s;
// 开辟 10个capacity的空间 ,但是size的有效数字为 0
s.reserve(10);
for (size_t i = 0; i < 10; i++)
{
s[i] = 'a';
}
for (auto e : s)
{
cout << e << endl;
}
}
int main()
{
teststring();
return 0;
}
- 然后我将程序运行起来,发现报出了错误❌
💬 有同学说:感觉这代码也没什么错呀?怎么会有错误呢?
- 大家要关注前面的
reserve(10)
,我们在上面说到对于【reserve】而言只是做的扩容而已,即只变化capacity
,而不会变化size
- 另一点,对于s
[i]
我们上面在讲元素访问的时候有说到过,这是下标 + []
的访问形式,在出现问题的时候会直接给出断言错误。因为这里我们在【reserve】的时候只是开出了指定的空间,但size
还是为0,此时去访问的时候肯定就出错了
正确的改进方法应该是像下面这样的:
- 如果我们要使用
下标 + []
的形式去访问元素的话,就需要开出合适的size
大小,才能在访问的时候不会造成越界问题
💦string类对象的修改操作
接下去呢我们来讲讲string类对象的修改操作
函数名称 | 功能说明 |
push_back | 在字符串后尾插字符c |
append | 在字符串后面追加一个字符串 |
operator+=()(⭐) | 在字符产后面追加字符串str |
insert(⭐) | 在指定位置插入字符或字符串 |
assign | 使用指定的字符串替换原字符串 |
erase (⭐) | 删除字符串中的一部分 |
replace | 替换指定区间的字符串 |
pop_back | 删除字符串的最后一个字符串 |
swap | 收缩到合适大小 |
注意: ⭐表示重点掌握
① push_back
- 很简单,就是往当前的字符串后面追加一个字符
- 但是要注意,
push_back()
仅能尾插一个字符,其他都是不可以的
② append
接下去呢我们再来讲讲【append】这个接口,它在string类中用的还是蛮多的
- 通过查看文档可以看到其重载的函数还是比较多的
string& append (const string& str); // 追加一个string对象
// 追加一个string对象中的指定字符串长度
string& append (const string& str, size_t subpos, size_t sublen);
string& append (const char* s); // 追加一个字符串
string& append (const char* s, size_t n); // 追加字符串中的前n个字符串
string& append (size_t n, char c); // 追加n个字符
string s2("bbbbb");
s1.append(s2);
cout << s1 << endl;
s1.append(" ");
s1.append("ccccc");
cout << s1 << endl;
s1.append(" ");
s1.append("hello", 3);
cout << s1 << endl;
s1.append(" ");
s1.append(10, 'e');
cout << s1 << endl;
以下是测试结果,读者可以自行对照
③ operator+=(string) (⭐)
对于上面的这两种调用函数的方式,你是否觉得过于麻烦呢?
- 接下去我介绍一种更加简便的字符串拼接操作,那就是
+=
,这个我们在讲 运算符重载 的时候有提到过。它一共有三个重载形式,分别是拼接一个string
类的对象、一个字符串、一个字符
分别来演示一下
- 首先是两个string对象的拼接
name1 += name2;
- 然后呢是拼接一个字符串
name1 += "xing";
- 最后呢则是拼接一个字符
name1 += 'g';
💬 可以看出这个
+=
确实是非常地方便,有了它你完全就懒得去用另外的【push_back】、【append】,当然它没有这二者的重载形式这么多,还是要以具体的情景为主
④ insert (⭐)
然后呢我们再来看看【insert】这个函数,重载形式也蛮多的
// 在指定位置插入一个string对象
string& insert (size_t pos, const string& str);
// 在指定位置插入一个string对象里的一部分
string& insert (size_t pos, const string& str, size_t subpos, size_t sublen);
// 在指定位置插入一个字符串
string& insert (size_t pos, const char* s);
// 在指定位置插入一个字符串的前n个字符
string& insert (size_t pos, const char* s, size_t n);
// 在指定位置插入n个字符
string& insert (size_t pos, size_t n, char c);
// 在指定迭代器的位置插入n个字符
void insert (iterator p, size_t n, char c);
// 在指定迭代器的位置插入一个字符,并且返回一个迭代器的位置
iterator insert (iterator p, char c);
- 首先是第一个,在
s
的第0个位置插入了一个string的对象
- 然后是第二个,比较复杂一些。下面代表的是我们在当前字符串s的第6个位置插入字符串s3从第0个位置开始长度的6个字符
- 接下去是第三个,我们在string对象s的第三个位置处插入一个字符串
“bbb”
,运行起来就看到确实插进去了
- 那我们也可以指定插入一个字符串中的前n个字符
- 然后的话是在第5个位置插入2个字符
d
- 学习过迭代器后再来看下面这个应该是没什么问题了,就是在起始位置插入指定的字符个数
- 接下去最后一个呢,则是在指定迭代器的位置插入一个字符,然后范围该位置的迭代器。看到我从这个地方开始向后遍历,打印了一下这个string对象
如果读者有看过 C 语言版的数据结构 的话就可以知道对于上面这些操作来说其底层实现都是需要挪动很多数据的,此时就会造成复杂度的提升,导致算法本身的效率下降。因此【insert】这个接口还是不推荐大家频繁使用
💬 通过上面一步步地演示,相信你对接口函数的重载形式如何去辨析一定有了一个自己的认知与了解,后面就不会讲这么详细了,读者可自己去试着测试看看各个重载示例
⑤ assign
讲完【insert】,我们再来瞧瞧【assign】,这个函数读者当做了解,不常用
- 它的功能就是起到一个 赋值 的效果,读者可了解一下文档
- 可以看到无论这个string对象
s
中有多少内容,在执行了【assign】之后就被覆盖成了新的内容。这里的话就演示一下这个了,其余的读者有兴趣可以自己去看看
⑥ erase (⭐)
接下去就是【erase】这个接口,用得还是比较多的
- 首先第一个,其效果就是删除子序列,这个
npos
我们前面在介绍string类的构造函数时有讲到过,这里就不再做介绍了
string& erase (size_t pos = 0, size_t len = npos);
- 很简单,我们来演示一下,比如说我们从下标为1的地方开始往后删,因为第二个参数没有给出具体的值,所以使用的是缺省值
npos
,直接删到结尾
- 不过【erase】用的更多的是头删,例如从第0个位置开始删,删一个
- 但是呢,我们可以这样去删,即传入这个首部迭代器的位置
- 当然,我们传入一个区间的迭代器也是可以的,例如这里传入了【begin】和【end】的位置就把整个字符串给删干净了
不过呢,这里还是要提一句,【erase】这个接口和【insert】一样,在修改原串的时候会造成大量的数据挪动,特别是在头删除的时候,需要挪动[n - 1]
个数据
⑦ replace
接下去这个接口,会让你眼前一惊,因为有非常多的重载类型
- 不过这一块地话读者也不用担心,我们去记一下常用的就行,其余的要用了再查文档
- 简单地来演示一下,例如这里我们要从第2个位置开始替换,一共替换2个字符,将其替换成
“ haha ”
,以下就是替换后的结果
- 再来看看下一个,我们从首部迭代器的后一个位置开始,到尾部迭代器的前一个位置结束,将这些字符替换成
“ eeee ”
💬 好,这里就简单演示两个,有兴趣的同学可以下去自己再看看
⑧ pop_back
有【push_back】,那就一定有【pop_back】,不过这是C++11新出来的
- 很简单,就是尾删一个字符
⑨ swap
接下去我们来看看【swap】这个接口。没错,它可以交换两个字符串
- 不过这一块的底层涉及到string对象的 深浅拷贝 问题,读者先了解一下
- 通过运行结果我们可以看到两个字符串确实发生了交换
💦string类对象的其他字符串操作
函数名称 | 功能说明 |
c_str | 返回 C语言格式字符串 |
substr(⭐) | 在str中从pos位置开始往,截取n个字符,然后将其返回 |
find(⭐) | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
find_frist_of | 从前往后找第一个匹配的字符 |
find_last_of | 从后往前找第一个匹配的字符 |
find_first_not_of | 从前往后找第一个不匹配的字符 |
find_last_not_of | 从后往前找第一个不匹配的字符 |
注意: ⭐表示重点掌握
① c_str
string】类给我们提供了一个接口函数叫做
c_str
,帮助我们将 string对象 转换为 字符串
- 转换的形式就是下面这样。可以看出这个接口真的打通了C和C++之间的一堵墙,很好地起到了一个连通的效果
② substr (⭐)
接下去是一个截取子串的接口
- 很好理解,就是从这个原本的string中截取出一部分的内容
- 当然如果不给长度的话默认使用的便是缺省值
③ find (⭐)
然后我们来看看【find】接口,这个接口用的还是比较广泛的,值得学习一下
- 它的功能是在当前的 string对象 中查找第一次出现的【指定对象】或者【字符串 / 字符】
- 立马我们就来试一下,在string对象
str
中寻找【def】,返回的位置便是第一次所查找到的位
- 当然也可以直接传入一个字符串
例题训练:分割url
上面呢我们简单介绍了接口函数【substr】和【find】,现在立马通过具体的情景来使用一下它们
- 以下这个就是我们要分割的字符串,网站就是我们本文所讲的string类。现在呢我们要将其分割为三部分:一个是协议部分
https
,第二个是域名部分legacy.cplusplus.com/reference
,第三个则是资源部分string/string/?kw=string
https://legacy.cplusplus.com/reference/string/string/?kw=string
然后我们就尝试去分割一下这三部分,下面是整体的分割图示
string str("https://legacy.cplusplus.com/reference/string/string/?kw=string");
- 首先是对 协议 的一个分割,即我们要取到前面的
https
,那么就要找到://
,那么此刻就可以使用到我们前面所学习过的find()
函数,去记录下这个位置。 - 接下去我们就要去取出从头部到这个位置的子串了,很明显就是使用
substr()
,起始位置传入0即可,长度的话传入pos1
,在讲解 数据结构--顺序表 的【size】时有说到过 当前位置的下标就是前面的数据个数
// 协议
string protocol;
size_t pos1 = str.find("://");
if (pos1 != string::npos)
{
protocol = str.substr(0, pos1);
}
- 接下去的 域名和资源名 我们可以一同去获取,首先需要调用的还是
find()
函数,先要确定的就是开始的位置,即这个【legacy】的l
,其距离上一次的pos1
刚好是后移3个的位置,所以我们从pos1 + 3
开始即可,那么要到达的位置就是/
,作为域名的第一次分割线。 - 接下去要确定的就是要取出的子串是那一部分,长度即为 尾部的pos2 - (pos1 + 3)
- 那么对于最后的【资源名】就很简单了,直接从
pos2
这个位置开始取,长度的话直接缺省即可,取到最后面,完全不需要考虑它的长度是多少 - 以下是代码
// 域名 资源名
string domain;
string uri;
size_t pos2 = str.find("/", pos1 + 3);
if (pos2 != string::npos)
{
domain = str.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = str.substr(pos2);
}
最后来看下运行结果,就发现每一块都取出来了
- 当然,这样分割不仅仅是针对上面的这个网址,我们找一个百度的主页地址来构造string类的对象,再去运行可以发现依旧是没问题可以去做一个截取
string str("https://www.baidu.com/index.htm");
void Test()
{
string str("https://www.baidu.com/index.htm");
// 协议
string protocol;
size_t pos1 = str.find("://");
if (pos1 != string::npos)
{
protocol = str.substr(0, pos1);
}
cout << protocol << endl;
string uri;
string domain;
size_t pos2 = str.find("/", pos1 + 3);
if (pos2 != string::npos)
{
domain = str.substr(pos1 + 3, pos2 - (pos1 + 3));
uri = str.substr(pos2);
}
cout << domain << endl;
cout << uri << endl;
}
④ rfind
讲完了【find】,我们再来看看【rfind】
- 很明显,对于【find】的来说是从前往后寻找第一次出现的位置;但对于【rfind】来说呢则是从后往前寻找第一次出现的位置,那即为其最后一次出现的位置
- 简单测试一下,看到这个字符
a
最后一次出现的位置就是在下标为4的地方
- 其他重载形式大家可以自己去测试一下,这里就不做一一展示了,我们来看一下没找到的情况,可以看到返回了一个很大的值,如果你记性好的话一定知道这个是
npos
的值
- 我们可以再来看看官方的文档,可以看到如果出现了不匹配的情况的话,函数就会返回
npos
的值
接下去再来介绍四组接口,它们很类似
⑤ find_first_of
- 首先第一个是在当前的string对象中寻找匹配的任何字符,不过呢在知晓了其功能后你一定会感到这个接口的名字是不是取得不太对,应该叫
find_any_of
才对,不过呢可能是祖师爷在设计的时候突然走神了也说不定🤣
- 立马来看看案例
void TestFindFirstOf()
{
string str("Please, replace the vowels in this sentence by asterisks.");
// 从前往后查找"aeiou"中任意一个字符(即'a'、'e'、'i'、'o'、'u')第一次出现的位
size_t found = str.find_first_of("aeiou");
while (found != string::npos)
{
str[found] = '*';
found = str.find_first_of("aeiou", found + 1);
}
cout << str << endl;
}
- 结合运行结果和代码我们可以看到原串中包含
aeiou
五个元音字母的字符都会替换成了[*]
。如果你有了解过 strtok() 的话就可以知道上面的代码逻辑和它的实现是存在着异曲同工之妙的
⑥ find_last_of
看完【find_first_of】,我们再来看看【find_last_of】
- 它的功能刚好和【find_first_of】和相反的,是从后一个字符开始查找
void TestFindLastOf()
{
string str("Please, replace the vowels in this sentence by asterisks.");
size_t found = str.find_last_of("aeiou");
while (found != string::npos)
{
str[found] = '*';
found = str.find_last_of("aeiou", found - 1);
}
cout << str << endl;
}
- 稍微改改代码,运行起来我们就可以看到,也是可以起到同样的效果
下面还有两个接口,和上面两个刚好是对立面
⑦ find_first_not_of
- 首先来看看第一个,通过文档我们可以看到是
not match
,即不匹配的情况
void TestFindFirstNotOf()
{
string str("look for non-alphabetic characters...");
size_t found = str.find_first_not_of("abcdefghijklmnopqrstuvwxyz ");
if (found != string::npos)
{
cout << "The first non-alphabetic character is " << str[found];
cout << " at position " << found << '\n';
}
}
- 也是通过运行结果我们可以观察到,在字符串
str
中寻找26个英文字母 + 空格的时候,第一个找到的位置就是【12】,即为[-]
⑧ find_last_not_of
最后一个【find_last_not_of】,再坚持一下,马上就结束了(ง •_•)ง
void TestFindLastNotOf()
{
string str("look for non-alphabetic characters...");
size_t found = str.find_last_not_of("abcdefghijklmnopqrstuvwxyz ");
if (found != string::npos)
{
cout << "The last non-alphabetic character is " << str[found];
cout << " at position " << found << '\n';
}
}
- 可以看到从后往前找的话最后一个就是
[.]
,它的位置即为36
💦string类对象的非成员函数重载
接下去我们再来看看string类对象的非成员函数重载
函数名称 | 功能说明 |
operator+() | 尽量少用,因为传值返回,导致深拷贝效率低 |
relational operator(⭐) | 大小比较 |
operator>>() | 流插入重载 |
operator<<() | 流提取重载 |
getline(⭐) | 获取一行字符串 |
注意: ⭐表示重点掌握
① operator+ ()
接下去我们来说
operator+()
,看到它是否有想起operator+=()
呢,我们来对比辨析一下
- 可以看到对于
operator+=()
就是在后面追加字符串,不过operator+()
起到的是一个拼接的效果
- 如果你有看过 类和对象 的话,我在讲到日期的相加时间对比了【+】和【+=】的效果,前者在相加之后自身是不会有影响的,但是后者相加之后自身会受到影响。我们可以来看一下
- 如果是【+】的话,自身是不会受到影响的,我们要把结果放到另一个 string对象 中去
但是呢,二者的最本质区别还是在于这个效率问题,对于【+】而言,其底层在实现的时候因为无法对
this指针
本身造成修改,所以我们会通过拷贝构造出一个临时对象,对这个临时对象去做修改后返回,那我们知道返回一个出了作用域就销毁的对象,只能使用传值返回,此时又要发生一个拷贝
因此本接口其实不太推荐读者使用,了解一下即可,尽量还是使用【+=】来得好
② relational operators
接下去的话是一些关系运算符,这个我们在讲【日期类】的时候也是有自己模拟实现过,基本上实现了前面几个的话后面都是可以去做一个复用的,底层这一块如果读者想要深入了解的话就去看看日期类吧
- 其实大家仔细去看的话就可以发现这个接口的实现是非常冗余的,其实只给出第一中 string对象 和 string对象 比就可以了,后面的字符串其实在比较的时候可以去做一个 隐式类型转换
- 我这里来演示两个。可以看到如果是成立的话返回
true
,VS中用【1】来表示;反之则返回false
,VS中用【0】来表示
- 再来看两个,可以发现有了这个之后我们在比较两个 string对象 的时候就非常方便了
下面两个的话我们可以一起说,其实你看到现在的话完全就不需要我说了,因为我们一直在使用这个东西,在对 string对象 进行操作的之后将其打印输出使用的就是重载之后的【流插入】
③ operator>>()
- 首先我们来说说【流提取】,其实就是和我们使用
cin >>
在做输入操作的时候一样,控制台会先去等待我们输入一个值
④ operator<<()
- 然后就是【流插入】,通过去缓冲区中拿取数据,然后将其显示在控制台上
⑤ getline (⭐)
接下去再来说说【getline】,有了它我们可以到缓冲区中拿取一整行的数据
- 之前我们在学习C语言的时候使用
scanf()
,在读取字符串的时候经常是读到空格就结束了,而无法读取到后面的内容
然后我去网上找了很多的办法,一共是有以下三种
scanf("%[^\n]", s1);
② 第二种就是通过 gets_s 来进行读取
gets_s(s1);
③ 第三种乃是通过文件结束符EOF来进行判断,其是直接读取到换行符\n
为止
while ((scanf("%s", s1)) != EOF)
{
printf("%s ", s1);
}
💬 但是呢,在我们学习了
getline()
函数后,就不需要这么麻烦了,其可以在缓冲区中读取一整行的数据,而不会遇到空格就截止
四、共勉
以下就是我对 string类最全解析 的理解,如果有不懂和发现问题的小伙伴,请在评论区说出来哦,同时我还会继续更新对C++ vector 类的理解,请持续关注我哦!!!
文章来源:https://www.toymoban.com/news/detail-854238.html
文章来源地址https://www.toymoban.com/news/detail-854238.html
到了这里,关于【C++ STL】string类最全解析(什么是string?string类的常用接口有哪些?)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!