Last Updated: 2024-01-02 11:09:40 Tuesday
-- TOC --
字符串的处理在代码中是高频操作,C语言定义字符串是一块以NULL结束的内存,相关函数接口很底层,error-prone。C++出现了string对象,可以在一个更高的抽象层面简洁高效地处理字符串。
string对象实际上是一个char类型的类似vector的容器,它被定义为:
std::basic_string<char>
string是类型为char的容器,因此具有C++容器的一些标准名称接口,同时它还具有方便处理字符串的接口。由于string本质上是存放char的容器,因此需要具备容器思维,C++保持了所有容器标准名称接口含义的一致性。比如reserve用来增加容器的capacity,但不改变size。这对于string对象也适用,此时就不能是C语言的思维,不能认为有了空间就可以任意赋值,对于string对象而言,要有了size才能任意赋值。比如:
下面是有问题的代码片段:
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';
同时,C++的string对象也保持了与C一致的mental picture,访问string对象的任意位置,如使用[]
时,得到的是char类型数据,而char类型数据如果参与计算,就是int。
// #include <string>
string s1 { "0123456" };
string s2 { s1 };
string s3; // empty string
string s4 { 'a' }; // "a"
string s41(1, 'a'); // "a"
string s5(5, 'c'); // "ccccc"
string s6("0123456", 3); // "012", [0:3]
string s7(s1, 3); // "3456", [3:]
string s8 { s1+"789" }; // "0123456789"
从char数组到string对象
这是一种有风险的创建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.size() << endl;
return 0;
}
输出:
01 2
由于char数组中间有一个0x00,导致创建的string对象长度仅为2。
加法拼接
检讨一下这一行错误代码:s8 = 'g' + "890" + s1
,错误的原因是,这两个加法,谁先谁后是不确定的。C++中有一个术语,叫做evaluation order
,说的就是这个事儿。In general, C++ has no clearly specified execution order for operands。
类似的错误还有
b = ++a + a
,b的值也是不确定的。
to_string,转string对象
这是个真方便的接口,将很多常用的类型转换成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;
}
坑,to_string不支持char类型参数,char类型会被当成int处理。
to_string('a'); // "97"
string s41(1, 'a'); // "a"
string suffix "s"
"s"
后缀,是定义在stdlib中的特质string对象的后缀。
#include <iostream>
#include <string>
#include <type_traits>
using namespace std;
int main(void) {
auto a { "12345" };
static_assert(is_same_v<decltype(a),const char*>);
auto b { "abcde"s };
static_assert(is_same_v<decltype(b),string>);
cout << a << b << endl;
return 0;
}
对于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;
}
示例:
#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
异常。可直接使用==
和!=
来比较两个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;
}
输出:
2
9
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;
}
输出:
6
8
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;
}
输出:
0
13
10
5
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接口有好几套不同的参数风格。
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
1123
1112323
111abc2323
111ad323
basic_string::replace: __pos (which is 100) > this->size() (which is 8)
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
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
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 "};
s.erase(s.find_last_not_of(whitespaces)+1);
s.erase(0, s.find_first_not_of(whitespaces));
}
当输入的s为空串时,两个find接口都返回string:npos
,以上代码依然能够成功执行。string:npos+1 == 0
!
本文链接:https://cs.pynote.net/sf/c/cpp/202209031/
-- EOF --
-- MORE --