4)string类对象的修改操作
接下去呢我们来讲讲string类对象的修改操作
① 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 += "feng";
- 最后呢则是拼接一个字符
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对象的 ==深浅拷贝== 问题,读者先了解一下
- 通过运行结果我们可以看到两个字符串确实发生了交换
这里再补充一道面试题
- 本题如果这里的
replace()
接口的话较为合适,但是呢效率却不是很高,下面我介绍一种高效的办法,利用到的是我们上面所讲的+=
,只需要去遍历一下这个字符串即可,然后判断其是否为【空格】即可,如果不是的话就直接拼接过来,如果是空格的话就拼接%20
- 这种方法大大地降低了时间复杂度,无需去考虑挪动数据的问题,大家可以参考一下这种做法
string replaceSpaces(string S, int length) { string str = ""; for(int i = 0;i < length; ++i) { if(S[i] == ' '){ str += "%20"; }else{ str += S[i]; } } return str; }
5)string类对象的其他字符串操作
然后我们再来看看有关string类对象的其他字符串操作接口
函数名称 | 功能说明 |
c_str | 返回C格式字符串 |
substr | 在str中从pos位置开始,截取n个字符,然后将其返回 |
find | 从字符串pos位置开始往后找字符c,返回该字符在字符串中的位置 |
rfind | 从字符串pos位置开始往前找字符c,返回该字符在字符串中的位置 |
find_first_of | 从前往后找第一个匹配的字符 |
find_last_of | 从后往前找第一个匹配的字符 |
find_first_not_of | 从前往后找第一个不匹配的字符 |
find_last_not_of | 从前往后找第一个不匹配的字符 |
① c_str
- 假设现在我有一个名为
test.cpp
的文件,想要使用C语言中的fopen()
打开它,但是呢却遇到了一些的问题
void TestCStr() { string str = "test.cpp"; fopen(str, "r"); }
- 编译之后发现报出错误说:
不存在从“std:string"到"const char*”的适当转换函数
,原因就在于我们这个【test.cpp】使用到是string类来进行存储,而如果你去查看 fopen 的文档的话,就可以发现 第一个参数所要传入的是一个字符串,这就是问题所在
FILE * fopen ( const char * filename, const char * mode );
此时呢【string】类给我们提供了一个接口函数叫做 c_str
,帮助我们将 string对象 转换为 字符串
- 转换的形式就是下面这样。可以看出这个接口真的打通了C和C++之间的一堵墙,很好地起到了一个连通的效果
FILE* fout = fopen(str.c_str(), "r");
💬 再拓展一个接口叫做【data】,仔细对比一下可以发现二者很类似,因为【data】是很早之前就定义好的接口,而【c_str】则是后面才被定义出来的,现在我们用的一般都是【size_t】
② substr
接下去是一个截取子串的接口
- 很好理解,就是从这个原本的string中截取出一部分的内容
- 当然如果不给长度的话默认使用的便是缺省值
pos
③ 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
- 那有的同学就懵逼了,只要怎么去割呢?还记得我们在C语言中所学习过的 字符串函数 吗,使用里面的【strcpy】、【strstr】、【strtok】就可以去完成这个逻辑,但是呢过程会非常地繁琐。况且我们在学习了 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");
④ 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."); 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
6)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()
函数后,就不需要这么麻烦了,其可以在缓冲区中读取一整行的数据,而不会遇到空格就截止
五、总结与提炼
最后来总结一下本文所学习的内容
- 本文我们重点讲到的是STL中的string类,首先我们初步认识了这个类,逐个地去了解了它的一些接口函数,包括【默认成员函数】、【常见容量操作】、【访问及遍历操作】、【修改操作】、【其他字符串操作】以及【非成员函数重载】。基本上文档中的每一个接口我们都有去了解过,希望读者可以烂熟于心,常常翻阅使用
以上就是本文要介绍的所有内容,感谢您的阅读:rose::rose::rose: