前言在上一篇文章中我们认识了 C 标准库中的string类并学习了字符串的构造、元素访问、迭代器、范围for以及容量相关接口。不过仅仅能够创建和遍历字符串显然是不够的。在实际使用中我们还经常需要向字符串中追加内容、在指定位置插入或删除字符、查找某段内容以及截取子字符串。因此本文将继续补充string类中常用的接口主要内容包括使用push_back、append和operator追加内容使用insert在指定位置插入内容使用erase和replace删除、替换内容使用find、rfind和substr完成查找与截取使用c_str与 C 语言接口进行衔接使用operator和getline完成字符串拼接与输入。string提供的重载非常多如果把每一个版本都列出来反而不容易记忆所以本文主要围绕常用写法展开遇到具体需求时再去查阅对应文档即可。一、字符串内容的尾部追加1. push_backpush_back用于在字符串末尾追加一个字符std::strings(hello);s.push_back( );调用完成后s的内容由hello变成了hello末尾多出了一个空格。它的一般使用形式为s.push_back(ch);需要注意push_back每次只能追加一个字符。如果需要追加一整段字符串就可以使用下面的append或operator。2. appendappend用于在原字符串末尾追加一段内容s.append(world);这时s就会变成hello world除了直接追加 C 字符串append还有很多重载例如追加另一个string中的一部分std::strings1(hello );std::strings2(C string);s1.append(s2,0,3);std::couts1std::endl;运行结果hello C其中0是从s2的哪个位置开始3表示最多追加三个字符。3. operatorstring对运算符进行了重载所以也可以像普通变量一样追加字符或字符串s haha;相比于append的写法更加简洁所以日常拼接字符串时使用得很多。std::strings(hello);s!;s C;std::coutsstd::endl;运行结果hello! C这三个接口可以简单区分为接口主要用途push_back在末尾追加一个字符append在末尾追加字符串并提供较多重载operator追加字符或字符串写法最简洁需要注意追加内容可能导致string扩容。只要字符串发生了可能改变内容或容量的操作后续就不要继续使用以前保存的迭代器、引用或底层字符指针重新获取是最稳妥的做法。二、使用 insert 在指定位置插入内容追加操作只能把内容放到字符串末尾而insert可以将字符或字符串插入到指定位置。来看下面的代码voidTest01(){std::strings(hello);s.push_back( );s.append(world);std::coutsstd::endl;// 实际上用 比较多s haha;std::coutsstd::endl;// 零位置插入 pp但是这个接口会把后面的数据// 向后挪动所以请谨慎使用s.insert(0,pp);std::coutsstd::endl;// 插入指定位置的子字符串// 函数原型// string insert(size_t pos, const string str, size_t subpos, size_t sublen);std::string s1hello;std::string s2abcdef;s1.insert(2,s2,1,3);std::couts1;// 插入多个字符// 函数原型// string insert(size_t pos, size_t count, char ch);// 在 pos 位置插入 count 个 chstd::string s3hello;s3.insert(2,3,x);std::couts3;}运行结果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 s1hello;std::string s2abcdef;s1.insert(2,s2,1,3);这行代码可以拆成两步理解从s2的下标1开始取出三个字符得到bcd将bcd插入s1的下标2处。因此最终结果为hebcdllo如果需要截取的字符个数超过了s2剩余的长度只会取到末尾但起始位置subpos不能超过s2.size()。3. 插入多个相同字符std::string s3hello;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()进行递减后再删除。C11 之后如果只是尾删直接使用pop_back()会更加清楚。下面是完整示例voidTest02(){// 删除std::strings(hello world);s.erase(4,5);std::coutsstd::endl;// 只传一个参数就从这个位置开始全删了s.erase(0);std::coutsstd::endl;// 也有迭代器版本syearmonthday;// 头删s.erase(s.begin());std::coutsstd::endl;// 尾删除s.erase(--s.end());std::coutsstd::endl;}运行结果hellld earmonthday earmonthda第二行输出的是空字符串所以运行结果中会出现一个空行。此外erase还有删除迭代器区间的版本std::strings(0123456789);s.erase(s.begin()2,s.begin()5);std::coutsstd::endl;它删除的是迭代器半开区间[begin() 2, begin() 5)也就是字符2、3、4最终得到0156789四、使用 replace 替换内容replace可以替换字符串中指定位置的内容我们可以将它简单理解为先删除指定范围中的旧内容再在同一个位置插入新内容。来看下面的代码voidTest03(){// 替换std::strings(I love you);s.replace(1,1,%%);std::coutsstd::endl;s.replace(1,2, );std::coutsstd::endl;s.replace(7,3,C);std::coutsstd::endl;// 查找size_t poss.find(z);if(posstd::string::npos)std::cout没找到std::endl;elsestd::coutpos -posstd::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 与 npos1. findfind用于从前向后查找字符或字符串并返回第一次匹配的位置size_t poss.find(z);如果找到了返回值就是对应字符的下标如果没有找到则返回std::string::npos所以不能直接使用查找结果一般要先进行判断if(posstd::string::npos){std::cout没找到std::endl;}else{std::coutpos - posstd::endl;}npos是一个特殊的静态成员常量用于表示“没有找到”它不是字符串中的最后一个下标也不是一个可以正常访问的位置。除了查找字符还可以查找字符串std::strings(one two one);autoposs.find(one);std::coutposstd::endl;运行结果0find还可以指定从哪个位置开始向后查找autoposs.find(one,1);这次会跳过下标0处的匹配继续向后查找因此返回第二个one的位置8。2. rfindrfind与find的主要区别是查找方向。它从字符串后面向前查找并返回最后一次匹配的位置std::strings(Main.cpp.com);size_t poss.rfind(.);这个字符串中有两个.它们的下标分别是4和8Main.cpp.com ↑ ↑ 4 8find(.)返回4而rfind(.)返回8。find和rfind都不会修改原字符串。没有找到目标时它们也不会因此抛出异常而是返回npos。六、使用 substr 截取子字符串substr用于从原字符串中截取一部分内容并返回一个新的string对象省略命名空间后它的常见形式为stringsubstr(size_t pos0,size_t countnpos)const;其中pos表示从哪个位置开始截取count表示最多截取多少个字符如果不传count则默认一直截取到字符串末尾。1. 基本使用std::strings(hello world);std::string subs.substr(6,5);std::coutsubstd::endl;运行结果worldsubstr返回的是一个新字符串不会修改原来的s。如果count超过了剩余字符个数只会截取到末尾并不会因此报错但如果pos s.size()则会抛出std::out_of_range。2. 提取文件后缀voidTest05(){std::strings(Main.cpp.com);size_t poss.rfind(.);std::string suffixs.substr(pos);std::coutsuffixstd::endl;}运行结果.com这段代码先使用rfind找到最后一个点再从该位置截取到字符串末尾。对于固定的Main.cpp.com上面的代码没有问题。但是如果将它写成通用的后缀提取函数就必须考虑文件名中没有.的情况。如果没有找到点rfind会返回npos接着调用substr(npos)会因为起始位置超过字符串长度而抛出异常。因此更稳妥的写法是voidTestSuffix(){std::strings(Main.cpp.com);autoposs.rfind(.);if(pos!std::string::npos){std::string suffixs.substr(pos);std::coutsuffixstd::endl;}else{std::cout没有找到文件后缀std::endl;}}如果只希望得到com而不保留前面的点可以在判断成功后写成std::string suffixs.substr(pos1);七、使用 c_str 与 C 语言接口衔接虽然 C 提供了string但是很多 C 语言接口只认识以\0结尾的字符指针。例如fopen接收的文件名参数是const char*不能直接把一个string对象传进去。c_str()可以返回一个指向 C 格式字符串的只读指针std::stringfile(data.txt);constchar*strfile.c_str();文件读取示例下面沿用 C 风格文件读取的思路。使用这些接口时有两个问题必须注意fopen打开失败时会返回空指针读取前要先判断fgetc的返回类型是int因为它还要表示特殊值EOF不能使用char接收后再判断文件结尾。因此写成下面这样会更加安全#includecstdio#includeiostream#includestringvoidTest04(){std::string file;std::cinfile;// 为了兼容 C 语言设计的接口std::FILE*finstd::fopen(file.c_str(),r);if(finnullptr){std::cerr文件打开失败std::endl;return;}intch0;while((chstd::fgetc(fin))!EOF){std::cout.put(static_castchar(ch));}std::fclose(fin);}这里的c_str()并没有复制出一份由我们管理的新字符数组它返回的指针仍然由原来的string对象管理。因此不要通过这个指针修改字符串内容原string对象销毁后指针就不能再使用原字符串发生可能改变内容或容量的操作后应重新调用c_str()获取指针。另外示例中的std::cin file遇到空格会停止读取。如果文件路径可能包含空格就应该使用后面介绍的getline读取完整路径。八、operator 与字符串拼接operator可以将两个字符串拼接起来并返回一个新的string对象std::strings1(hello);std::string s2s1 world;std::couts2std::endl;运行结果hello world标准库还提供了const char* string的重载所以这样写也是合法的std::string s3world s1;std::couts3std::endl;运行结果world hello需要注意下面这种写法是错误的// 两边都是字符串字面量并没有 string 对象参与// std::string s hello world;至少要让其中一边成为stringstd::string sstd::string(hello )world;operator会产生一个新的字符串而operator会直接修改左边的字符串。如果需要在循环中不断向同一个字符串追加内容一般优先使用、append并在能够估计最终长度时配合reserve避免创建过多临时对象和发生多次扩容。九、使用 getline 读取一整行1. cin 与 getline 的区别使用输入运算符读取字符串时std::cins;它会跳过前导空白并在遇到空格、制表符或换行符等空白字符时停止。假设输入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 s2s1 world;std::couts2std::endl;std::string s3world s1;std::couts3std::endl;std::string s;// 可以读取包含空格的字符串而 cin 遇到空格会停止读取// 默认以换行符作为分隔getline(std::cin,s);std::coutsstd::endl;// 期望遇到 % 号停止std::string ss;getline(std::cin,ss,%);std::coutssstd::endl;}这里没有写std::getline也可以通过编译是因为参数中包含std::cin和std::string编译器能够在std命名空间中找到对应函数。不过为了让代码所属位置更加直观也可以显式写成std::getline。3. cin 后紧接 getline 的问题如果前面刚刚使用过std::cinvalue;输入缓冲区中可能还留有一个换行符。此时立刻调用getline它会直接读到这个换行符从而得到一个空字符串。如果不需要保留下一行开头的空白可以这样处理std::getline(std::cinstd::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则解决了输入运算符遇到空格就停止的问题。将这些接口组合起来我们已经可以比较方便地完成常见的字符串增、删、改、查和输入操作了。完