C++编程语言:标准库:第36章——字符串类(Bjarne Stroustrup)
第 36 章 字符串类(Strings)
目录
36.1 引言
36.2 字符分类
36.2.1 分类识别函数(Classification Functions)
36.2.2 字符特征类(Chracter Traits)
36.3 字符串类(basic_string)
36.3.1 string对比于C风格字符串
36.3.2 构造函数
36.3.3 基本运算
36.3.4 字条串I/O操作
36.3.5 数值转换(Numeric Conversions)
36.3.6 类STL运算(STL-Like Operations)
36.3.7 find函数族(The find Family)
36.3.8 子字符串(Substrings)
36.4 建议(Advice)
36.1 引言
标准库在 <cctype> (§36.2) 中提供了字符分类操作,在 <string> (§36.3) 中提供了字符串及其相关操作,在 <regex> (第 37 章) 中提供了正则表达式匹配,并在 <cstring> (§43.4) 中提供了对 C 风格字符串的支持。第 39 章讨论了如何处理不同的字符集、编码和约定(语言环境)。
§19.3 中给出了简化的字符串实现。
36.2 字符分类
标准库提供了分类函数来帮助用户操作字符串(和其他字符序列),以及指定字符类型属性的特征,以帮助实现字符串操作。
36.2.1 分类识别函数(Classification Functions)
字符分类识别函数 | |
isspace(c) | 判断c是否为空白(空格‘’,水平制表符(tab)(跳格)‘\t’,换行符‘\n’, 垂直制表符(跳格)‘\v’,换页(form feed) ‘\f’,回车(carriage return)‘\f’)。 |
isalpha(c) | 判断c是否为字母( 'a'..'z', 'A'..'Z' ),注意,不要用下划线‘_’。 |
isdigit(c) | 判断c是否为一个十进制数( '0'…'9') |
isxdigit(c) | 判断c是否为一个十六进制数(十进制数或 'a'..'f' 或 'A'..'F') |
isupper(c) | 判断c是否为一个大写字母 |
islower(c) | 判断c是否为一个小写字母 |
isalnum(c) | isalpha(c)或isdigit(c) |
iscntrl(c) | 判断c是否为一个控制字符ASCII 0..31 和 127 |
ispunct(c) | 判断c是否为一个字母,数字,空白字符,或不可见控制字符 |
isprint(c) | 判断c是否为可打印(ASCII ‘’..‘~’’) |
isgraph(c) | 判断是否为isalpha(c)或isdigit(c)或ispunct(c),注意,没有空白。 |
此外,标准库提供了两个大小写转换函数。
大小写转换函数 | |
toupper(c) | c转为大写 |
tolower(c) | c转为小写 |
宽字符等效的函数在 <cwctype> 中提供。
字符分类函数对“C”语言环境(§39.5.1,§39.5.2)敏感。其他语言环境的等效函数在 <locale>(§39.5.1)中提供。
这些字符分类函数之所以有用,原因之一是字符分类可能比看起来更棘手。例如,新手可能会写出:
if ('a'<ch && ch<'z') // a character
这比以下更冗长(并且很可能更慢):
if (islower(ch)) // a lowercase character
此外,无法保证字符在代码空间中是连续的。另有,使用标准字符分类更容易转换为其他语言环境:
if (islower,danish) // a lowercase character in Danish(丹麦语)
// (假设 ‘‘danish’’ 一个丹麦语本地化的名称)
请注意,丹麦语比英语多三个小写字符,因此使用“a”和“z”的初始显式测试是完全错误的。
36.2.2 字符特征类(Chracter Traits)
如§23.2所示,字符串模板原则上可以使用任何具有适当复制操作的类型作为其字符类型。但是,对于没有用户定义复制操作的类型,可以提高效率并简化实现。因此,标准字符串要求用作其字符类型的类型必须是POD(§8.2.6)。这也有助于使字符串的I/O操作简单高效。
字符类型的属性由其 char_traits 定义。char_traits 是模板的特化:
template<typename C> struct char_traits { };
所有 char_traits 均定义在 std 中,标准 char_traits 则在 <string> 中呈现。通用 char_traits 本身没有属性;只有针对特定字符类型的 char_traits 特化才具有属性。以 char_traits<char> 为例:
template<>
struct char_traits<char> { // char_traits operations should not throw exceptions
using char_type = char;
using int_type = int; // type of integer value of character
using off_type = streamoff; // offset in stream
using pos_type = streampos; // position in stream
using state_type = mbstate_t; // multibyte stream state (§39.4.6)
// ...
};
该标准提供了 char_traits 的四种专业化(§iso.21.2.3):
template<> struct char_traits<char>;
template<> struct char_traits<char16_t>;
template<> struct char_traits<char32_t>;
template<> struct char_traits<wchar_t>;
标准 char_traits 的成员都是static函数:
char_traits<C> static 成员 (§iso.21.2) | |
c=to_char_type(i) | int_type 到 char_type 的转换 |
i=to_int_type(c) | char_type 到 int_type 的转换 |
eq_int_type(c,c2) | to_int_type(c)==to_int_type(c2) |
eq(c,c2) | c 是否被视为等于 c2? |
lt(c,c2) | c 是否被视为小于 c2? |
i=compare(p,p2,n) | [p:p+n) 和 [p2:p2+n) 的词典比较 |
assign(c,c2) | 对于 char_type,c=c2 |
p2=assign(p,n,c) | 将 n 个 c 副本分配给 [p:p+n); p2=p |
p3=move(p,p2,n) | 将 [p:p+n) 复制到 [p2:p2+n);[p:p+n) 和 [p2:p2+n) 可能重叠;p3=p |
p3=copy(p,p2,n) | 将 [p:p+n) 复制到 [p2:p2+n);[p:p+n) 和 [p2:p2+n) 不得重叠;p3=p |
n=length(p) | n 是 [p:q) 中的字符数,其中 ∗q 是第一个元素,因此 eq(q,charT{}) |
p2=find(p,n,c) | p 指向 [p:p+n) 中 c 的第一次出现,或者 nullptr |
i=eof() | i 是表示文件结束的 int_type 值 |
i=not_eof(i) | 若 !eq_int_type(i,eof()) 测结果为i; 否则 i 可以是任何不等于 eof() 的值 |
与 eq() 的比较通常不仅仅是简单的 == 。例如,不区分大小写的 char_traits 会定义其 eq(),这样 eq('b','B') 就会返回 true。
由于 copy() 不能防止重叠范围,因此它可能比 move() 更快。
compare() 函数使用 lt() 和 eq() 来比较字符。它返回一个 int 值,其中 0 表示完全匹配,负数表示第一个参数按字典顺序位于第二个参数之前,正数表示第一个参数位于第二个参数之后。
I/O 相关函数用于低级 I/O(§38.6)的实现。
36.3 字符串类(basic_string)
在 <string> 中,标准库提供了一个通用的字符串模板basic_string:
template<typename C,
typename Tr = char_traits<C>,
typename A = allocator<C>>
class basic_string {
public:
using traits_type = Tr;
using value_type = typename Tr::char_type;
using allocator_type = A;
using size_type = typename allocator_traits<A>::size_type;
using difference_type = typename allocator_traits<A>::difference_type;
using reference = value_type&;
using const_reference = const value_type&;
using pointer = typename allocator_traits<A>::pointer;
using const_pointer = typename allocator_traits<A>::const_pointer;
using iterator = /* implementation-defined */;
using const_iterator = /* implementation-defined */;
using reverse_iterator = std::reverse_iterator<iterator>;
using const_reverse_iterator = std::reverse_iterator<const_iterator>;
static const size_type npos = −1; // integer representing end-of-string
// ...
};
元素(字符)连续存储,以便底层输入操作可以安全地使用 basic_string 的字符序列作为源或目标。
basic_string 提供了强有力的保证(§13.2):如果 basic_string 操作抛出异常,则字符串保持不变。
针对一些标准字符类型提供了特化功能:
using string = basic_string<char>;
using u16string = basic_string<char16_t>;
using u32string = basic_string<char32_t>;
using wstring = basic_string<wchar_t>;
所有这些字符串都提供了大量的操作。
与容器(第 31 章)类似,basic_string 不应用作基类,而是提供移动语义,以便可以高效地按值返回。
36.3.1 string对比于C风格字符串
我假设读者已经通过本书中的众多示例对string有所了解,因此我首先从几个示例开始来对比string的使用和 C 风格字符串(§43.4),C 风格字符串在主要熟悉 C 和 C 风格 C++ 的程序员中很受欢迎。
考虑通过连接用户标识符和域名来组成电子邮件地址:
string address(const string& identifier, const string& domain)
{
return identifier + '@' + domain;
}
void test()
{
string t = address("bs","somewhere");
cout << t << '\n';
}
这很简单。现在考虑一个合理的 C 风格版本。C 风格字符串是一个指向以0结尾的字符数组的指针。用户控制分配并负责释放:
char∗ address(const char∗ identifier, const char∗ domain)
{
int iden_len = strlen(identifier);
int dom_len = strlen(domain);
char∗ addr = (char∗)malloc(iden_len+dom_len+2); // remember space for 0 and '@'
strcpy(identifier,addr);
addr[iden_len] = '@';
strcpy(domain,addr+iden_len+1);
return addr;
}
void test2()
{
char∗ t = address("bs","somewhere");
printf("%s\n",t);
free(t);
}
我写对了吗?希望如此。至少它给出了我预期的输出。像大多数经验丰富的 C 程序员一样,我第一次就写对了 C 版本(我希望如此),但还有很多细节需要完善。然而,经验(例如错误日志)表明情况并非总是如此。通常,这种简单的编程任务会交给相对新手,他们还不了解所有必要的技巧。C 风格的 address() 函数的实现包含许多棘手的指针操作,并且它的使用需要调用者记住释放返回的内存。你更愿意维护哪段代码?
有时,人们声称 C 风格的字符串比 string的字符串更高效。然而,在大多数情况下,string的分配和释放操作比 C 风格的等效字符串更少(这得益于小字符串优化和移动语义;§19.3.3,§19.3.1)。此外,strlen() 是一个 log(N) 运算,而 string::size() 只是一个简单的读取操作。在示例中,这意味着 C 风格的代码会遍历每个输入字符串两次,而字符串版本对每个输入只进行一次遍历。这种级别的效率考量往往是错误的,但string版本却具有根本性的优势(edge)。
C 风格字符串和string的根本区别在于,string是一种具有约定俗成的语义的固有类型,而 C 风格字符串则是由一些实用函数支持的一组约定。考虑赋值和比较:
void test3()
{
string s1 = "Ring";
if (s1!="Ring") insanity();
if (s1<"Opera")cout << "check";
string s2 = address(s1,"Valkyrie");
char s3[] = "Ring";
if (strcmp(s3,"Ring")!=0) insanity();
if (strcmp(s3,"Opera")<0) cout << "check";
char∗ s4 = address(s3,"Valkyrie");
free(s4);
}
最后,考虑排序:
void test4()
{
vector<string> vs = {"Grieg", "Williams", "Bach", "Handel" };
sort(vs.begin(),vs.end()); // assuming that I haven’t defined sort(vs)
const char∗ as[] = {"Grieg", "Williams", "Bach", "Handel" };
qsort(as,sizeof(∗as),sizeof(as)/sizeof(∗as),(int(∗)(const void∗,const void∗))strcmp);
}
C 风格的字符串排序函数 qsort() 在 §43.7 中介绍。同样,sort() 的速度与 qsort() 一样快(通常甚至快得多),因此没有必要为了性能而选择更低级、更冗长且更难维护的编程风格。
36.3.2 构造函数
basic_string 提供了多种令人眼花缭乱的构造函数:
basic_string<C,Tr,A>构造函数(iso.21.4.2) x可以为一个basic_string,一个C风格字符串,或者一个initializer_list<char_type> | |
basic_string s {a}; | s 是带有分配器 a 的空字符串;explicit |
basic_string s {}; | 默认构造函数:basic_string s {A{}}; |
basic_string s {x,a}; | s 从 x 获取字符;使用分配器 a |
basic_string s {x}; | 移动和复制构造函数: basic_string s {x,A{}}; |
basic_string s {s2,pos,n,a}; | s 获取字符 s2[pos:pos+n);使用分配器 a |
basic_string s {s2,pos,n}; | basic_string s {s2,pos,n,A{};} |
basic_string s {s2,pos}; | basic_string s {s2,pos,string::npos,A{}}; |
basic_string s {p,n,a}; | 从 [p:p+n] 初始化 s;p 是 C 风格字符串;使用分配器 a |
basic_string s {p,n}; | basic_string s {p,n,A{}}; |
basic_string s {n,c,a}; | s 保存字符 c 的 n 个副本;使用分配器 a |
basic_string s {n,c}; | basic_string s {n,c,A{};} |
basic_string s {b,e,a}; | s 从 [b:e] 获取字符;使用分配器 a |
basic_string s {b,e}; | basic_string s {b,e,A{}}; |
s.˜basic_string() | 析构函数:释放所有资源 |
s=x | 复制:s从x获得字条 |
s2=move(s) | 移动:s2 从 s 中获取字符;noexcept |
最常见的(构造函数)变体也是最简单的:
string s0; // 空string
string s1 {"As simple as that!"}; // 从C风格字条串构造string
string s2 {s1}; // 复制构造函数
析构函数几乎总是被隐式调用。
没有只接受若干数量元素的字符串构造函数:
string s3 {7}; // error : no string(int)
string s4 {'a'}; // error : no string(char)
string s5 {7,'a'}; // OK: 7 'a's
string s6 {0}; // 当心: 传递 nullptr
s6 的声明显示了习惯于 C 风格字符串的程序员有时会犯的一个错误:
const char∗ p = 0; // 置p为 ‘‘no string’’
遗憾的是,编译器无法捕获 s6 的定义,或者 const char∗ 持有 nullptr 的更糟糕的情况:
string s6 {0}; // 当心: 传递 nullptr
string s7 {p}; // 可以或不可以,取决于 p 的值
string s8 {"OK"}; // OK: 传递C风格字符串的指针
不要尝试用 nullptr 初始化字符串。轻则会导致严重的运行时错误,重则会导致难以捉摸的未定义行为。
如果你尝试构造一个字符串,其字符数超出了你的实现所能处理的范围,构造函数就会抛出 std::length_error。例如:
string s9 {string::npos,'x'}; // throw length_error
值 string::npos 表示超出字符串长度的位置,通常用于表示“字符串的末尾”。例如:
string ss {"Fleetwood Mac"};
string ss2 {ss,0,9}; // "Fleetwood"
string ss3 {ss,10,string::npos}; // "Mac"
请注意,子字符串符号是 (位置,长度) 而不是 [开始,结束)。
没有string类型的字面量(literals)。可以使用用户自定义字面量(§19.2.6),例如“The Beatles”和“Elgar”。请注意 s 后缀。
36.3.3 基本运算
basic_string 提供比较、大小和容量控制以及访问操作。
basic_string<C,Tr,A>比较(iso.21.4.8) s 或 s2 不能同时是 C 风格字符串 | |
s==s2 | s 是否等于 s2?使用 traits_type 比较字符值 |
s!=s2 | !(s==s2) |
s<s2 | s 是否按字典顺序位于 s2 之前? |
s<=s2 | 按字典顺序排列,s 是否在 s2 之前或等于 s2? |
s>s2 | s 是否按字典顺序位于 s2 之后? |
s>=s2 | 按字典顺序排列,s 是否在 s2 之后或等于 s2? |
有关更多比较操作,请参阅§36.3.8。
basic_string 的大小和容量机制与vector(§31.3.3)相同:
basic_string<C,Tr,A>大小和容量(iso.21.4.4) | |
n=s.size() | n 是 s 中字符的数量。 |
n=s.length() | n=s.size() |
n=s.max_size() | n是s.size()的最大可能值。 |
s.resize(n,c) | 使 s.size()==n ;获得 c 的值并加入s |
s.resize(n) | s.resiz e(n,C{}) |
s.reserve(n) | 确保 s 可以容纳 n 个字符而无需进一步分配 |
s.reserve() | 无效:s.reserve(0) |
n=s.capacity() | s 可以容纳 n 个字符,无需进一步分配 |
s.shrink_to_fit() | 使得 s.capacity==s.size() |
s.clear() | 清空s |
s.empty() | s是否为空 |
a=s.get_allocator() | a 是s的分配器 |
resize() 或 reserve() 会导致 size() 超过 max_size(),从而引发 std::length_error。
resize() 或 reserve() 会导致 size() 超过 max_size(),从而引发 std::length_error。
一个例子:
void fill(istream& in, string& s, int max)
// 使用 s 作为底层输入的目标(简化)
{
s.reserve(max); // make sure there is enough allocated space
in.read(&s[0],max);
const int n = in.gcount(); // number of characters read
s.resize(n);
s.shrink_to_fit(); //discard excess capacity
}
这里,我“忘记”利用已读字符数了。真是太草率了。
basic_string<C,Tr,A>访问(iso.21.4.5) | |
s[i] | 下标:s[i] 是 s 中第 i 个元素的引用;没有范围检查 |
s.at(i) | 下标:s.at(i) 是 s 中第 i 个元素的引用;如果 s.size()<=i,则抛出 range_error |
s.front() | s[0] |
s.back() | s[s.size()−1] |
s.push_back(c) | 追加字符c |
s.pop_back() | 从s中移除最后一个字符:s.erase(s.size()−1) |
s+=x | 将 x 追加到 s 的末尾;x 可以是字符、字符串、C 风格字符串或初始化列表 <char_type> |
s=s1+s2 | 连接:s=s1;s+=s2 的优化版本; |
n2=s.copy(s2,n,pos) | s 从 s2[pos:n2) 中获取字符 其中 n2 为 min(n,s.size()−pos); 如果 s.size()<pos,则抛出 out_of_range 错误 |
n2=s.copy(s2,n) | s 从 s2 中获取所有字符;n=s.copy(s2,n,0) |
p=s.c_str() | p 是 s 中字符的 C 风格字符串版本(以零结尾); const C∗ |
p=s.data() | p=s.c_str() |
s.swap(s2) | 交换 s 和 s2 的值;noexcept |
swap(s,s2) | s.swap(s2) |
使用 at() 进行超出范围的访问会抛出 std::out_of_range。如果 +=()、push_back() 或 + 导致 size() 超过 max_size(),则会抛出 std::length_error。
字符串无法隐式转换为 char∗ 。这种做法在很多地方都尝试过,但发现容易出错。因此,标准库提供了显式转换函数 c_str() 来将 string 转换为 const char∗ 。
字符串可以包含零值字符(例如 '\0')。使用 strcmp() 等函数,如果该函数对包含零字符的字符串执行 s.c_str() 或 s.data() 的结果假定遵循 C 风格字符串约定,则可能会引发意外结果。
36.3.4 字条串I/O操作
可以使用 << (§38.4.2) 输出 basic_string,并使用 >> (§38.4.1) 读入:
basic_string<C,Tr,A>I/O操作(iso.21.4.8.9) | |
in>>s | 从in读一个空白分隔的字到s中 |
out<<s | 输出s到out |
getline(in,s,d) | 将 in 中的字符读入 s 中,直到遇到字符 d;d 从 in 中移除,但不会附加到 s |
getline(in,s) | getline(in,s,'\n') 其中 '\n' 被加宽以匹配字符串的字符类型 |
导致 size() 超过 max_size() 的输入操作将抛出 std::length_error。
getline() 会从输入流中删除其终止符(默认为 '\n'),但不会将其输入到字符串中。这简化了行处理。例如:
vector<string> lines;
for (string s; getline(cin,s);)
lines.push_back(s);
字符串 I/O 操作都会返回其输入流的引用,以便可以链式执行操作。例如:
string first_name;
string second_name;
cin >> first_name >> second_name;
输入操作的字符串目标在读取前设置为空,并扩展以容纳读取的字符。读取操作也可以通过到达文件末尾(§38.3)来终止。
36.3.5 数值转换(Numeric Conversions)
在 <string> 中,标准库提供了一组函数,用于从字符串或 wstring(注意:不是 basic_string<C,Tr,A>)中的字符表示中提取数值。所需的数值类型已编码在函数名称中:
数值转换(iso.21.5) s 可以是string或 wstring | |
x=stoi(s,p,b) | 字符串到 int;x 是整数;如果 p!=nullptr,则从 s[0] 开始读取,∗p 设置为用于 x 的字符数;b 是数字的基数(介于 2 和 36 之间,包括 2 和 36) |
x=stoi(s,p) | x=stoi(s,p,10);十进制数 |
x=stoi(s) | x=stoi(s,nullptr,10);十进制数;不报告字符数 |
x=stol(s,p,b) | 字符串转为long |
x=stoul(s,p,b) | 字符串转为 unsigned long |
x=stoll(s,p,b) | 字符串转为 long long |
x=stoull(s,p,b) | 字符串转为 unsigned long long |
x=stof(s,p) | 字符串转为float |
x=stod(s,p) | 字符串转为double |
x=stold(s,p) | 字符串转为long double |
s=to_string(x) | s 是 x 的字符串表示形式;x 必须是整数或浮点数 |
ws=to_wstring(x) | s 是 x 的 wstring 表示形式;x 必须是整数或浮点数 |
这些 sto∗ (* indicates String to xxx(some other types)) 函数都有三个变体,例如 stoi。例如:
string s = "123.45";
auto x1 = stoi(s); // x1 = 123
auto x2 = stod(s); // x2 = 123.45
sto∗ 函数的第二个参数是一个指针,用于表示在字符串中搜索数值的进度(译注:搜索到哪个位置)。例如:
string ss = "123.4567801234";
size_t dist = 0; // 存放已读字符数
auto x = stoi(ss,&dist); // x = 123 (整数)
++dist; //忽略小数这个点
auto y = stoll(&ss[dist]); // x = 4567801234 (a long long)
这不是我最喜欢的从字符串解析多个数的接口。我更喜欢使用 string_stream (§38.2.2)。
开头的空格会被跳过。例如:
string s = " 123.45";
auto x1 = stoi(s); // x1 = 123
基数参数的范围为 [2:36],其中 0123456789abcdefghijklmnopqrstuvwxyz 用作“数字”,其值由其在此序列中的位置决定。任何超出基数(base value)的数值都将被视为错误或扩展。例如:
string s4 = "149F";
auto x5 = stoi(s4); // x5 = 149
auto x6 = stoi(s4,nullptr,10); // x6 = 149
auto x7 = stoi(s4,nullptr,8); // x7 = 014
auto x8 = stoi(s4,nullptr,16); // x8 = 0x149F
string s5 = "1100101010100101"; // binar y
auto x9 = stoi(s5,nullptr,2); // x9 = 0xcaa5
如果转换函数在其字符串参数中找不到可以转换为数字的字符,则会抛出 invalid_argument 异常。如果转换函数发现目标类型无法表示的数字,则会抛出 out_of_range 异常;此外,转换为浮点类型时,errno 会被设置为 ERANGE (§40.3)。例如:
stoi("Hello, World!"); // throws std::invalid_argument
stoi("12345678901234567890"); // throws std::out_of_range; errno=ERANGE
stof("123456789e1000"); // throws std::out_of_range; errno=ERANGE
sto∗ 函数在其名称中编码了目标类型。这使得它们不适合用于目标可以作为模板参数的通用代码。在这种情况下,请考虑使用 to<X> (§25.2.5.1)。
36.3.6 类STL运算(STL-Like Operations)
basic_string 提供了常用的迭代器集:
basic_string<C,Tr,A> 字符串迭代器(§iso.21.4.3) 所有的操作都是noexcept的 | |
p=s.begin() | p 是指向 s 第一个字符的迭代器 |
p=s.end() | p 是指向 s 末尾之后的迭代器 |
p=s.cbegin() | p 是指向第一个字符的 const_iterator |
p=s.cend() | p 是指向 s 末尾之后的 const_iterator |
p=s.rbegin() | p 是 s 的逆序列的开头 |
p=s.rend() | p 是 s 的逆序列的结尾 |
p=s.crbegin() | p 是指向 s 反向序列开头的 const_iterator |
p=s.crend() | p 是指向 s 反向序列末尾的 const_iterator |
由于string具有所需的成员类型以及获取迭代器的函数,因此可以与标准算法(第 32 章)一起使用。例如:
void f(string& s)
{
auto p = find_if(s.begin(),s.end(),islower);
// ...
}
最常见的string操作都直接由 string 提供。希望这些版本能够针对string进行优化,使其超越一般算法的易操作性。
标准算法(第 32 章)对字符串的适用性不如人们想象的那么强。一般算法倾向于假设容器的元素在孤立的情况下是有意义的。但对于字符串来说,情况通常并非如此。
basic_string 提供复杂的assignment():
basic_string<C,Tr,A> 赋值(§iso.21.4.6.3) 所有操作都返回应用它们的字符串 | |
s.assign(x) | s=x; x 可以是字符串、C 风格的字符串或 initializer_list<char_type> |
s.assign(move(s2)) | 移动:s2 是一个字符串;noexcept |
s.assign(s2,pos,n) | s 获取字符 s2[pos:pos+n) |
s.assign(p,n) | s 获取字符 [p:p+n);p 是 C 风格字符串 |
s.assign(n,c) | s 获取字符 c 的 n 个副本 |
s.assign(b,e) | s 获取来自 [b:e) 的字符 |
我们可以在 basic_string 中insert(),append()和erase() :
basic_string<C,Tr,A> 插入和删除(§iso.21.4.6.2,§iso.21.4.6.4,§iso.21.4.6.5) 所有操作都返回应用它们的字符串 | |
s.append(x) | 将 x 附加到 s 的末尾;x 可以是字符、string、C 风格字符串或initializer_list<char_type> |
s.append(b,e) | 在 s 末尾附加 [b:e) |
s.append(s2,pos,n) | 将 s2[pos:pos+n) 附加到 s 的末尾 |
s.append(p,n) | 将字符 [p:p+n) 附加到 s 的末尾;p 是 C 风格字符串 |
s.append(n,c) | 将字符 c 的 n 个副本附加到 s 的末尾 |
s.insert(pos,x) | 在 s[pos] 之前插入 x;x 可以是字符、字符串、C 风格字符串或 initializer_list<char_type> |
s.insert(p,c) | 在迭代器 p 之前插入 c |
s.insert(p,n,c) | 在迭代器 p 之前插入 n 个 c 的副本 |
insert(p,b,e) | 在迭代器 p 之前插入 [b:e) |
s.erase(pos) | 从 s 中删除尾随字符,以 s[pos] 开头;s.size() 变为 pos |
s.erase(pos,n) | 从 s 中删除 n 个字符,从 s[pos] 开始;s.size() 变为 max(pos,s.size()−n) |
例如:
void add_middle(string& s, const string& middle) // add middle name
{
auto p = s.find(' ');
s.insert(p,' '+middle);
}
void test()
{
string dmr = "Dennis Ritchie";
add_middle(dmr,"MacAlistair");
cout << dmr << '\n';
}
对于vector,append()(在末尾添加字符)通常比在其他地方insert()更有效。
下面我使用 s[b:e) 来表示 s 中元素 [b:e) 的序列:
basic_string<C,Tr,A> 替换(§iso.21.4.6.6) 所有操作都返回应用它们的字符串 | |
s.replace(pos,n,s2,pos2,n2) | 用 s2[pos2:pos2+n2) 替换s[pos:pos+n); s2是一个string或C风格字符串 |
s.replace(pos,n,p,n2) | 用 [p:p+n2)替换s[pos:pos+n); |
s=s.replace(pos,n,s2) | 用 s2 替换s[pos:pos+n); s2是一个string或C风格字符串 |
s.replace(pos,n,n2,c) | 将 s[pos:pos+n) 替换为字符 c 的 n2 个副本 |
s.replace(b,e,x) | 将 [b:e) 替换为 x;x 可以是字符串、C 风格字符串或 initializer_list<char_type> |
s.replace(b,e,p,n) | 将 [b:e) 替换为 [p:p+n) |
s.replace(b,e,n,c) | 将 [b:e) 替换为 n 个字符 c |
s.replace(b,e,b2,e2) | 将 [b:e) 替换为 [b2:e2) |
replace() 函数会将一个子字符串替换为另一个子字符串,并相应地调整字符串的大小。例如:
void f()
{
string s = "but I have heard it works even if you don't believe in it";
s.replace(0,4,""); //erase initial "but "
s.replace(s.find("even"),4,"only");
s.replace(s.find(" don't"),6,""); // erase by replacing with ""
assert(s=="I have heard it works only if you believe in it");
}
依赖于“神奇”常量(例如要替换的字符数)的代码容易出错。
replace() 返回调用它的对象的引用。这可以用于链式操作:
void f2()
{
string s = "but I have heard it works even if you don't believe in it";
s.replace(0,4,"").replace(s.find("even"),4,"only").replace(s.find(" don't"),6,"");
assert(s=="I have heard it works only if you believe in it");
}
36.3.7 find函数族(The find Family)
用于查找子字符串的函数种类繁多,令人眼花缭乱。通常,find() 从 s.begin() 开始向前搜索,而 rfind() 则从 s.end() 开始向后搜索。find 函数使用 string::npos(“非位置”)表示“未找到”。
basic_string<C,Tr,A> 查找元素(§iso.21.4.7.2) x 可以是字符,string或 C 风格字符串。所有操作均为 noexcept | |
pos=s.find(x) | 在 s 中查找 x;pos 是找到的第一个字符的索引或 string::npos |
pos=s.find(x,pos2) | pos=find(basic_string(s,pos2) |
pos=s.find(p,pos2,n) | pos=s.find(basic_string{p,n},pos2) |
pos=s.rfind(x,pos2) | 在 s[0:pos2] 中查找 x;pos 是 x 中距离 s 末尾最近的第一个字符的位置,或 string::npos |
pos=s.rfind(x) | pos=s.rfind(p,string::npos) |
pos=s.rfind(p,pos2,n) | pos=s.rfind(basic_string{p,n},pos2) |
例如:
void f()
{
string s {"accdcde"};
auto i1 = s.find("cd"); // i1==2 s[2]=='c' && s[3]=='d'
auto i2 = s.rfind("cd"); // i2==4 s[4]=='c' && s[5]=='d'
}
find∗of() 函数与 find() 和 rfind() 函数的不同之处在于,它查找的是单个字符,而不是整个字符序列:
basic_string<C,Tr,A> 查找来自于集合的元素(§iso.21.4.7.4) x 可以是字符,string或 C 风格字符串;p是C风格字符串。所有操作均为 noexcept | |
pos2=s.find_first_of(x,pos) | 在 s[pos:s.size()) 中查找 x 中的一个字符; pos2 是 x 中第一个字符在 s[pos:s.size()) 或 string::npos 中的位置 |
pos=s.find_first_of(x) | pos=s.find_first_of(s2,0) |
pos2=s.find_first_of(p,pos,n) | pos2=s.find_first_of(pos,basic_string{p,n}) |
pos2=s.find_last_of(x,pos) | 在 s[0:pos) 中查找 x 中的一个字符; pos2 是 x 中距离 s 或 string::npos 末尾最近的字符的位置 |
pos=s.find_last_of(x) | pos=s.find_first_of(s2,0) |
pos2=s.find_last_of(p,pos,n) | pos2=s.find_last_of(pos2,basic_string{p,n}) |
pos2=s.find_first_not_of(x,pos) | 在 s[pos:s.size()) 中查找不属于 x 的字符; pos2 是 x 中第一个不属于 s[pos:s.size()) 或 string::npos 的字符的位置 |
pos=s.find_first_not_of(x) | pos=s.find_first_not_of(s2,0) |
pos2=s.find_first_not_of(p,pos,n) | pos2=s.find_first_not_of(pos,basic_string{p,n}) |
pos2=s.find_last_not_of(x,pos) | 在 s[0:pos) 中查找不属于 x 的字符; pos 是 x 中距离 s 末尾最近的字符的位置,或 string::npos |
pos=s.find_last_not_of(x) | pos=s.find_first_not_of(s2,0) |
pos2=s.find_last_not_of(p,pos,n) | pos=s.find_last_not_of(pos,basic_string{p,n}) |
例如:
string s {"accdcde"};
auto i1 = s.find("cd"); // i1==2 s[2=='c' && s[3]=='d'
auto i2 = s.rfind("cd"); // i2==4 s[4]=='c' && s[5]=='d'
auto i3 = s.find_first_of("cd"); // i3==1 s[1]=='c'
auto i4 = s.find_last_of("cd"); // i4==5 s[5]=='d'
auto i5 = s.find_first_not_of("cd"); // i5==0 s[0]!='c' && s[0]!='d'
auto i6 = s.find_last_not_of("cd"); // i6==6 s[6]!='c' && s[6]!='d'
36.3.8 子字符串(Substrings)
basic_string 提供了子字符串的底层概念:
basic_string<C,Tr,A> 子字符串(§iso.21.4.7.8) | |
s2=s.substr(pos,n) | s2=basic_string(&s[pos],m) 其中m=min(s.size()−n,n) |
s2=s.substr(pos) | s2=s.substr(pos,string::npos) |
s2=s.substr() | s2=s.substr(0,string::npos) |
注意,substr() 创建了一个新的字符串对象:
void user()
{
string s = "Mary had a little lamb";
string s2 = s.substr(0,4); // s2 == "Mary"
s2 = "Rose"; // 不影响 s
}
我们可以比较子字符串:
basic_string<C,Tr,A> 比较(§iso.21.4.7.9) | |
n=s.compare(s2) | 对 s 和 s2 进行字典顺序比较;使用 char_traits<C>::compare() 进行比较;如果 s==s2,则 n=0;如果 s<s2,则 n<0;如果 s2>s,则 n>0;noexcept; |
n2=s.compare(pos,n,s2) | n2=basic_string{s,pos,n}.compare(s2) |
n2=s.compare(pos,n,s2,pos2,n2) | n2=basic_string{s,pos,n}.compare(basic_string{s2,pos2,n2}) |
n=s.compare(p) | n=compare(basic_string{p}); p是一个C风格字符串 |
n2=s.compare(pos,n,p) | n2=basic_string{s,pos,n}.compare(basic_string{p}); p是一个C风格字符串 |
n2=s.compare(pos,n,p,n2) | n2=basic_string{s,pos,n}.compare(basic_string{p,n2}); p是一个C风格字符串 |
例如:
void f()
{
string s = "Mary had a little lamb";
string s2 = s.substr(0,4); // s2 == "Mary"
auto i1 = s.compare(s2); // i1 is positive
auto i2 = s.compare(0,4,s2); // i2==0
}
明确使用常量来表示位置和长度是脆弱且容易出错的。
36.4 建议(Advice)
[1] 使用字符分类判断函数,而不是手动验证字符范围;§36.2.1。
[2] 如果实现类似字符串的抽象,请使用 character_traits 来实现字符操作;§36.2.2。
[3] basic_string 可用于创建任何类型的字符串;§36.3。
[4] 将string用作变量和成员,而不是基类;§36.3。
[5] 优先使用string操作,而不是 C 风格的字符串函数;§36.3.1。
[6] 按值返回string(依赖于移动语义);§36.3.2。
[7] 使用 string::npos 表示“字符串的其余部分”;§36.3.2。
[8] 不要将 nullptr 传递给期望 C 风格字符串的string函数;§36.3.2。
[9] string可以根据需要增长和收缩;§36.3.3。
[10] 当需要进行范围检查时,请使用 at() 而不是迭代器或 [];§36.3.3,§36.3.6。
[11] 当需要优化速度时,请使用迭代器和 [] 而不是 at();§36.3.3,§36.3.6。
[12] 如果使用string,请在某处捕获 length_error 和 out_of_range 错误;§36.3.3。
[13] 仅在必要时使用 c_str() 生成字符串的 C 风格字符串表示;§36.3.3。
[14] string输入对类型敏感,不会溢出;§36.3.4。
[15] 优先使用 string_stream 或通用值提取函数(例如 to<X>),而不是直接使用 str∗ 数值转换函数; §36.3.5。
[16] 使用 find() 操作定位字符串中的值(而不是编写显式循环);§36.3.7。
[17] 直接或间接地使用 substr() 读取子字符串,使用 replace() 写入子字符串;§36.3.8。
内容来源:
<<The C++ Programming Language >> 第4版,作者 Bjarne Stroustrup