11.7 - std::string_view(第二部分)
Key Takeaway
std:: string_view
提供了一个观察字符串的视图,字符串本身存放在二进制文件中- 创建
std:: string_view
时不会发生复制,但是修改时会影响到所有的对象。 remove_prefix
和remove_suffix
分别用于从字符串视图的左侧和右侧删除字符(不会影响到原字符串)- 要将
std:: string_view
转换为C风格的字符串,我们可以先转换为std:: string
- 使用
str.c_str()
获取C风格字符串 - 如果我们想写一个接受字符串形参的函数,将形参设置为
std:: string_view
是最灵活的选择,因为它可以高效地配合C风格的字符串参数(包括字符串字面量)、std:: string
参数(将隐式转换为' std:: string_view ')和std:: string_view
参数来工作: - 优先使用
std:: string_view
(按值传递)而不是const std:: string&
,除非你需要调用其他要求使用C风格字符串或std::string
的函数。 - 确保
std::string_view
观察的字符串不会离开作用域,也不会被修改。- 因为
std::string_view
的生命周期是独立于它“观察”的字符串的(也就是说该字符串对象可以在先于std::string_view
被销毁)。这种情况下访问std::string_view
就会造成未定义行为。
- 因为
- 只有在
std::string_view
没有被修改的情况下且字符串以空结束符结尾的情况下使用std::string_view::data()
如果字符串没有以空结束符结尾,则std::string_view::data()
会导致未定义行为。
作者注
本课程的部分内容被移动到了4.18 - std:: string_view 简介 。因此,这节课中有部分尚未清理的重复部分。
在上一节课中,我们介绍了 C 语言风格的字符串,但是使用它们的风险很高。尽管 C 语言风格字符串的性能很好,但是它们并不易用,安全性也不如 std:: string
。
但是 std:: string
(在 4.17 - std:: string 简介 中介绍)也有它的缺点,尤其是当我们需要常量字符串的时候。
考虑下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
和期望的一样,打印结果如下:
1 |
|
main
函数会将字符串 "hello"
复制 3 次,总共得到 4 个副本。首先是字符串字面量 “hello”
,它在编译时已知,并存储在二进制文件中。当我们创建 char[]
时,会创建一个副本。接下来的两个 std:: string
对象分别创建一个字符串副本。因为 std:: string
被设计为可修改的,每个 std:: string
必须包含它自己的字符串副本,这样给定的 std:: string
可以被修改而不影响任何其他的 std:: string
对象。
即使 const std:: string
也存在这个问题,即使它们不能修改。
std:: string_view
想象这样一个场景,你坐在家中,从窗户望向街道的时候你看到一辆汽车,你可以看到它,但你却无法触摸或者移动它。你的窗户只是为你提供了一个可以观察汽车的视图,而车则是完全独立的对象。
C++17 引入了使用字符串的另外一种方式——std:: string_view
,它通过 <string_view>
头文件提供。
和 std:: string
不同的是,它不会保存一份字符串的副本,std:: string_view
只是提供了一个被定义在其他地方的字符串的视图。
我们可以使用 std:: string_view
重新编写上面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
输出结果是相同的,但这次不再需要创建字符串 “hello”
的副本。字符串 "hello"
存储在二进制文件中,不会在运行时分配。text
只是字符串 "hello"
上的一个视图,所以不需要创建副本。当我们复制 std:: string_view
时,新的 std:: string_view
观察到的字符串与被复制的 std:: string_view
是相同。这意味着 str
和 more
都不会创建任何副本。它们是现有字符串 "hello"
的视图。
std:: string_view
不仅仅快,而且还具有 std:: string
的大部分功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
由于 std:: string_view
并不会创建一个字符串的副本,当我们修改 std:: string_view
的时候,被观察的字符串也会被修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
我们修改了 arr
,但是 str
也被修改了。这是因为它们实际上共享的是同一个字符串。如果你需要使用 std:: string_view
,那么最好不要去修改它对应的字符串,避免造成困扰和错误。
最佳实践
使用 std:: string_view
代替 C 语言风格字符串。
对于只读字符串,优先使用 std:: string_view
而不是 std:: string
。除非你已经具有了一个 std:: string
。
视图修改函数
回到之前有关”窗户“的比喻,考虑一个有”窗帘“的窗户。通过拉上左侧或右侧的窗帘,我们可以限制能够从窗户看到的范围,但是这并不会实际改变窗户外的物体,我们只是减小了视角而已。
类似地,std:: string_view
也提供了一些函数用于操作观察字符串的视图。通过这些函数我们可以修改看到的字符串,但不会修改被观察的字符串本身。
remove_prefix
用于从视图的左侧删除字符,而 remove_suffix
则用于从视图的右侧删除字符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
打印结果为:
1 2 3 |
|
与真正的窗帘不同, std:: string_view
不能被重新打开。一旦你缩小了区域,重新扩大它的唯一方法就是通过重新分配源字符串给它来重置视图。
std::string_view
可以配合没有空字符结束符的字符串
C风格的字符串和 std:: string
不同,std:: string_view
不使用空结束符来标记字符串的结束。相反,它知道字符串在哪里结束,因为它会跟踪字符串的长度。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
打印结果如下:
1 |
|
将 std::string_view
转换为 C 风格字符串
一些旧的函数(比如旧的 strlen
函数)仍然期望使用C风格的字符串。要将std:: string_view
转换为C风格的字符串,我们可以先转换为std:: string
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
打印结果:
1 |
|
不过,每次std:: string_view
需要被作为C风格字符串传递时,都需要创建一个std::string
,这么做的开销是非常大的,因此应该尽量避免这么做。
通过 const std::string&
还是 std::string_view
来传递字符串?
一个经常遇到的问题是:应该通过 const std::string&
还是 std::string_view
传递字符串?那种方式最好?
如果我们想写一个接受字符串形参的函数,将形参设置为 std::string_view
是最灵活的选择,因为它可以高效地配合C风格的字符串参数(包括字符串字面量)、std::string
参数(将隐式转换为std::string_view
')和 std::string_view
参数来工作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
|
注意,我们通过值传递std::string_view
,而不是通过 const
引用类型来传递。这是因为std::string_view
的复制速度通常较快,而按值传递对于复制开销较小的类型来说是最优的。
有一种情况,将形参设置为 const std::string&
通常更好:如果你的函数需要调用其他接受C风格字符串或std::string
形参的函数,那么 const std::string&
可能是更好的选择,因为 std::string_view
不能保证以空字符结束(而这是C风格字符串函数所期望的),并且不能有效地转换回std::string
。
最佳实践
优先使用std:: string_view
(按值传递)而不是const std:: string&
,除非你需要调用其他要求使用C风格字符串或std::string
的函数。
作者注
后面的一些课程是在引入std::string_view
之前编写的,所以仍然使用的是const std::string&
(实际上用std::string_view
更好),我们正在着手清理相关的代码。
所有权问题
std::string_view
的生命周期是独立于它“观察”的字符串的(也就是说该字符串对象可以在先于std::string_view
被销毁)。这种情况下访问std::string_view
就会造成未定义行为。
std::string_view
表示的字符串必须是在其他地方已经被创建的字符串。它可能是一个随着程序启动停止而创建销毁的字面量,也可能是一个 std::string
,这种情况下当str::string
销毁时该字符串也就不存在了。
std:: string_view
自身不能创建任何的字符串,它只是观察某个字符串的视图。
下面这个程序就出现了所有权问题:
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 |
|
1 2 3 4 |
|
在askForName()
函数中,我们创建了一个变量 name
并且通过 std::cin
对其进行了赋值。然后,我们创建了变量 view
,通过这个变量我们可“观察”上述字符串。在函数结束时,我们返回 view
,但是该变量实际观察的字符串 (name
) 已经被销毁了,因此 view
现在指向的是一块已经被释放的内存。函数返回的是一个悬垂(dangling) std::string_view
.
在mian
函数中访问被返回的std::string_view
会导致未定义行为,在笔者的电脑上,会打印奇怪的字符。
问题发生的原因在于你通过std::string
创建了一个 std::string_view
,然后又修改了 std::string
。修改 std::string
会导致内部字符串被销毁或被替换(在代码的其他部分)。std::string_view
总是指向该字符串, 但该字符串可能已经不是它原来的模样或者已经不存在了。
注意
确保 std::string_view
观察的字符串不会 离开作用域,也不会被修改。
使用data()
函数来打开”窗户“
std::string_view
观察的字符串,可以通过data()
函数来访问,它返回的结果是一个C风格的字符串。这种方式提供了一种快速访问被观察字符串的方法。但是它只能在std::string_view
视图没有被修改的时候使用(例如通过 remove_prefix
或 remove_suffix
修改视图),,该字符串是有空结束符的。
在下面的例子中,std::strlen
函数不能识别 std:: string_view
,因此我们必须传入 str.data()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
1 2 |
|
当 std:: string_view
被修改后,data()
就不能完成我们期望的任务了。下面的例子展示了当视图被修改后data()
的行为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
1 2 3 |
|
这显然不是我们期望的,这是因为我们使用 data()
来访问一个已经被修改的 std::string_view
。字符串的长度信息在我们使用data()
时就丢失了,std:: strlen
和 std:: cout
会持续读取直到遇到第一个空结束符,也就是“balloon”的末尾。
注意
只有在std::string_view
没有被修改的情况下且字符串以空结束符结尾的情况下使用std:: string_view:: data()
如果字符串没有以空结束符结尾,则std:: string_view:: data()
会导致未定义行为。
不完整的实现
作为一个相对较新的特性来说,std::string_view
的实现完成度并不如预期。
```cpp hl_lines="5,6" std::string s{ "hello" }; std::string_view v{ "world" };
// 不能工作 std::cout << (s + v) << '\n'; std::cout << (v + s) << '\n';
// 可能不安全,也可能并不是我们希望的做法,因为我们此时将std::string_view当做 // C风格字符串来使用 std::cout << (s + v.data()) << '\n'; std::cout << (v.data() + s) << '\n';
// 可以, 可以但是不优雅,而且浪费资源,因为我们需要构建一个新的 std::string.
std::cout << (s + std::string{ v }) << '\n';
std::cout << (std::string{ v } + s) << '\n';
std::cout << (s + static_cast
没道理不能使用第五行和第六行的方式,这个特性可能会在未来的C++版本中被支持。