在大多数程序中,文本处理都是其重要的组成部分。C++标准库提供了string类型,使程序员不必再使用C风格的文本处理方式——通过指针操作字符数组。C++标准库还提供了string_view类型,允许程序员以容器的方式访问字符序列,而无论它们存储在哪里,是string对象还是字符数组。此外,C++标准库还提供了正则表达式,用于在文本中匹配特定的模式。标准库中的正则表达式与大多数编程语言所提供的正则表达式类似。string和regex都支持包括Unicode等在内的多种字符编码。
标准库中的string类型,具有比简单的字符串字面量,更完整的字符串处理能力。string是用于管理不同字符序列的regular类型。string类型提供了很多有用的字符串处理功能,如字符串拼接等。例如:
xxxxxxxxxx
31string compose(const string& name, const string& domain) {
2 return name + '@' + domain;
3}
xxxxxxxxxx
11auto mbox = compose("minwei", "tedu.cn"); // minwei@tedu.cn
在本例中,mbox被初始化为“minwei@tedu.cn”。函数compose中的字符串“加法”表示拼接处理。它可以将string对象、字符串字面量、C风格字符串,或者一个单个字符,拼接在一个string对象所表示的字符串末尾。标准库为string类型定义了一个转移构造函数,因此即使以传值而非传引用方式返回一个很长的字符串,也会很高效。
在很多应用中,字符串拼接常被用于在一个字符串的末尾追加一些内容。这可以通过“+=”操作来实现。例如:
xxxxxxxxxx
31string s1 = "the 1st line", s2 = "the 2nd line";
2s1 = s1 + '\n'; // 在s1的尾部追加一个换行符
3s2 += '\n'; // 在s2的尾部追加一个换行符
这两种在字符串末尾追加内容的方式,在语义上时完全等价的。后者显然比前者更加简洁、明确地表达了代码编写者的意图,而且可能还更高效。
string对象是可修改的。除了“+”和“+=”,string还支持下标([])和提取子字符串等操作。例如:
xxxxxxxxxx
101string s3 = "Hello, World!";
2for (int i = 0; i < s3.size(); ++i)
3 cout << s3[i] << ' '; // H e l l o , W o r l d !
4cout << endl;
5string s4 = s3.substr(7, 5);
6cout << s4 << endl; // World
7s3.replace(7, 5, "c++");
8cout << s3 << endl; // Hello, c++!
9s3[7] = toupper(s3[7]);
10cout << s3 << endl; // Hello, C++!
substr函数返回一个string对象,保存其参数指定的子字符串的拷贝。其第一个参数表示子字符串第一个字符在原字符串中的下标,第二个参数表示子字符串的长度。由于下标从0开始,因此s4的值为“World”。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
H | e | l | l | o | , | W | o | r | l | d | ! |
replace函数替换字符串中的内容,从指定位置(第一个参数)开始,将指定长度(第二个参数)的子字符串,替换为指定的字符串(第三个参数)。替换的内容和被替换的子字符串不一定一样长。替换后的s3为“Hello, c++!”
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
H | e | l | l | o | , | W | o | r | l | d | ! | |
H | e | l | l | o | , | c | + | + | ! |
toupper函数返回其参数字符的大写字符。s3最后变为“Hello, C++!”
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
---|---|---|---|---|---|---|---|---|---|---|---|---|
H | e | l | l | o | , | W | o | r | l | d | ! | |
H | e | l | l | o | , | c | + | + | ! | |||
H | e | l | l | o | , | C | + | + | ! |
在string支持的所有字符串操作中,最常用的无外乎赋值操作(=)、下标操作([]或at)、比较操作(<、<=、>、>=、==、!=)、迭代操作(begin和end)、I/O流操作(<<和>>)等。
字符串的比较操作(<、<=、>、>=、==、!=),可以在string对象之间,也可以在string对象和C风格字符串或字符串字面值之间。例如:
xxxxxxxxxx
51string s5 = "yes", s6 = "no";
2cout << boolalpha << (s5 == s6) << endl; // false
3char s7[] = "year";
4cout << (s5 >= s7) << endl; // true
5cout << (s5 < "year") << endl; // false
C风格字符串的本质,就是一个以空字符('\0')结尾的字符数组。
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|
C | S | t | r | i | n | g | NUL |
空字符(NUL)不是0字符,也不是空格,而是一个值为0的单个字节,其字面值表示为'\0'。它并不是字符串有效内容的一部分,仅作为字符串结束的标志。string提供两个只读访问接口c_str和data,以获得指向C风格字符串的指针。例如:
xxxxxxxxxx
31string s8 = "C-String";
2const char* s9 = s8.c_str(), * s10 = s8.data();
3cout << s9 << ' ' << s10 << endl; // C-String C-String
根据定义,字符串字面量的数据类型是“const char*”。要想获得string类型的字面量,需为其添加s后缀。例如下面的代码是无法通过编译的:
xxxxxxxxxx
11cout << "Hello, " + "World!" << endl; // 编译错误,指针不能相加
但这样可以:
xxxxxxxxxx
11cout << "Hello, "s + "World!" << endl; // Hello, World!
后缀s定义在std::literals::string_literals命名空间中。
在当前的string实现版本中,通常会使用短字符串优化(SSO)技术,即将较短的字符串直接保存在string对象内部,而将较长的字符串保存在自由存储中。例如下面的代码:
xxxxxxxxxx
21string s1 = "short";
2string s2 = "long long long long long";
它们的内存布局可能会象下面这样:
当一个string对象的值从短字符串变为长字符串时,它的存储方式会发生相应地改变,反之亦然。那么多长的字符串算短字符串,多长的字符串又算长字符串呢?这由具体实现决定,14个字符可能是一个分水岭。
string的实际性能严重依赖运行时环境。特别是在多线程场景下,内存分配的时间开销可能会非常大。另外,当存在众多长度不同的字符串时,大量的内存碎片会进一步降低程序的运行性能。这些都是短字符串优化被普遍采用的原因。
string中的每个字符都是char类型的,这对于ASCII字符集而言显然已经足够了。为了支持更多的字符集,特别那些需要用char以外的数据类型表示一个字符的字符集,标准库定义了一个通用的字符串模板basic_string,其中的字符类型是参数化的。string实际上就是basic_string<char>的别名而已。
xxxxxxxxxx
41template<typename Char>
2class basic_string {
3 ... Char类型组成的字符串 ...
4};
xxxxxxxxxx
11using string = basic_string<char>;
用户可以定义任意字符类型的字符串。例如用zchar类型表示一个汉字字符,则表示汉字字符串的类型可被定义为:
xxxxxxxxxx
11using zstring = basic_string<zchar>;
这样,就可以在zstring——汉字字符串上执行常见的字符串操作了。
字符序列最常见的用途是将其作为参数传递给一个函数以供读取。这可以通过string类型以传值的方式实现,也可以传递string类型的引用,甚至C风格的字符串。在很多系统中,还存在着其它替代方案,比如使用标准库以外的字符串类型。在所有这些情况下,传递子字符串(字符串的一部分)都会增加额外的复杂度。为了解决这个问题,标准库提供了string_view类,即字符串视图,用一个指针——长度对,表示字符串中的字符序列。
xxxxxxxxxx
51ABCDEFGHIJ
2^
3{*, 10} -> ABCDEFGHIJ
4^
5{*, 8} -> BCDEFGHI
string_view类允许以只读方式访问基于字符的连续序列。这里的字符可以很多方式存储,包括标准库的string对象和C风格的字符串。string_view是一个很轻量级的对象,只维护一个指针和一个长度,并不负责任何与所表示字符序列有关的内存分配、释放与复制。例如下面的函数用于拼接两个字符串:
xxxxxxxxxx
41string cat(string_view sv1, string_view sv2) {
2 string res{ sv1 }; // 用sv1初始化res
3 return res += sv2; // 将sv2拼接到res末尾并返回res
4}
而对它的调用可能会象下面这样:
xxxxxxxxxx
141string king = "Harold";
2
3auto s1 = cat(king, "William");
4cout << s1 << endl; // HaroldWilliam
5auto s2 = cat(king, king);
6cout << s2 << endl; // HaroldHarold
7auto s3 = cat("Edward", "Stephen"sv);
8cout << s3 << endl; // EdwardStephen
9auto s4 = cat("Canute"sv, king);
10cout << s4 << endl; // CanuteHarold
11auto s5 = cat({ &king[0], 2 }, "Henry"sv);
12cout << s5 << endl; // HaHenry
13auto s6 = cat(cat({ &king[0], 2 }, { &king[2], 2 }), { &king[4], 2 });
14cout << s6 << endl; // Harold
对于函数参数而言,使用string_view对象接受实参,相比于用const string&,至少有三个优点:
它可用于以多种不同方式管理的字符序列
它可以轻松地传递子字符串
传递C风格的字符串时,无需创建string对象
表示string_view型字面量的sv后缀,定义在std::literals::string_view_literals命名空间中。使用sv后缀比不使用该后缀的好处是,有关字符串长度的计算在编译期完成,不消耗运行时间。
string_view类型同时还可以作为一个范围定义,因此可以用它来遍历所表示字符序列中的每个字符。例如:
xxxxxxxxxx
41void printLower(string_view sv) {
2 for (char ch : sv)
3 cout << (char)tolower(ch);
4}
string_view类型的显著限制在于它是只读的。例如,将参数字符串中的字符修改为小写的函数,就不能通过string_view传递参数。下面的代码将无法通过编译:
xxxxxxxxxx
41void toLower(string_view sv) {
2 for (char& ch : sv) // 编译错误
3 ch = tolower(ch);
4}
这种情况下,span也许是个值得考虑的选择。任何时候,一个string_view对象都相当于一个指针,所有与指针有关的限制,对string_view对象同样成立。例如下面的代码虽然可以通过编译:
xxxxxxxxxx
31string_view bad() {
2 return "Once upon a time"s;
3}
但使用bad函数的返回值将导致无法预期的后果:
xxxxxxxxxx
11cout << bad() << endl; // ...
在bad函数中,"Once upon a time"s,实际上是一个定义在局部作用域中的匿名string对象。该函数的返回值是一个string_view对象,其中的指针指向此局部string对象的字符序列。bad函数一旦返回,该局部string对象即被销毁,存放字符序列的内存空间即被释放,所返回string_view对象中的指针即成为悬空指针。对悬空指针的访问结果,在任何时候都是未定义的。
如果在使用string_view的过程中发生下标越界,结果将是未定义的。如果需要范围检查支持,可以使用at函数,它会在下标越界时抛出out_of_range异常。例如:
xxxxxxxxxx
81string_view s7 = { "12345" + 1, 3 }; // 234
2try {
3 cout << s7[3] << endl; // 5
4 cout << s7.at(3) << endl; // invalid string_view position
5}
6catch (const out_of_range& ex) {
7 cerr << ex.what() << endl;
8}
或者使用gsl::string_span作为替代。
C++17:string_view
正则表达式是一种很强大的文本处理工具。它通过一种简单、精炼的方法,描述文本中的模式,如“192.168.0.11”形式的IPv4地址,或形如“2024/10/21”的日期等。同时,它还提供了在文本中高效地查找模式的方法。C++标准库在<regex>头文件中定义了std::regex类及一系列函数,以支持对正则表达式的编程。下面是一个描述类似“TX 77845-4467”的美国邮政编码的正则表达式:
xxxxxxxxxx
11\w{2}\s*\d{5}(-\d{4})?
它描述了一种文本模式:以两个字母(\w{2})开始,后跟任意个空白字符(\s*),然后是五个数字(\d{5}),可能(?)还带有一个连字符引导的四位数字(-\d{4})。可以用该正则表达式,构造一个类型为regex的对象。例如:
xxxxxxxxxx
11regex pat{ R"(\w{2}\s*\d{5}(-\d{4})?)" };
其中的“R"(...)"”表示原始字符串字面量。原始字符串中反斜线连同后面的字符,不会被当做转义字符看待。“\n”就是一个“\”字符和一个“n”字符,不转义为换行符。正则表达式中包含大量的反斜线,使用原始字符串字面量会非常方便。如果换成常规字符串字面量,需要写成如下等价形式:
xxxxxxxxxx
11regex pat{ "\\w{2}\\s*\\d{5}(-\\d{4})?" };
在<regex>头文件中,标准库为正则表达式提供如下支持:
函数 | 功能 |
---|---|
regex_math | 将正则表达式与一个(已知长度的)字符串进行匹配 |
regex_search | 在一个(任意长的)数据流中,搜索与正则表达式匹配的字符串 |
regex_replace | 在一个(任意长的)数据流中,替换与正则表达式匹配的字符串 |
regex_iterator | 遍历匹配的结果和子匹配 |
regex_token_iterator | 遍历未匹配的部分 |
C++11:原始字符串字面量
C++11:正则表达式(reg)
使用模式的最简单的方式,就是在流中搜索它。例如:
xxxxxxxxxx
61int no = 1;
2for (string line; getline(cin, line); ++no) { // 读取一行至缓冲区
3 smatch matches; // 全部匹配文本集合
4 if (regex_search(line, matches, pat)) // 在一行内搜索匹配
5 cout << no << ": " << matches[0] << endl; // 输出整个匹配
6}
regex_search函数在line字符串中搜索任何与正则表达式pat匹配的子字符串,并将其保存到matches中,返回true。若未找到任何匹配,则返回false。matches其实就是一个vector<string>。matches[0]对应整个匹配,其余元素为子匹配。例如:
xxxxxxxxxx
91int no = 1;
2for (string line; getline(cin, line); ++no) { // 读取一行至缓冲区
3 smatch matches; // 全部匹配文本集合
4 if (regex_search(line, matches, pat)) { // 在一行内搜索匹配
5 cout << no << ": " << matches[0] << endl; // 输出整个匹配
6 if (matches.size() > 1 && matches[1].matched) // 如果有子匹配
7 cout << '\t' << matches[1] << endl; // 输出子匹配
8 }
9}
输入:
xxxxxxxxxx
11DC 20500-0001
输出:
xxxxxxxxxx
211: DC 20500-0001
2-0001
matches[0]对应整个模式“\w{2}\s*\d{5}(-\d{4})?”,而matches[1]则对应其中的子模式“-\d{4}”。
换行符“\n”也可以作为模式的一部分,因此可以对包含换行符的多行模式进行搜索。
正则表达式的编译,由regex在运行时完成。
regex可以识别几种不同的正则表达式符号集,如ECMA等。符号集中的一些字符具有特殊意义,如下表所示:
正则表达式中的特殊字符 | 含义 |
---|---|
. | 任意单个字符 |
( | 分组开始 |
) | 分组结束 |
[ | 字符集开始 |
] | 字符集结束 |
{ | 重复开始 |
} | 重复结束 |
\ | 下一个字符有特殊含义 |
* | 后缀,零次或多次 |
+ | 后缀,一次或多次 |
? | 后缀,零次或一次 |
| | 二选一(或) |
^ | 行开始、非 |
$ | 行结束 |
例如下面的正则表达式:
xxxxxxxxxx
11^A*B+C?$
描述了这样一种模式:以零或多个A开始,后跟一或多个B,最后是一个可选的C。以下文本均与该模式匹配:
xxxxxxxxxx
31AAAAAAAAAAAABBBBBBBBBC
2BC
3B
而下面的文本则不与该模式匹配:
xxxxxxxxxx
31AAAAA // 没有B
2AAAABC // 多了前导空格
3AABBCC // 多了一个C
正则表达式中被圆括号括起来分组,构成了一个子模式,可以从smatch中单独抽取出来。例如:
xxxxxxxxxx
31\d+-\d+ // 没有子模式
2\d+(-\d+) // 一个子模式
3(\d+)(-\d+) // 两个子模式
下表中的后缀表示一个模式是可选的还是重复多次的,如无这些后缀,则只能出现一次:
可选/重复后缀 | 含义 |
---|---|
{n} | 严格重复n次 |
{n,} | 至少重复n次 |
{n,m} | 至少重复n次,至多重复m次 |
* | 零次或多次,等价于{0,} |
+ | 一次或多次,等价于{1,} |
? | 零次或一次,等价于{0,1} |
例如下面的正则表达式:
xxxxxxxxxx
11A{3}B{2,4}C*
描述了这样一种模式:3个A,后跟2到4个B,最后零或多个C。以下文本均与该模式匹配:
xxxxxxxxxx
21AAABBC
2AAABBB
而下面的文本则不与该模式匹配:
xxxxxxxxxx
31AABBC // A太少
2AAABC // B太少
3AAABBBBBCCC // B太多
默认情况下,模式匹配器总是试图查找最长的匹配,即所谓最长匹配(贪心匹配)法则。例如用正则表达式“(ab)+”在文本“cccabababddd”中查找匹配,得到的结果是“ababab”。而如果在任何表示重复的符号({}、*、+、?)之后再放一个“?”后缀,则会采用最短匹配(懒惰匹配)法则。例如用正则表达式“(ab)+?”在文本“cccabababddd”中查找匹配,得到的结果是“ab”。
在正则表达式中,用类似“[ ... ]”的形式表示一个特定的字符集。例如用“[[:digit:]]”表示十进制数字字符集,用“[^[:digit:]]”表示非十进制数字字符集。下表列出了一些常用的字符集关键字:
关键字 | 字符集 |
---|---|
alnum | 字母、数字字符 |
alpha | 字母字符 |
blank | 除行分隔符以外的空白字符 |
cntrl | 控制字符 |
d | 十进制数字字符 |
digit | 十进制数字字符 |
graph | 图形字符 |
lower | 小写字符 |
可打印字符 | |
punct | 标点字符 |
s | 空白字符 |
space | 空白字符 |
upper | 大写字符 |
w | 字母、数字、下划线字符 |
xdigit | 十六进制数字字符 |
标准中规定,一些字符集还可以采用简写形式。例如:
简写 | 等价 | 字符集 |
---|---|---|
\d | [[:digit]] | 十进制数字字符 |
\s | [[:space:]] | 空白字符 |
\w | [_[:alnum:]] | 字母、数字、下划线字符 |
\D | [^[:digit:]] | 非十进制数字字符 |
\S | [^[:space:]] | 非空白字符 |
\W | [^_[:alnum:]] | 非字母、数字、下划线字符 |
此外,还有一些非标准的字符集简写形式。例如:
简写 | 等价 | 字符集 |
---|---|---|
\l | [[:lower:]] | 小写字符 |
\u | [[:upper:]] | 大写字符 |
\L | [^[:lower:]] | 非小写字符 |
\U | [^[:upper:]] | 非大写字符 |
为了保证程序代码的可移植性,建议使用完整的字符集名称,如“[[:digit:]]”,而非其简写形式,如“\d”。
合法的C++标识符(变量名、函数名、类型名等),必须以字母或下划线开头,后跟一个由字母、数字或下划线组成的字符序列,该序列可以为空。为了描述这样的模式,考虑如下正则表达式:
xxxxxxxxxx
71[:alpha:][:alnum:]* // 错误:表示字符集需要在外面再加一对方括号
2[[:alpha:]][[:alnum:]]* // 错误:第一个字符及其后面的字符可以是下划线,但未包括
3([[:alpha:]]|_)[[:alnum:]]* // 错误:第一个字符后面的字符可以是下划线,但未包括
4([[:alpha:]]|_)([[:alnum:]]|_)* // 正确,但太啰嗦
5[[:alpha:]_][[:alnum:]_]* // 正确,考虑了第一个字符及其后面的字符是下划线的情况
6[_[:alpha:]][_[:alnum:]]* // 正确,改变顺序不影响正确性
7[_[:alpha:]]\w* // 正确,\w等价于[_[:alnum:]]
下面的函数用于判断一个字符串是否是合法的C++标识符:
xxxxxxxxxx
41bool isIdentifier(const string& s) {
2 regex pat{ "[_[:alpha:]]\\w*" };
3 return regex_match(s, pat);
4}
注意,要在一个普通字符串字面量中包含一个反斜线,必须使用两个反斜线。使用原始字符串字面量可以避免这种麻烦。例如:
xxxxxxxxxx
41bool isIdentifier(const string& s) {
2 regex pat{ R"([_[:alpha:]]\w*)" };
3 return regex_match(s, pat);
4}
下表罗列了一些正则表达式,及与之匹配和不匹配的文本示例:
正则表达式 | 匹配示例 | 不匹配示例 |
---|---|---|
Ax* | A Axx Axxxxx | B |
Ax+ | Ax Axxx Axxxxxx | A |
\d-?\d | 1-2 12 | 1--2 |
\w{2}-\d{4,5} | Ab-1234 XX-54321 22-2222 | 22_2222 |
(\d*:)?(\d+) | 12:3 1:23 123 :123 | 123: |
(bs|BS) | bs BS | bS |
[aeiouy] | 仅由元音字母组成的字符串 | x |
[^aeiouy] | 仅有非元音字母组成的字符串 | e |
[a^eiouy] | 仅由元音字母和“^”组成的字符串 | _ |
在一个正则表达式中,被圆括号括起来的部分形成了一个分组,同时也是一个子模式,用sub_match表示。如果只想表示一个分组,而无意将其作为子模式,则应使用“(?: ... )”,而非单纯的“( ... )”。例如:
xxxxxxxxxx
11(\s|:|,)*(\d*) // 零到多个空白字符、冒号或逗号,后跟零到多个十进制数字
假设对数字之前的字符不感兴趣,则可写成:
xxxxxxxxxx
11(?:\s|:|,)*(\d*)
这里的“?:”使得只有数字部分才是子模式。
下表罗列了一些正则表达式分组的例子:
正则表达式 | 分组 |
---|---|
\d*\s\w+ | 无分组 |
(\d*)\s(\w+) | 两个分组 |
(\d*)(\s(\w+))+ | 两个分组(分组没有嵌套) |
(\s*\w*)+ | 一个分组,但有一到多个子模式,只有最 后一个子模式被保存为一个sub_match |
<(.*?)>(.*?)</\1> | 三个分组,“\1”表示与分组1一样 |
最后一个模式对解析XML文件非常有用。它可以查找标签开始(如“<b>”)和结束(如“</b>”)标记。注意,这里对标签开始和结束间的子模式,使用了懒惰匹配法则(“.*?”)。如果使用默认的贪心匹配法则(“.*”),对于下面的文本会发生问题:
xxxxxxxxxx
11Always look on the <b>bright</b> side of <b>life</b>.
如果第二个子模式采用贪心匹配法则,即“<(.*?)>(.*)</\1>”,则会将第一个“<b>”和最后一个“</b>”配对。这是正确的匹配行为,但或许并非代码编写者所期望的。
可以借助regex_iterator类型的对象,遍历一个流中所有与给定模式匹配的对象。为了遍历一个字符串中所有与给定模式匹配的子字符串,可以使用sregex_iterator(regex_iterator<string>)类型的迭代器。例如下面的代码将从一个完整的句子中拆分出每个单词,滤除单词间的空白字符和标点符号:
xxxxxxxxxx
51string words{ "the quick brown fox jumps over the lazy dog." };
2regex pat{ R"([^([:space:]|[:punct:])]+)" };
3for (sregex_iterator word{ words.begin(), words.end(), pat };
4 word != sregex_iterator{}; ++word)
5 cout << (*word)[0] << endl;
regex_iterator的默认构造函数用于创建一个表示序列终止的迭代器。regex_iterator是一种双向迭代器,因此不能用于遍历istream流对象。另外,只能通过regex_iterator读取数据,而不能通过它写入数据。
尽量使用std::string保存字符序列
优先选择std::string提供的功能操作字符串,而不是C语言的字符串函数
可以声明string类型的变量和成员,而不要将它作为基类
从函数中返回string类型的对象,并不会带来性能损失,转移语义和拷贝消除会很好地解决这方面的问题
直接或间接地使用substr函数读取子字符串,使用replace函数写入子字符串
用string表示的字符串,会在运行时根据需要自动伸缩
当需要范围检测时,应使用at函数,而不是迭代器或下标操作符([])
当需要优化性能是,应使用迭代器或下标操作符([]),而不是at函数
使用基于范围的for循环,可以安全地降低对越界检查的需求
将输入的字符串保存在string对象中,不用担心内存溢出的风险
只在迫不得已时,才通过c_str或data函数,从string对象中获得其C风格的字符串表示
使用stringstream类或通用的值提取函数(如to<X>),将字符串转换为数值
可以使用basic_string类模板构造由任意类型字符组成的字符串
后缀“s”表示string类型的字符串字面量
在需要读取以各种形式存储的字符序列时,建议使用string_view作为函数的参数
在需要写入以各种形式存储的字符序列时,建议使用string_span<char>作为函数的参数
可以将string_view对象视为绑缚了大小的指针,它并不真正拥有属于自己的字符串
后缀“sv”表示string_view类型的字符串字面量
将regex用于正则表达式的大部分常规操作
除非是最简单的模式,否则应采用原始字符串字面量的形式,表示正则表达式
使用regex_match函数匹配整个输入
使用regex_search函数在输入流中搜索模式
可以调整正则表达式的符号表示,以满足不同的标准要求
默认的正则表达式的符号表示,是ECMAScript中采用的表示法
使用正则表达式要注意节制,它很容易变成一种难读的语言
象“\1”、“\2”这样的写法,用于引用前面的子模式
用“?”将模式匹配策略显式指定为懒惰策略
用regex_iterator迭代器遍历流中的给定模式