前言
在上一篇文章中,我们认识了 C++ 标准库中的string类,并学习了字符串的构造、元素访问、迭代器、范围for以及容量相关接口。
不过仅仅能够创建和遍历字符串显然是不够的。在实际使用中,我们还经常需要向字符串中追加内容、在指定位置插入或删除字符、查找某段内容以及截取子字符串。
因此本文将继续补充string类中常用的接口,主要内容包括:
- 使用
push_back、append和operator+=追加内容; - 使用
insert在指定位置插入内容; - 使用
erase和replace删除、替换内容; - 使用
find、rfind和substr完成查找与截取; - 使用
c_str与 C 语言接口进行衔接; - 使用
operator+和getline完成字符串拼接与输入。
string提供的重载非常多,如果把每一个版本都列出来反而不容易记忆,所以本文主要围绕常用写法展开,遇到具体需求时再去查阅对应文档即可。
一、字符串内容的尾部追加
1. push_back
push_back用于在字符串末尾追加一个字符:
std::strings("hello");s.push_back(' ');调用完成后,s的内容由hello变成了hello,末尾多出了一个空格。
它的一般使用形式为:
s.push_back(ch);需要注意,push_back每次只能追加一个字符。如果需要追加一整段字符串,就可以使用下面的append或operator+=。
2. append
append用于在原字符串末尾追加一段内容:
s.append("world");这时s就会变成:
hello world除了直接追加 C 字符串,append还有很多重载,例如追加另一个string中的一部分:
std::strings1("hello ");std::strings2("C++ string");s1.append(s2,0,3);std::cout<<s1<<std::endl;运行结果:
hello C++其中0是从s2的哪个位置开始,3表示最多追加三个字符。
3. operator+=
string对+=运算符进行了重载,所以也可以像普通变量一样追加字符或字符串:
s+=" haha";相比于append,+=的写法更加简洁,所以日常拼接字符串时使用得很多。
std::strings("hello");s+='!';s+=" C++";std::cout<<s<<std::endl;运行结果:
hello! C++这三个接口可以简单区分为:
| 接口 | 主要用途 |
|---|---|
push_back | 在末尾追加一个字符 |
append | 在末尾追加字符串,并提供较多重载 |
operator+= | 追加字符或字符串,写法最简洁 |
需要注意,追加内容可能导致string扩容。只要字符串发生了可能改变内容或容量的操作,后续就不要继续使用以前保存的迭代器、引用或底层字符指针,重新获取是最稳妥的做法。
二、使用 insert 在指定位置插入内容
追加操作只能把内容放到字符串末尾,而insert可以将字符或字符串插入到指定位置。
来看下面的代码:
voidTest01(){std::strings("hello");s.push_back(' ');s.append("world");std::cout<<s<<std::endl;// 实际上用 += 比较多s+=" haha";std::cout<<s<<std::endl;// 零位置插入 pp,但是这个接口会把后面的数据// 向后挪动,所以请谨慎使用s.insert(0,"pp");std::cout<<s<<std::endl;// 插入指定位置的子字符串// 函数原型// string& insert(size_t pos, const string& str, size_t subpos, size_t sublen);std::string s1="hello";std::string s2="abcdef";s1.insert(2,s2,1,3);std::cout<<s1;// 插入多个字符// 函数原型// string& insert(size_t pos, size_t count, char ch);// 在 pos 位置插入 count 个 chstd::string s3="hello";s3.insert(2,3,'x');std::cout<<s3;}运行结果:
hello world hello world haha pphello world haha hebcdllohexxxllo最后两个结果连在一起,是因为代码输出s1和s3时没有换行,不影响插入结果本身。
1. 在指定位置插入字符串
s.insert(0,"pp");第一个参数0表示插入位置,所以内容会被放到原字符串最前面:
hello world haha ↓ pphello world haha字符串下标从0开始。当pos == s.size()时,相当于在字符串末尾插入;如果pos > s.size(),则会抛出std::out_of_range异常。
2. 插入另一个字符串的子串
std::string s1="hello";std::string s2="abcdef";s1.insert(2,s2,1,3);这行代码可以拆成两步理解:
- 从
s2的下标1开始取出三个字符,得到bcd; - 将
bcd插入s1的下标2处。
因此最终结果为:
hebcdllo如果需要截取的字符个数超过了s2剩余的长度,只会取到末尾;但起始位置subpos不能超过s2.size()。
3. 插入多个相同字符
std::string s3="hello";s3.insert(2,3,'x');这里表示在下标2处插入三个字符x,得到:
hexxxllo第二个参数3表示字符个数,并不是结束下标,这一点和后面很多接口的count参数相同。
在字符串头部或中间插入内容时,插入点后面的字符通常需要整体向后移动,并且可能触发扩容。因此insert虽然使用方便,但不适合在很长的字符串中无意义地频繁头插或中间插入。
三、使用 erase 删除内容
1. 根据下标和字符个数删除
erase可以从指定下标开始删除若干个字符:
std::strings("hello world");s.erase(4,5);这里从下标4开始删除5个字符,也就是删除半开区间[4, 9)中的o wor,所以结果并不是hello,而是:
helllderase(pos, count)中的第二个参数表示删除数量,而不是结束位置。如果count大于剩余字符个数,则只会删除到字符串末尾。
2. 只传一个下标参数
s.erase(0);下标版本的erase只传一个参数时,会从这个位置一直删除到字符串末尾。因此从下标0开始删除,效果就相当于清空整个字符串。
3. 迭代器版本
erase还提供了迭代器版本,可以删除迭代器指向的字符:
s.erase(s.begin());这表示删除字符串的第一个字符。
s.erase(--s.end());end()指向最后一个字符的下一个位置,先执行--才会移动到最后一个有效字符,因此这行代码表示尾删。
需要注意,这两种写法都要求字符串非空。空字符串的begin()等于end(),此时不能删除它指向的内容,也不能对end()进行递减后再删除。C++11 之后,如果只是尾删,直接使用pop_back()会更加清楚。
下面是完整示例:
voidTest02(){// 删除std::strings("hello world");s.erase(4,5);std::cout<<s<<std::endl;// 只传一个参数就从这个位置开始全删了s.erase(0);std::cout<<s<<std::endl;// 也有迭代器版本s+="yearmonthday";// 头删s.erase(s.begin());std::cout<<s<<std::endl;// 尾删除s.erase(--s.end());std::cout<<s<<std::endl;}运行结果:
hellld earmonthday earmonthda第二行输出的是空字符串,所以运行结果中会出现一个空行。
此外,erase还有删除迭代器区间的版本:
std::strings("0123456789");s.erase(s.begin()+2,s.begin()+5);std::cout<<s<<std::endl;它删除的是迭代器半开区间[begin() + 2, begin() + 5),也就是字符2、3、4,最终得到:
0156789四、使用 replace 替换内容
replace可以替换字符串中指定位置的内容,我们可以将它简单理解为:
先删除指定范围中的旧内容,再在同一个位置插入新内容。
来看下面的代码:
voidTest03(){// 替换std::strings("I love you");s.replace(1,1,"%%");std::cout<<s<<std::endl;s.replace(1,2," ");std::cout<<s<<std::endl;s.replace(7,3,"C++");std::cout<<s<<std::endl;// 查找size_t pos=s.find('z');if(pos==std::string::npos)std::cout<<"没找到"<<std::endl;elsestd::cout<<"pos ->"<<pos<<std::endl;}三次替换的变化过程为:
I love you I%%love you I love you I love C++第一行:
s.replace(1,1,"%%");表示从下标1开始替换一个字符,把原来的空格换成%%。
第二行又把两个%替换成一个空格,字符串重新变回I love you。
最后:
s.replace(7,3,"C++");从下标7开始替换三个字符,也就是把you换成C++。
可以发现,被删除的字符数量和新字符串的长度不必相同,所以replace既可能使字符串变长,也可能使字符串变短。
和前面的接口一样,如果pos > s.size(),replace会抛出std::out_of_range;如果删除数量超过剩余长度,则只会删除到末尾。
五、find、rfind 与 npos
1. find
find用于从前向后查找字符或字符串,并返回第一次匹配的位置:
size_t pos=s.find('z');如果找到了,返回值就是对应字符的下标;如果没有找到,则返回:
std::string::npos所以不能直接使用查找结果,一般要先进行判断:
if(pos==std::string::npos){std::cout<<"没找到"<<std::endl;}else{std::cout<<"pos -> "<<pos<<std::endl;}npos是一个特殊的静态成员常量,用于表示“没有找到”,它不是字符串中的最后一个下标,也不是一个可以正常访问的位置。
除了查找字符,还可以查找字符串:
std::strings("one two one");autopos=s.find("one");std::cout<<pos<<std::endl;运行结果:
0find还可以指定从哪个位置开始向后查找:
autopos=s.find("one",1);这次会跳过下标0处的匹配,继续向后查找,因此返回第二个one的位置8。
2. rfind
rfind与find的主要区别是查找方向。它从字符串后面向前查找,并返回最后一次匹配的位置:
std::strings("Main.cpp.com");size_t pos=s.rfind('.');这个字符串中有两个.,它们的下标分别是4和8:
Main.cpp.com ↑ ↑ 4 8find('.')返回4,而rfind('.')返回8。
find和rfind都不会修改原字符串。没有找到目标时,它们也不会因此抛出异常,而是返回npos。
六、使用 substr 截取子字符串
substr用于从原字符串中截取一部分内容,并返回一个新的string对象:
省略命名空间后,它的常见形式为:
stringsubstr(size_t pos=0,size_t count=npos)const;其中:
pos表示从哪个位置开始截取;count表示最多截取多少个字符;- 如果不传
count,则默认一直截取到字符串末尾。
1. 基本使用
std::strings("hello world");std::string sub=s.substr(6,5);std::cout<<sub<<std::endl;运行结果:
worldsubstr返回的是一个新字符串,不会修改原来的s。
如果count超过了剩余字符个数,只会截取到末尾,并不会因此报错;但如果pos > s.size(),则会抛出std::out_of_range。
2. 提取文件后缀
voidTest05(){std::strings("Main.cpp.com");size_t pos=s.rfind('.');std::string suffix=s.substr(pos);std::cout<<suffix<<std::endl;}运行结果:
.com这段代码先使用rfind找到最后一个点,再从该位置截取到字符串末尾。
对于固定的Main.cpp.com,上面的代码没有问题。但是如果将它写成通用的后缀提取函数,就必须考虑文件名中没有.的情况。
如果没有找到点,rfind会返回npos,接着调用substr(npos)会因为起始位置超过字符串长度而抛出异常。因此更稳妥的写法是:
voidTestSuffix(){std::strings("Main.cpp.com");autopos=s.rfind('.');if(pos!=std::string::npos){std::string suffix=s.substr(pos);std::cout<<suffix<<std::endl;}else{std::cout<<"没有找到文件后缀"<<std::endl;}}如果只希望得到com而不保留前面的点,可以在判断成功后写成:
std::string suffix=s.substr(pos+1);七、使用 c_str 与 C 语言接口衔接
虽然 C++ 提供了string,但是很多 C 语言接口只认识以\0结尾的字符指针。例如fopen接收的文件名参数是const char*,不能直接把一个string对象传进去。
c_str()可以返回一个指向 C 格式字符串的只读指针:
std::stringfile("data.txt");constchar*str=file.c_str();文件读取示例
下面沿用 C 风格文件读取的思路。使用这些接口时,有两个问题必须注意:
fopen打开失败时会返回空指针,读取前要先判断;fgetc的返回类型是int,因为它还要表示特殊值EOF,不能使用char接收后再判断文件结尾。
因此写成下面这样会更加安全:
#include<cstdio>#include<iostream>#include<string>voidTest04(){std::string file;std::cin>>file;// 为了兼容 C 语言设计的接口std::FILE*fin=std::fopen(file.c_str(),"r");if(fin==nullptr){std::cerr<<"文件打开失败"<<std::endl;return;}intch=0;while((ch=std::fgetc(fin))!=EOF){std::cout.put(static_cast<char>(ch));}std::fclose(fin);}这里的c_str()并没有复制出一份由我们管理的新字符数组,它返回的指针仍然由原来的string对象管理。因此:
- 不要通过这个指针修改字符串内容;
- 原
string对象销毁后,指针就不能再使用; - 原字符串发生可能改变内容或容量的操作后,应重新调用
c_str()获取指针。
另外,示例中的std::cin >> file遇到空格会停止读取。如果文件路径可能包含空格,就应该使用后面介绍的getline读取完整路径。
八、operator+ 与字符串拼接
operator+可以将两个字符串拼接起来,并返回一个新的string对象:
std::strings1("hello");std::string s2=s1+" world";std::cout<<s2<<std::endl;运行结果:
hello world标准库还提供了const char* + string的重载,所以这样写也是合法的:
std::string s3="world "+s1;std::cout<<s3<<std::endl;运行结果:
world hello需要注意,下面这种写法是错误的:
// 两边都是字符串字面量,并没有 string 对象参与// std::string s = "hello " + "world";至少要让其中一边成为string:
std::string s=std::string("hello ")+"world";operator+会产生一个新的字符串,而operator+=会直接修改左边的字符串。如果需要在循环中不断向同一个字符串追加内容,一般优先使用+=、append,并在能够估计最终长度时配合reserve,避免创建过多临时对象和发生多次扩容。
九、使用 getline 读取一整行
1. cin >> 与 getline 的区别
使用输入运算符读取字符串时:
std::cin>>s;它会跳过前导空白,并在遇到空格、制表符或换行符等空白字符时停止。
假设输入:
hello world那么s中只会得到:
hello如果希望保留一行中的空格,可以使用:
getline(std::cin,s);getline默认一直读取到换行符,所以这时s中保存的是完整的hello world。换行符会从输入流中取走,但不会保存到字符串里。
2. 指定分隔符
getline还可以传入第三个参数,指定遇到哪个字符时停止:
std::string ss;getline(std::cin,ss,'%');假设输入:
hello world%abc那么ss中得到:
hello world分隔符%会被从输入流中取走,但不会存入字符串。此时换行符不再是指定分隔符,如果一直没有输入%,读取可能跨过换行继续进行,直到遇到%或文件结束。
将这些内容整理到一起,完整示例如下:
voidTest06(){std::strings1("hello");std::string s2=s1+" world";std::cout<<s2<<std::endl;std::string s3="world "+s1;std::cout<<s3<<std::endl;std::string s;// 可以读取包含空格的字符串,而 cin >> 遇到空格会停止读取// 默认以换行符作为分隔getline(std::cin,s);std::cout<<s<<std::endl;// 期望遇到 % 号停止std::string ss;getline(std::cin,ss,'%');std::cout<<ss<<std::endl;}这里没有写std::getline也可以通过编译,是因为参数中包含std::cin和std::string,编译器能够在std命名空间中找到对应函数。不过为了让代码所属位置更加直观,也可以显式写成std::getline。
3. cin >> 后紧接 getline 的问题
如果前面刚刚使用过:
std::cin>>value;输入缓冲区中可能还留有一个换行符。此时立刻调用getline,它会直接读到这个换行符,从而得到一个空字符串。
如果不需要保留下一行开头的空白,可以这样处理:
std::getline(std::cin>>std::ws,s);std::ws会先吃掉前导空白,再由getline读取内容。不过它也会移除原本可能需要保留的行首空格,所以是否使用要根据实际输入格式决定。
十、常用接口速查
| 接口 | 主要作用 | 是否修改原字符串 |
|---|---|---|
push_back | 在末尾追加一个字符 | 是 |
append | 在末尾追加字符串 | 是 |
operator+= | 追加字符或字符串 | 是 |
insert | 在指定位置插入内容 | 是 |
erase | 删除指定位置或区间 | 是 |
replace | 替换指定位置的内容 | 是 |
find | 从前向后查找 | 否 |
rfind | 从后向前查找 | 否 |
substr | 截取并返回新的子字符串 | 否 |
c_str | 返回 C 格式字符串指针 | 否 |
operator+ | 拼接并返回新的字符串 | 否 |
getline | 从输入流读取一段字符串 | 修改接收结果 |
十一、需要注意的细节
1. count 表示数量,不是结束位置
在insert、erase、replace和substr等接口中,count或len一般表示处理多少个字符,而不是结束下标。
例如:
s.erase(4,5);表示从下标4开始删除五个字符,对应的半开区间为[4, 9)。
2. npos 不是有效下标
find和rfind没有找到目标时会返回npos。把它继续传给substr或用于下标访问都可能产生错误,所以一定要先判断。
3. end() 不能直接用于删除
end()指向尾后位置,它不能被解引用,也不能直接传给单迭代器版本的erase。尾删前要确保字符串非空,再使用有效的最后一个字符位置。
4. 修改后重新获取旧指针或迭代器
追加、插入、删除和替换都可能使原来的迭代器、引用和字符指针失效。不要依赖某一次操作是否刚好发生扩容,修改完成后重新获取更安全。
5. getline 默认遇到换行符停止
std::cin >> s才是遇到空白字符停止;getline(std::cin, s)默认读取到换行符。两者不要混淆。
十二、总结
本文继续补充了string类中常用的修改、查找和输入接口。
在字符串末尾追加内容时,可以根据需要使用push_back、append或operator+=;需要在指定位置插入时可以使用insert,但要注意中间插入可能需要移动后面的字符。
删除和替换分别可以使用erase与replace。这些接口中的第二个整数参数通常表示字符数量,并不是结束下标。使用迭代器版本删除元素时,则必须保证迭代器指向有效字符。
查找时,find从前向后寻找,rfind从后向前寻找。如果没有找到,它们会返回string::npos。配合substr可以完成后缀提取等操作,不过在通用代码中一定要先判断查找是否成功。
c_str()为string和 C 语言接口之间提供了衔接方式,而getline则解决了输入运算符遇到空格就停止的问题。将这些接口组合起来,我们已经可以比较方便地完成常见的字符串增、删、改、查和输入操作了。
完