C++ 复习教程第一章(使用 string 和 string_view)
第2章 —— 使用 string 和 string_view
你编写的每个应用程序都会使用某种类型的字符串。使用老式 C 语言时,没有太多选择,只能使用普通的以 null 结尾的字符数组来表示字符串。遗憾的是,这种表示方式会导致很多问题,例如会导致安全攻击的缓冲区溢出。C++标准库包含了一个安全易用的 substring 类,这个类没有这些缺点;
2.1动态字符串
在将字符串当成一等对象支持的语言中,字符串有很多有吸引人的特性,例如可扩展至任意大小,或能提取或替换子字符串。在其他语言(如 C 语言)中,字符串几乎就像后加入的功能;C 语言中并没有真正好用的 string数据类型,只有固定的字节数组。“字符串库”只不过是一组非常原始的函数,甚至没有边界检查的功能。C++ 提供了 string 类型作为一等数据类型。
1.1C风格的字符串
在 C 语言中,字符串表示为字符的数组。字符串中的最后一个字符是 null 字符(’\0’), 这样,操作字符串的代码就知道字符串在哪里结束。
官方将这个 null 字符定义为 NUL, 这个拼写中只有一个 L, 而不是两个 L,NUL和 NULL 指针是两回事。
尽管 C++提供了更好的字符串抽象,但理解 C 语言中使用的字符串技术非常重要,因为在 C++程序设计中仍可能使用这些技术。最常见的一种情况是 C++程序调用某个第三方库中(作为操作系统接口的一部分)用 C 语言编写的接口。
目前,程序员最容易犯的错误是忘记为 ‘\0’ 分配空间。例如,字符串 “hello” 看上去有 5 个字符长,但是实际在内存上需要 6 个字符空间才能保存这个字符串的值。
C++ 中包含一些来自 C 语言的字符串操作函数,它们被定义在
中定义,通常,这些函数不直接操作内存分配。 例如,strcpy() 函数有两个字符串参数。这个函数将第二个字符串赋值到第一个字符串,而不考虑第二个字符串能否恰当地填入第一个字符串中。下面代码试图在 strcpy() 函数之上构建一个包装器,这个包装器能够分配正确数量的内存并返回结果,而不是接受一个已经分配好的字符串。这个函数通过 strlen() 函数获取字符串的长度。调用者则负责释放 copyString() 分配的空间:
1
2
3
4
5
6
char* copyString(cnost char* str)
{
char* result = new char[strlen(str)]; // BUG !!! OFF BY ONE !
strcpy(result, str);
return result;
}cpoyString() 函数的代码这样写是不正确的。strlen() 函数返回字符串长度,而不是保存这个字符串所需的内存量。对于字符串 “hello”,strlen() 返回的是 5,而不是 6。为字符串分配内存的正确方式是在实际字符所需空间 +1。一开始看到到处都要加 1 可能会感到有点奇怪,但这是其工作方式,所以在使用 C 风格的字符串时要注意记住这一点。正确的实现代码如下:
1
2
3
4
5
6
char* copyString(cnost char* str)
{
char* result = new char[strlen(str) + 1];
strcpy(result, str);
return result;
}要记住 strlen() 只返回字符串中实际字符数目的一种方式是:考虑如果为一个由几个其他字符串构成的字符串分配空间,应该怎么做。例如,如果函数接收 3 个字符串参数,并返回一个由这 3 个字符串串联而成的字符串,那么这个返回的字符串应该有多大?为精确分配足够空间,空间的大小应该是 3 个字符串的长度相加,然后加上 1 留给尾部的 ‘\0’ 字符。如果 strlen() 的字符串长度包含 strcpy() 和 strcat() 函数执行这个操作。strcat()中的 cat 表示串联:
1
2
3
4
5
6
7
8
9
char* appendStrings(const char* str1, const char* str2, cosnt char* str3)
{
char* result = new str[strlen(str1) + strlen(str2) + strlen(str3) + 1];
strcopy(result, str1);
strcat(result, str2);
strcat(reslut, str3);
return result;
}C 和 C++中的 sizeof() 操作符可用于获得给定数据类型或变量的大小。例如,sizeof(char)返回 1, 因为字符的大小是 1 字节。但在 C 风格的字符串中,sizeof() 和 strlen() 是不同的,绝对不要通过 sizeof() 获得字符串的大小。它根据 C 风格的字符串的存储方式来返回不同大小。如果 C 风格的字符串存储为 char[],则 sizeof() 返回字符串使用的实际内存,包括’\0’字符。例如:
存储为 char[] 下的 C 风格字符串:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
char text1[] = "abcdef";
size_t s1 = sizeof(text1); // is 7
size_t s2 = strlen(text1); // is 6
size_t s0 = sizeof(&text1); // is situation as sizeof(text2) below
// 这里返回的是这个数组的长度
/*
优点:
占用的内存大小是在编译时确定的,sizeof() 可以直接获取数组的大小。
适用于需要修改字符串内容的场景,因为数组是可修改的。
缺点:
占用的内存空间较大,因为数组的大小包括了字符串内容和额外的 null 终止符。
不适用于常量字符串,因为数组的内容可修改
*/存储为 char 下的 C 风格字符串:*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const char* text2 = "abcdef";
size_t s3 = sizeof(text2); // is platform-dependent
size_t s4 = strlen(text2); // is 6
// 在 32 位模式下编译时,s3 的值为 4;
// 在 64 位模式下编译时,s4 的值位 8;
// 原因是因为这返回的是 指针 const char* 的大小
std::cout << (text1 == text2) << " " << (std::strcmp(text2, text1) == 0) << std::endl; // 返回 0
// 这说明这完全不一样的两种东西
/*
优点:
占用的内存大小是在编译时确定,sizeof() 得到的是指针大小,不受字符串内容影响。
适用于常量字符串,因为字符串内容是不可修改的。
占用的内存空间相对较小,因为只有指针的大小。
缺点:
不适用于需要修改字符串内容的场景,因为字符串是常量。
*/
char* ptr = new char[7];
strcpy_s(ptr, 7, "abcdef");警告:
在 Microsoft Visual Studio 中使用 C 风格的字符串函数时,编译器可能会给出安全相关的警告甚至错误,说明这些函数已经被废弃了。使用其他 C 标准库函数可以避免这些警告,例如 strcpy_s()和 strcat_s(), 这些函数是“安全 C 库”(ISO/IEC TR24731)标准的一部分。然而,最好的解决方案是切换到 C++的 string 类,本章后面的”C++ std::string 类” 小节会讨论这个类;
1.2字符串字面量*(string literal)*
1.字面量池和不同类型定义方式
- 注意,C++ 程序中编写的字符串要用引号包围。例如,下面的代码输出字符串 “hello”,这段代码中包含这个字符串本身,而不是一个包含这个字符串的变量:
1
std::cout << "hello" << std::endl;
- 与字符串字面量关联的真正内存位于内存的只读部分。通过这种方式,编辑器可重用等价字面量的引用,从而优化内存的使用。也就是说,即使一个程序使用了 500 次 “hello” 字符串字面量,编译器也只在内存中创建一个 “hello” 实例。**这种技术被称为字面量池 *(literal pooling)***;
- 字符串字面量可赋值给变量,但因为字符串字面量位于内存的只读部分,且使用了字面量池,所以这样做会产生风险——也就是说,字符串的字面量类型为 “n 个 const char 的数组”,但是显然为了兼容较老的不支持 const 的代码,大部分编译器不会强制程序将字符串字面量赋值给 const char 类型的变量*。但是,如果试图修改字符串,一般情况下,这种行为是没有定义的,会使得程序崩溃;但也可能能够使得程序继续执行,却又一些很莫名其妙的副作用;可能不加通告地忽略修改行为;可能修改行为是有效的,这完全取决于编译器。
- 下面的行为是未定义的:
1
2
char* ptr = "hello"; // Assign the string literal to a variable.
ptr[1] = 'a'; // Undefined behavior!
- 但是,更好的代码习惯是更好的:
1
2
const char* ptr = "hello"; // Assign the string literal to a variable.
ptr[1] = 'a'; // Error! Attempts to write to read-only memory
- 此外,使用 array 方式存储能够实现修改,也就是说,这时的 C 风格字符串并不在字面量池中,而是可以进行修改的:
1
2
3
char arr[] = "hello"; // Compiler takes care of creating appropriate sized
// character array arr.
arr[1] = 'a'; // The contents can be modified.
2.原始字符串字面量 (raw string literal)
- 原始字符串字面量是可横跨多行代码的字符串字面量,不需要转义嵌入的双引号,像 ‘\t’ 和 ‘\n’ 这种转义序列不按照转义序列的方式处理,而是按照普通文本的方式处理。转义字符在第 1 章讨论过了。如果像下面这样编写普通的字符串字面量,那么会收到一个编译器错误,因为字符串包含了未转义的双引号:
1
const char* str = "Hello "World"!"; // Error!
- 对于普通字符串,必须转义双引号,如下所示:
1
const char* str = "Hello \"World\"!";
- 对于原始字符串字面量,就不需要转义双引号了:
1
2
3
// 格式:
// R"(......)"
const char* str = R"(Hello "World"!)";
- 换行转义:
1
const char* str = "Line 1\nLine 2";
- 原始字符串字面量换行,直接 Enter 换行,与之前效果一样:
1
2
const char* str = R"(Line 1
Line2)";
注意:
原始字符串字面量会忽略掉转义序列,但是与转义序列情况中 “a”b”c” 一样的错误情况,原始字符串也会出现如下情况,不能在字符串中嵌入 )”,示例:
1
const char* str = R"(Embedded )" characters)"; // Error!
如果要需要嵌入 )”,则需要使用拓展的原始字符串字面量语法:
1
2
3
4
5
6
7
// 格式:
// R"d-char-sequence(r-char-sequence)d-char-sequence)-"
// d-char-sequence 可以是任意小于 16 个字符的分割符序列
// r-char-sequence 是要输出的字符串代码
const char* str = R"-(Embedded)" character)-";
const char* str = R"10101(Embedded)" character)10101";
// Embedded)" character 两行代码输出一致在操作数据库查询字符串、正则表达式和文件路径时,原始字符串字面量可以令程序编写更加方便。第 19 章将会讨论正则表达式;
1.3C++ std::string 类
C++ 提供了一个得到极大改善的字符串概念,并作为标准库的一部分提供这个字符串的实现。在 C++ 中,std::string 是一个类,(实际上是 basic_string 模板类的一个实例),这个类支持
中提供的许多功能,还能自动管理内存分配。string 类在 std 名称空间的 头文件中定义,之前已经多次使用到 string 类了,下面深入学习:
1.C 风格的字符串有什么问题
为理解 C++ string 类的必要性,需要考虑 C 风格字符串的优势和劣势:
优势:
- 很简单,底层使用基本的字符类型和数组结构,注意,C 数组是与指针强相关的,可以将 C 数组看作为指向数组第一个元素的指针,它具有数组指针的双重性;
- 轻量级,如果使用得当只会占用所需内存;
- 很低级,因此可以按操作原始内存方式轻松操作和赋值字符串;
- 能够很好地被 C 语言程序员理解——为什么还要学习新事物?
劣势:
- 为模拟一等字符串数据类型,需要付出很多努力;
- 使用难度大,而且很容易产生难以找到的内存 bug;
- 没有利用 C++ 的面向对象的特性;
- 要求程序员了解底层的表达方式;
尤其注意,事实上,C 风格字符串的优点同样注定地导致了它的缺点;
上面的列表实际很精心被提出来的,从而能够让我们思考应该有更好的方式。如后面所述,C++ 的 string 类解决了字符串的所有问题,并且证明了 C 字符串相比一等数据类型的那些优势事实上是极其不恰当的。
2.使用 string 类
尽管 string 是一个类,但是几乎总可以将 string 当成内建类型使用,事实上,把 string 想象为简单类型更容易发挥它的特性。通过运算符重载的神奇作用,C++ 的 string 使用起来比 C 字符串简单容易很多。例如,给 string 重定义 + 运算符,以表示“字符串串联”。示例如下,将得到 1234:
+重载
1
2
3
4
5
>string A("12" "21");
>// string 的传入"",中间如果是空白符,则连接在一起,例:("12" "21")会连接在一起相当于 "1221"
>string B("34");
>string C;
>C = A + B;+=重载
1
2
3
>string A("12");
>string B("34");
>A += B;此外,C 风格的字符串不能很好地执行 == 比较:
假如有以下两个字符串:
1
2
3
4
5
6
7
8
>char* a = "12";
>char b[] = "12";
>// 按照如下方式比较总得到的结果是 false,因为它比较的是指针的值,而不是字符串的内容
>if (a == b)
>// 要比较 C 字符串要使用如下代码:
>if (strcmp(a, b) == 0)此外,C 字符串也无法通过 <、 >、 <=、 >= 的比较,因此仍需要通过 strcmp() 根据字符串字典的顺序返回 -1、0、1 的值进行判断,这样将会使得代码很笨拙而且很容易出错;
而在 C++ 中这些比较运算符,operate==、opertae!= 和 operate< 等都被重载了,这些运算符可以操作真正的字符串字符,这样只需要通过运算符就可以完成基本操作,单独的字符可以通过运算符 operate[] 访问示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
>std::string myString = "hello";
>myString += ", there";
>std::string myOtherString = mystring;
>if (myString == myOtherString)
>{
myOtherString[0] = 'H';
>}
>std::cout << myString << std::endl;
>std::cout << myOtherString << std::endl;
>// 以上代码输出:
>hello, there
>Hello, there注意,从上述代码中,我们还 能够看出,string 类能够自动处理内存需求被再次分配和调整大小,因此不会出现内存溢出的情况,这使得对字符串的处理变得更安全方便;所有的这些 string 类对象都创建为堆栈变量。尽管 string 类可定需要完成大量分配内存和调整大小的工作,但是 string 类的析构函数会在 string 对象离开作用域的时候清理内存;
另外需要注意的是,运算符总能以预期的方式方式工作。例如, = 运算符复制字符串(这种复制不是复制指针,因此改变被复制指针不会对复制者有任何影响),这是最有可能预期的操作。如果习惯使用基于数组的字符串,那么这种方式可能会带来全新的体验,也可能令你感到迷惑,不用担心,一旦学会信任 string 类总能做出正确的行为,那么代码就会变得很简单了;
为达到兼容目的,还可以应用 string 类中的 c_str() 方法返回 C 风格字符串的 const 字符指针。不过,一旦 string 执行任何内存重分配或 string 对象被销毁了,那么这个 cosnt 指针就永久失效了。因此,当使用这个操作时需要谨慎明白这种指针丢失风险;所以,应当在使用结果之前调用这个方法,以便它能够准确地反映 string 的当前内容,并且永远不要从函数中返回在基于堆栈的 string 上调用 c_str() 的结果。
此外,还有一个 data() 方法,在 C++14 以及更早的版本中,始终与 c_str() 一样返回 const char 。从 C++ 17 开始,在非 const 字符上调用时, data() 方法将返回 char**;
除此以外,可以查看这本书的附录 B,查看可在 string 类对象上执行的所受对象操作;
3.std::string 字面量
源代码中的字符串字面量通常被解释为 const char*。使用用户定义的标准字面量 s 可以把字符串字面量解释为 std::string,例如:
1
2
3
4
5
auto string1 = "Hello World";
auto stirng2 = "Hello World"s;
// 用户定义的标准字面量 s 需要 using namespace std::string_literals;
// 或者需要 using namespace std;
4.高级数值转换
std 名称空间包含很多辅助函数,以便完成数值和字符串之间的转换。下面的函数可用于将数值转换为字符串。所有的这些函数都负责内存分配,它们会创建一个新的 string 对象并返回。
- string to_string(int val);
- string to_string(unsigned val);
- stirng to_string(long val);
- string to_string(unsigned long val);
- string to_string(long long val);
- string to_string(unsigned long long val);
- string to_string(float val);
- string to_string(double val);
- string to_string(long double val);
这些函数的使用简单直观,例如,下面示例中将 long double 值转化为字符串:
1
2
long double d = 3.14L;
string s = to_string(d);下面同样是定义在 std 名称空间中的函数,其中,str 表示将要进行转换的字符串;idx 是一个指针,这个指针将接收第一个未完成转换的字符的索引;base 表示转换过程中使用的进制。idx 可以是空指针,如果是空指针则被忽略。
如果不能执行任何转换,这些函数将抛出 invalid_argument 异常;
如果转换的值超出返回类型的范围,则抛出 out_of_range 异常;
int stoi(const string& str, size_t idx = 0, int base = 10);*
long stoi(const string& str, size_t idx = 0, int base = 10);*
unsigned long stol(const string& str, size_t idx = 0, int base = 0);*
long long stoll(const string& str, size_t idx = 0, int base = 0);*
unsigned long long stoull(const string& str, size_t idx = 0, int base = 0);*
float stof(const string& str, size_t idx = 0);*
double stod(const string& str, size_t idx = 0);*
long double stold(const string& str, size_t idx = 0);*
上面的使用时要加 std:,下面的不需要。
int strtoi(const string& str, size_t idx = 0, int base = 10);*
long strtoi(const string& str, size_t idx = 0, int base = 10);*
unsigned long strtol(const string& str, size_t idx = 0, int base = 0);*
long long strtoll(const string& str, size_t idx = 0, int base = 0);*
unsigned long long strtoull(const string& str, size_t idx = 0, int base = 0);*
float strtof(const string& str, size_t idx = 0);*
double strtod(const string& str, size_t idx = 0);*
long double strtold(const string& str, size_t idx = 0);*
示例:
1
2
3
4
5
const std::string toParse = " 123USB";
size_t index = 0;
int value = std::stoi(toParse, &index);
std::cout << "Parsed value: " << value << std::endl;
std::cout << "First non-parsed character: " << "''" << toParse[index] << "'" <<std::endl;输出如下:
1
2
Parsed value: 123
First non-parsed character: 'U'
5.低级数值转换
使用思路就是:用 s.data() 获取起始指针,利用 s.data() + s.size() 或者 s.data() + s.strlen() 获取终止指针,然后都是将值传递给相应需要的需要得到的变量中;
C++17 也提供了许多低级数值转换函数,这些都在
头文件中定义。这些函数不执行内存分配,而使用由调用者分配的缓存区。另外,对它们进行优化,以实现高性能,并独立于本地化(有关本地化的内容,详见第 19 章)。最终结果是,与其他更高级的数值转换函数相比,这些函数的运行速度要快几个数量级。如果性能要求高,需要进行独立于本地化的转换,则应当使用这些函数;例如,在数值数据与人类可读格式(如 JSON、XML 等)之间进行序列化/反序列化; 要将整数转化为字符使用下面一组函数,这里,IntegerT 可以是任意有符号或无符号的整数类型或字符类型,结果的类型是 to_chars_result :
to_chars_result to_chars(char first, char last,IntegerT value, int base = 10);**
to_chars_result 类型定义如下:
1
2
3
4
5
6
7
8
struct to_chars_result
{
char* ptr;
errc ec;
}
// 如果转换成功,那么 ptr 成员等于写入字符的下一位置(one-past-the-end)的指针;
// 如果转换失败,即: ec == errc::value_too_large,则 ptr 成员等于 last;示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
#include <string>
#include <charconv>
using std::cout;
using std::endl;
using std::string;
int main()
{
string out(10, ' ');
auto result = std::to_chars(out.data(), out.data() + out.size(), 12345);
if (result.ec == std::errc())
{
cout << "Converted value: " << out << endl;
}
else {
std::cerr << "Conversion error" << std::endl;
}
return 0;
}
// 使用结构化绑定可以将其写为:
std::string out(10, '');
auto [ptr, ec] = std::to_chars(out.data(), out.data() + out.size(), 12345);
if (result.ec == std::cerr()) {...}类似的,以下为浮点型的转换函数:
to_chars_result to_chars(char first, char last, FloatT value);**
to_chars_result to_chars(char first, char last, FloatT value, chars_format fomat);**
to_chars_result to_chars(char first, char last, FloatT value, chars_format fomat, int precision);**
这里,FloatT 可以是 float、double 或 long double。可使用 chars_format 标志的组合来指定格式:
chars_format 类型定义如下:
1
2
3
4
5
6
7
enum class chars_format
{
scientific; // Style: (-)d.ddde±dd
fixed; // Style: (-)ddd.dd
hex; // Style: (-)h.hhhp±d (Note: no 0x)
general = fixed | scientific // See the following staff
};默认格式是 chars_format::general, 这将导致 to_chars()将浮点值转换为 (-)ddd.ddd 形式的十进制表示形式,或(.)d.ddde土dd 形式的十进制指数表示形式,得到最短的表示形式,小数点前至少有一位数字(如果存在)。如果指定了格式,但未指定精度,将为给定格式自动确定最简短的表示形式,最大精度为 6 个数字;
对于相反的转换,即将字符序列转换为数值,可使用下面的一组函数:
from_chars_result from_chars(const char first, const char last, IntegerT& value, int base = 10);**
from_chars_result from_chars(const char first, const char last. FloatT& value, chars_format format = chars_format::general);**
from_chars_result 类型定义:
1
2
3
4
5
struct from_chars_result
{
const char* ptr;
errc ec;
};结果类型的 ptr 成员是指向未转换的第一个字符的指针;如果所有字符都成功转换,则它等于 last。如果所有字符都未转换,则 ptr 等于 first, 错误代码的值将为 errc::invalid_argument。如果解析后的值过大,无法由给定类型表示,则错误代码的值将是 errc::result_out_range。注意,from_chars()不会忽略任何前导空白。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 手动移动指针到第一个非空白字符
while (std::isspace(*str))
{
++str;
}
// 使用这种方法来移到要使用的数字前
// 示例如何使用:
#include <iostream>
#include <charconv>
int main() {
const char* str = "123.456";
double value;
// 使用 std::from_chars 进行字符串到浮点数的转换
auto result = std::from_chars(str, str + std::strlen(str), value);
if (result.ec == std::errc())
{
// 转换成功
std::cout << "Converted value: " << value << std::endl;
}
else
{
// 转换失败
std::cerr << "Conversion error" << std::endl;
}
return 0;
}
1.4std::string_view 类
在 C++17 之前,为了接收只读字符串而选择怎样的形参类型始终是个问题:
如果选择
const char*
,那么用户在使用 std::string 的话,就必须调用其上的 c_str() 或 data() 来获取 const char *,更糟糕的是,函数将失去 std::string 的良好面向对象性和良好的辅助方法。我们这样想的话为什么不使用
const string&
?这种情况,需要始终使用 std::string。比如,如果传递一个字符串字面量,编译器将不加通告地创建一个临时字符串对象(其中包含字符串字面量的副本),并将该对象传递给函数,因此会增加一些开销。在 C++ 中,字符串字面量(如
"hello"
)是以字符数组的形式存在的,类型为const char[N]
,其中 N 是字符串的长度。当将字符串字面量传递给接受const std::string&
的函数时,由于函数参数是引用,编译器会尝试将字符串字面量隐式地转换为std::string
对象。为了进行这种转换,
std::string
类型有一个接受const char*
的构造函数。因此,编译器会创建一个临时的std::string
对象,将字符串字面量的内容复制到该对象中,然后将这个临时对象传递给函数。这种隐式的转换可能会引入一些性能开销,因为它涉及到内存分配和复制操作。如果函数只是需要读取字符串而不修改它,使用
const std::string&
可能会显得不够高效。因此,人们有时会为此编写函数的 const char* 和 const string& 两个函数接收版本,这显然不够优雅。
在 C++17 中,通过引入 std::string_view 类解决了所有这些问题,std::string_view 类 std::basic_string_view类模板的实例化,在
头文件中定义。string_view 基本上就是 const string& 的简单替代品,但不会产生开销。它从不复制字符串,string_view 支持与 std::string 类似的接口。一个例外是缺少 c_str(), 但 data() 是可用的。另外,string_view 确实添加了 remove_prefix(size_t) 和 remove_suffix(size_t) 方法;前者将起始指针前移给定的偏移量来收缩字符串,后者则将结尾指针倒退给定的偏移量来收缩字符串。 具体操作上就是用 std::string_view() 构造函数 替代 const std::string& 样式的传参方式,来减小开支,因为
std::string_view()
构造函数并不会引起额外的动态内存分配或拷贝,因为它只是一个视图,不拥有字符串的所有权。这个构造函数创建了一个空的std::string_view
对象,但并不分配内存或复制字符串。
1
2
3
4
std::string_view myString = "Hello, World!";
// 移除末尾的前7个字符
myString.remove_suffix(7);
std::cout << "After removing suffix: " << myString << std::endl;
- 注意,无法连接一个 string 和一个 string_view 类型,下面的代码将无法编译:
1
2
3
4
5
6
string str = "Hello";
string_view sv = " world";
auto result = str + sv;
// 最后一行改为:
auto result = str + sv.data();
- 如果知道如何使用 std::string, 那么使用 string_view 将变得十分简单,如下面的代码片段所示。extractExtension() 函数提取给定文件名的扩展名并返回:
1
2
3
4
string_view extractExtension(string_view fileName)
{
return fileName.substr(fileName.rfind('.'));
}
- 该函数可用于所有不同类型的字符串:
1
2
3
4
5
string fileName = R"(c:\temp\my file.ext)";
cout << "C++ string: " << extractExtension(fileName) << endl;
const char* cString = R"(c:\temp\my file.ext)";
cout « "C string: " << extractExtension(cString) << endl;
cout « "Literal: " « extractExtension(R"(c:\temp\my file.ext)") << endl;
- 注意,通常按值传递 string_views, 因为它们的复制成本极低。
- 复制成本极低原因:它们只包含指向字符串的指针以及字符串的长度。
- 在对 extractExtension() 的所有这些调用中,并非进行单次复制。extractExtension() 函数的 fileName 参数只是指针和长度,该函数的返回类型也是如此。这都十分高效。
string_view 构造函数的使用:
- 它可以接收任意原始缓存区和长度,这可用于从字符串缓冲区(并非以 NUL 终止) 构建 string_view。如果确实有一个以 NUL 终止的字符串缓冲区,但是如果已经知道了字符串长度,构造函数不必再统计字符串数目
1
2
3
4
5
6
size_t length = 3;
const char* cString = R"(c:\temp\my file.ext)";
cout << std::string_view(cString, length) << endl;
cout << std::string(cString, length) << endl;
// 使用 string_view 更高效
- 总结起来就是,string_view 比 string 强的地方就是,它不用再进行隐式的 string 类型构造,因为 C 风格字符串和 string 字面量 会进行隐式转换,复制构造,这里会有开销差距。
- 我上面说了,既然没有隐式构造,那么我们的返回值是 string_view 那么就不能被当作 string 参量看待也就是我接下来举出的例子说明的事情:
1
2
3
4
5
6
7
8
9
10
11
12
13
void handleExtension(const string& extension){...}
// 没有隐式转换存在,不能使用以下方式调用函数:
handleExtension(extractExtension("my file.ext"));
// 可选用以下方法:
// 1.显式转换 explitcit ctor
handleExtension(string(extractExtension("my file.ext")));
// 2.data() 方法
handleExtension(extractExtension("my file.ext").data());
/* 注意,即使原本的传入参数应该为 const string& 类型,但是 C 风格的字符串会首先隐式地调用 string 构造函数,然后再调用 const string& 构造函数,以完成代码 */
handleExtension(C-String);注意:在每当使用只读字符串作为参数时,可以使用 std::string_view 代替 const char 或 const string&。*
std::string_view 字面量:
可使用标准的用户定义的字面量 sv,将字符串字面量解释为 std::string_view,例如:
1
auto sv = "My string_view"sv;
标准的用户定义的字面量 sv 需要 using namespace std::string_view_literals; 或 using namespace std;
总结:
自动调用隐式构造函数的情况:
1
2
3
4
5
6
7
8
9
10
char* ---> string_view 可行
string ---> string_view 可行
string_view ---> string_view 可行
char* ---> string 可行
string ---> string 可行
string_view ---> string 不可行
...---> char* 都不可行
// 以上的左侧是实际参数,右边是函数期待的参数,可行代表可以运行,不可行代表不接受字面量在不告知的情况下,是 const char* 类型或者说是 const char[] 类型;
1.5非标准字符串
许多 C++程序员都不使用 C++风格的字符串,这有几个原因。一些程序员只是不知道有 string 类型,因为它并不总是 C++ 规范的一部分。其他程序员发现,C++ string 没有提供他们需要的行为,所以开发了自己的字符串类型。也许最常见的原因是,开发框架和操作系统有自己的表达字符串的方式,例如 Micros。仕 MFC 中的CString 类。它常用于向后兼容或解决遗留的问题。在 C++ 中启动新项目时,提前确定团队如何表示字符串是非常重要的。务必注意以下几点:
- 不应当选取 C 风格的字符串表示。
- 可对自己所用框架中的可用字符串进行标准化,如 MFC、QT 内置的字符串功能。
- 如果为字符串使用 std::string,应当使用 std::string_view 将只读字符串作为参数传递给函数;否则,看一下框架是否支持类似于它的类。
2.2本章小结
一句话,用 string,传只读字符串用 string_view。