字符串的处理在代码中是高频操作,C语言定义字符串是一块以null结束的内存,相关函数接口很底层,error-prone。C++出现了string对象,可以在一个更高的抽象层面简洁高效地处理字符串。
std::basic_string<char>
string是类型为char的容器,因此具有容器的一些基本接口,同时它还具有方便处理字符串的接口。汇总一下string对象的接口,overview:
// element access
operator[]
front
c_str // returns a non-modifiable standard C character array version of the string
// iterator
begin
cbegin
rbegin
crbegin
crend
// capacity
empty
length // the same with size
max_size
reserve
capacity
shrink_to_fit
// operation
clear
insert
erase
push_back
pop_back
append // 会改变size
operator+=
compare
starts_with
ends_with
contains
replace
substr
resize
resize_and_overwrite
// search
rfind
find_first_of
find_first_not_of
find_last_of
find_last_not_of
// constants
npos // static, special value. The exact meaning depends on the context
// numeric conversion
stoll
stoul
stoull
stold
to_string
下面是有问题的代码片段:
string a;
a.reserve(4);
a[2] = 'a';
运行时错误:
/builddir/build/BUILD/gcc-12.2.1-20220819/obj-x86_64-redhat-linux/x86_64-redhat-linux/libstdc++-v3/include/bits/basic_string.h:1221: std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::reference std::__cxx11::basic_string<_CharT, _Traits, _Alloc>::operator[](size_type) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>; reference = char&; size_type = long unsigned int]: Assertion '__pos <= size()' failed.
Aborted (core dumped)
什么情况?
reserve虽然预留了足够的空间,但直接在index=2的位置赋值的时候,没有通过__pos <= size()
的检查。不是说[]
操作符不检查麻.....实际上,[]
是对已经存在的对象进行修改,如果对象还不存在,就是个错误,index不能超过size!
string a;
a.resize(4);
a[2] = 'a';
下面的测试代码,主要应用了string对象的创建,长度函数和转C字符串函数,以及字符串的任意拼接,位置内容的修改。
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456"}; // C++ style, same as s1 = "123456";
string s2{};
string s3;
string s4 = s1;
// cannot printf c++ string object
cout << s1 << " " << s1.length() << endl;
cout << "a" << s2 << "|" << s3 << "b" << endl;
cout << s2.length() << s3.length() << endl;
cout << s4 << endl;
// "cccccc"
string s5(6, 'c'); // cannot be a string with double quote
cout << s5 << endl;
string s6{'a'};
cout << s6 << endl;
printf("%s\n", s6.c_str()); // one char string object
string s7{"abcdefg"};
for (size_t i{}; i<s7.length(); ++i)
cout << s7[i] << "-";
cout << endl;
for (size_t i{}; i<s7.length(); ++i)
cout << string{s7[i]} + "-"; // it's working...
cout << endl;
for (size_t i{}; i<s7.length(); ++i)
printf("%c-", s7[i]); // char!
printf("\n");
// return a c string
printf("%s\n", s7.c_str());
string s8 = s1 + s7;
cout << s8 << endl;
s8 = s1 + s6;
cout << s8 <<endl;
s8 = s1 + "777";
cout << s8 <<endl;
s8 += s8;
cout << s8 <<endl;
s8 = s1 + 'g';
cout << s8 << endl;
s8 = s1 + 'g' + "890";
cout << s8 << endl;
//s8 = 'g' + "890" + s1; // wrong, undifined result
s8 = 'g' + s1 + "uiouio";
cout << s8 << endl;
s8 = "12345" "abcde"; // C style
cout << s8 << endl;
s8[2] = 'g'; // direct modification
cout << s8 << endl;
return 0;
需引用<string>
头文件,才能使用string对象。(注意不是<cstring>
)
length接口返回字符串的长度,size_t类型;(还有个size接口,与length一样)
string对象的输出,需要使用cout
,如果要使用printf打印字符串,需要转换,用c_str接口。
字符串的拼接已经与其它动态类型语言相差无几了(比如Python),直接用+
号,可以拼char,也可以拼另一个string对象。(如果没有string参与加号拼接,就是非法的,但可以省略加号,这是C语言就支持的拼接语法)
string对象支持indexing,比如s7[i]
,indexing出来的是char。可以直接对string对象的某个位置进行修改!
相对Python,C++的string对象还是复杂,没办法,要底层,要跟C兼容。Python中没有char,用单字母字符串来代替,的确简化了统一了。而且Python提供的字符串slicing功能,真心方便。
不要再认为string的末尾有个null,string是个复杂对象,隐藏了内部实现细节。
检讨一下这一行错误代码s8 = 'g' + "890" + s1
,错误的原因是,这两个加法,谁先谁后是不确定的。C++中有一个术语,叫做evaluation order
,说的就是这个事儿。In general, C++ has no clearly specified execution order for operands。类似的错误还有b = ++a + a
,b的值也是不确定的。(写成多行,不要有歧义,别作...)
这是一种有风险的创建string对象的方式,有bug的代码如下:
#include <iostream>
#include <string>
using namespace std;
int main(void) {
char aa[5]{0x30,0x31,0x00,0x32,0x33};
string s1{aa};
cout << s1 << " " << s1.length() << endl;
return 0;
由于char中间有一个0x00,导致创建的string对象长度仅为2。
对于string对象,可以直接使用[]
的方式获取某个位置的char,但这种方式有风险,一旦越界,程序就会崩溃,这种崩溃是try...catch...无法捕获的。此时,可以使用at
,它有边界检查,一旦越界,会抛出可捕获的异常。
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string s{"12345"};
try {
cout << s.at(10) << endl;
} catch (exception& e) {
cout << e.what() << endl;
return 0;
这是个真方便的接口,将很多常用的类型转换成string对象。
#include <string>
#include <iostream>
using namespace std;
int main(void) {
cout << to_string(123) << endl;
cout << to_string(123L) << endl;
cout << to_string(123UL) << endl;
cout << to_string(123ULL) << endl;
cout << to_string(1.23f) << endl;
return 0;
请看示例:
#include <string>
#include <iostream>
using namespace std;
int main(void) {
string s1{" 123\n\t\v\f\r"};
string s2{"1.23"};
int a1 = stoi(s1);
double a2 = stod(s2); // double
float a3 = stof(s2); // float
cout << a1 << " " << a2 << " " << a3 << endl;
return 0;
注意a1
,它演示了stoi系列接口,能够自动trim字符串。(这里还有自己写的trim)
如果转换越界,这一组接口会抛出std::out_of_range
异常。(而原C标准库中的atoi
这一组接口,承诺不会抛出异常,但可能会出现转换后overflow的情况)
可直接使用==
和!=
来比较两个string对象所包含的内容是否相同,以及用<
和>
按ASCII顺序比较两个字符串对象的大小(直到遇到不同的字符或末尾)。测试代码如下:
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"12345"};
string s2{s1};
string s3{"123a"};
if (s1 == s2)
printf("s1 == s2\n");
if (s1 != s3)
printf("s1 != s3\n");
if (s1 < s3)
printf("s1 < s3\n");
return 0;
下面三种方式,都可以用来判断是否为空string对象:
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;
int main(void) {
string s{};
if (s.empty())
cout << "s.empty" << endl;
if (s == "")
cout << "s == \"\"" << endl;
if (s.size() == 0)
cout << "s.size() == 0" << endl;
return 0;
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string s{"123456789"};
for (auto c: s)
cout << c;
cout << endl;
return 0;
insert接口用于插入一个子串,但不能插入一个char:
#include <iostream>
#include <cstdio>
#include <string>
using
namespace std;
int main(void) {
string s1{"123456"};
string s2, s3, s4;
s2 = s3 = s4 = s1;
cout << s1 << endl;
s2.insert(3, "abc");
cout << s2 << endl;
s3.insert(0, "qaz");
cout << s3 << endl;
try {
s4.insert(10, "ghj");
cout << s4 << endl;
} catch(std::out_of_range& e) {
cout << "exception out_of_range: " << e.what() << endl;
string s5 = s1;
//s5.insert(1, 'a'); // wrong
s5.insert(1, string{'a'});
cout << s5 << endl;
return 0;
insert接口的第1个参数是index,如果index超过了string的长度,throw std::out_of_range。
直接insert一个char是非法的。
erase从一个位置开始,删除一个长度的子串,in-place,直接修改string对象。也可以只提供开始index,将后面的全部erase掉。测试代码如下:
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456"};
string s2, s3, s4, s5;
s2 = s3 = s4 = s5 = s1;
cout << s1 << endl;
// 123
s2.erase(3, 3);
cout << s2 << endl;
// 456
s3.erase(0, 3);
cout << s3 << endl;
try {
s4.erase(10, 2);
cout << s4 << endl;
} catch(std::out_of_range& e) {
cout << "exception out_of_range: " << e.what() << endl;
// no erase virtually
s5.erase(1,0);
cout << s5 << endl;
try {
s5.erase(10,0);
cout << s5 << endl;
} catch (std::out_of_range& e) {
cout << "exception out_of_range: " << e.what() << endl;
// erase to the end
string s6 = s1;
s6.erase(1);
cout << s6 << endl;
return 0;
开始位置(第1个参数,index)不能越界,但是长度(第2个参数)可以越界。空string对象可以执行s.erase(0)
,index可以等于size,不能大于。
substr是C++字符串切片操作的接口,它的prototype如下:
basic_string substr( size_type pos = 0, size_type count = npos ) const;
两个参数都有默认值,有const表示它是个accessor,返回对象是新创建的。
可能会抛出异常,std::out_of_range if pos > size()
substr()
,一个完整的copy;
substr(pos)
,从pos开始,切到最后;
substr(pos,count)
,普通切片。
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string s{"123456"};
cout << s.substr() << endl;
cout << s.substr(2) << endl;
cout << s.substr(2,3) << endl;
try {
cout << s.substr(9) << endl;
} catch(std::out_of_range& e) {
cout << "exception out_of_range: " << e.what() << endl;
return 0;
find接口查找子串或某个字符,可以指定查找的开始位置index,找到就停下来,返回index,如果没有找到,返回string:npos
(这是一个超大的数,保证大于任何有效index,等于(size_t)-1
:
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456abc34567"};
cout
<< s1.find("3") << endl;
cout << s1.find("34",6) << endl;
// not found, no throw
size_t w = s1.find("34", 32);
cout << string::npos << endl;
cout << w << endl;
// not found
size_t p = s1.find("gg");
if (p == (size_t)-1)
cout << "not found" << endl;
return 0;
18446744073709551615
18446744073709551615
not found
rfind与find不同的地方在于,其第2个参数,表示找到这个index后就停下来,不再继续找下去,应该是restricted find这个英文。停下来的index是要参与比较的,如果与要查到的子串的首字母匹配,会继续匹配下去。
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456abc34567"};
cout << s1.rfind("3") << endl;
cout << s1.rfind("34",6) << endl;
// return 0
cout << s1.rfind("12345",0) << endl;
// return 1
cout << s1.rfind("2345",1) << endl;
// not found
cout << s1.rfind("abc",0) << endl;
// not found, no throw
cout << s1.rfind("34",32) << endl;
size_t p = s1.rfind("gg",32);
if (p == string::npos)
cout << "not found" << endl;
return 0;
find_first_of,find_last_of
find_first_of,首次出现第1个参数中的字符串中任意一个字符的index,第2个参数表示从这个index开始搜索。
find_last_of,最后出现第1个参数中的字符串中任意一个字符的index,第2个参数表示从末尾逆向移动的位置数,此位置作为开始index。
没找到返回string:npos
。
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456abc34567"};
cout << s1.find_first_of("cba") << endl;
cout << s1.find_last_of("cba") << endl;
cout << s1.find_first_of("cba",10) << endl;
cout << s1.find_last_of("cba",5) << endl;
return 0;
18446744073709551615
18446744073709551615
find_first_not_of,find_last_not_of
#include <iostream>
#include <cstdio>
#include <string>
using namespace std;
int main(void) {
string s1{"123456abc34567"};
cout << s1.find_first_not_of("cba") << endl;
cout << s1.find_last_not_of("cba") << endl;
cout << s1.find_first_not_of("cba",10) << endl;
cout << s1.find_last_not_of("cba",5) << endl;
return 0;
empty接口用来判断字符串是否为一个空串,是true,不是false:
#include <cstdio>
#include <string>
int main(void) {
std::string ss = "";
std::string sy = ".";
printf("ss: %s\n", ss.empty()?"true":"false");
printf("sy: %s\n", sy.empty()?"true":"false");
return 0;
ss: true
sy: false
clear接口的作用,将字符串对象清空,就是将其变成一个空串。
#include <cstdio>
#include <string>
int main(void) {
std::string ss = "abcdef";
std::string sy = ".....";
ss.clear();
sy.clear();
printf("ss: %s\n", ss.empty()?"true":"false");
printf("sy: %s\n", sy.empty()?"true":"false");
return 0;
ss: true
sy: true
replace接口有好几套不同的参数风格。
第1组replace
string& replace(size_t index, size_t len, const string& str);
string& replace(size_t index, size_t len, const char* str);
从index指定的位置开始,用str替换len这么长的子串。当len==0的时候,是插入的效果。len的值可以大于string对象的长度,但是index必须是合法值,支持传统C字符串,不支持单个字符。下面是测试代码:
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string ss = "abcdefg";
string rr = "123";
ss.replace(0,1,rr);
cout << ss << endl;
ss.replace(1,100,rr);
cout << ss << endl;
ss.replace(2, 0, rr);
cout << ss << endl;
ss.replace(3, 0, "abc");
cout << ss << endl;
ss.replace(4, 3, "d");
cout << ss << endl;
try {
ss.replace(100, 0, rr);
} catch(exception& e) {
cout << e.what() << endl;
return 0;
123bcdefg
1112323
111abc2323
111ad323
basic_string::replace: __pos (which is 100) > this->size() (which is 8)
第2组replace
string& replace(size_t index, size_t len, const string& str,
size_t str_index, size_t str_len);
string& replace(size_t index, size_t len, const string& str, size_t str_index);
string& replace(size_t index, size_t len, const char* str,
size_t str_index, size_t str_len);
string& replace(size_t index, size_t len, const char* str, size_t str_len);
与上一组接口相比,这一组replace增加一到两个参数,用于指定用于替换的str的index和len,即只把str的一部分用来进行替换。当使用string对象时,可以只指定index。当使用传统C字符串时,可以指定从指针位置开始的长度(为什么这两者要设计成不一样?)。下面是测试代码:
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string rr = "123";
string ss = "abcdefg";
ss.replace(0,1,rr,0,2);
cout << ss << endl;
ss.replace(4,0,rr,1,2);
cout << ss << endl;
string ss = "abcdefg";
ss.replace(0,1,"123",0,2);
cout << ss << endl;
ss.replace(4,0,"123",1,2);
cout << ss << endl;
string ss = "abcdefg";
ss.replace(0,1,rr,2);
cout << ss << endl;
ss.replace(4,0,"123",2);
cout << ss << endl;
return 0;
12bcdefg
12bc23defg
12bcdefg
12bc23defg
3bcdefg
3bcd12efg
第3组replace
string& replace(const_iterator i1, const_iterator i2, const string& str);
string& replace(const_iterator i1, const_iterator i2, const char* str);
string& replace(const_iterator i1, const_iterator i2, const char* str, size_t str_len);
这一组是迭代器(迭代器也是一种类型),i1到i2表达了一个范围,对应前面接口的index和len。很奇怪有一些接口在类型为string对象的时候,反而没有实现,是不是很少人会用到?!
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string rr = "123";
string ss = "abcdefghijklmn";
ss.replace(ss.begin()+3, ss.end()-2, rr);
cout << ss << endl;
string ss = "abcdefghijklmn";
ss.replace(ss.begin()+3, ss.end()-2, "123");
cout << ss << endl;
string ss = "abcdefghijklmn";
ss.replace(ss.begin()+3, ss.end()-2, "123", 2);
cout << ss << endl;
return 0;
abc123mn
abc123mn
abc12mn
第4组replace
string& replace(const_iterator i1, const_iterator i2, size_t n, char c);
string& replace(size_t index, size_t len, size_t n, char c);
这一组使用字符char了,当要用char来替换的时候,需要指定重复的次数。
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string ss = "abcdefghijklmn";
ss.replace(ss.begin()+3, ss.end()-2, 3, 'x');
cout << ss << endl;
string ss = "abcdefghijklmn";
ss.replace(0, 8, 3, 'x');
cout << ss << endl;
return 0;
abcxxxmn
xxxijklmn
用R
来表示raw string,必须要在最外层使用一组括号()
。
#include <iostream>
#include <string>
using namespace std;
int main(void) {
string s1{R"(abc\n123\n789)"};
cout << s1 << endl;
string s4{"abc\\n123\\n789"}; // same with above
cout << s4 << endl;
// special marco NOT_PRINT_FLAG, no use
string s2{R"NOT_PRINT_FLAG(1234567890)NOT_PRINT_FLAG"};
cout << s2 << " " << s2.size() << endl;
// real \n in raw string
string s3{R"(first
second
third)"};
cout << s3 << endl;
return 0;
C++标准string对象不提供trim接口,不知道这是什么考虑?自己造一个吧:
void trim(string& s) {
string whitespaces{"\t\n\r\f\v "};