在之前的课程中,我们介绍了两种字符串类型:std::string
(5.7 – std::string
简介)和 std::string_view
(5.8 – std::string_view
简介)。
因为 std::string_view
是我们首次接触视图类型,我们将花更多时间进一步讨论它。我们将重点讨论如何安全地使用 std::string_view
,并提供一些说明如何错误使用它的示例。最后,我们将给出一些关于何时使用 std::string
与 std::string_view
的指导原则。
理解所有权与观察者模式
所有权的概念与成本
让我们暂时引入一个类比。假设你决定要画一幅自行车的画。但你没有自行车!你该怎么办?
嗯,你可以去当地的自行车行买一辆。这样你就拥有了那辆自行车。这样做有一些好处:你现在拥有一辆可以骑的自行车。你可以保证这辆自行车在你需要时总是可用。你可以装饰它,或者移动它。但这个选择也有一些缺点:自行车很贵。而且如果你买了一辆,你现在就要对它负责。你必须定期维护它。当你最终决定不再需要它时,你必须妥善处理它。
所有权的关键洞见
所有权可能很昂贵。 作为所有者,你有责任获取、管理并妥善处置你所拥有的对象。
观察者模式的优势与限制
在你出门时,你瞥了一眼前窗。你注意到你的邻居把他们的自行车停在了你的窗户对面。你可以直接画一幅你邻居的自行车(从你窗户看到的景象)。这种选择有很多好处。你省去了自己获取一辆自行车的开销。你不需要维护它。你也不负责处理它。当你结束观察时,你只需拉上窗帘,继续你的生活。这结束了对该对象的观察,但对象本身不受此影响。这种选择也有一些潜在的缺点。你不能油漆或定制你邻居的自行车。而且在你观察自行车时,你的邻居可能会决定改变自行车的外观,或者把它完全移出你的视线。你最终可能会看到一些意想不到的东西。
观察者的关键洞见
观察是廉价的。 作为观察者,你对你正在观察的对象没有任何责任,但你也无法控制这些对象。
std::string的所有权机制
你可能想知道为什么 std::string
会对其初始化器进行昂贵的拷贝。当一个对象被实例化时,会为该对象分配内存,以存储其在整个生命周期中需要使用的任何数据。这块内存是为该对象保留的,并保证在该对象存在期间一直存在。这是一个安全的空间。std::string
(以及大多数其他对象)会将其接收到的初始化值拷贝到这块内存中,这样它们以后就可以拥有自己独立的、可供访问和操作的值。一旦初始化值被拷贝,该对象就不再以任何方式依赖于初始化器。
初始化的关键洞见
初始化后的对象无法控制在初始化完成后初始化器会发生什么。
std::string_view的实践应用
最佳使用场景
std::string_view
的最佳用途是作为只读函数参数。这允许我们传入 C 风格字符串、std::string
或 std::string_view
参数,而无需进行拷贝,因为 std::string_view
会创建参数的一个视图。
#include <iostream>
#include <string>
#include <string_view>
void printSV(std::string_view str) // 现在是 std::string_view,创建实参的视图
{
std::cout << str << '\n';
}
int main()
{
printSV("Hello, world!"); // 用 C 风格字符串字面量调用
std::string s2{ "Hello, world!" };
printSV(s2); // 用 std::string 调用
std::string_view s3 { s2 };
printSV(s3); // 用 std::string_view 调用
return 0;
}
常见错误和陷阱
让我们看几个因误用 std::string_view
而导致问题的案例:
#include <iostream>
#include <string>
#include <string_view>
int main()
{
std::string_view sv{}; // 创建一个空的 string_view
{ // 创建一个嵌套块
std::string s{ "Hello, world!" }; // 创建一个属于此嵌套块的局部 std::string
sv = s; // sv 现在正在观察 s
} // s 在此处被销毁,因此 sv 现在正在观察一个无效的字符串
std::cout << sv << '\n'; // 未定义行为
return 0;
}
视图操作技术
视图修改方法
想象一下你房子里的一个窗户,看着街上停着的一辆电动汽车。你可以透过窗户看到汽车,但你不能触摸或移动汽车。你的窗户只是提供了对汽车的观察,汽车是一个完全独立的对象。
#include <iostream>
#include <string_view>
int main()
{
std::string_view str{ "Peach" }; // 初始视图
std::cout << str << '\n'; // 输出 "Peach"
// 从视图的左侧移除 1 个字符
str.remove_prefix(1);
std::cout << str << '\n'; // 输出 "each"
// 从视图的右侧移除 2 个字符
str.remove_suffix(2);
std::cout << str << '\n'; // 输出 "ea"
str = "Peach"; // 重置视图
std::cout << str << '\n'; // 输出 "Peach"
return 0;
}
实用指南
std::string的使用场景
使用 std::string
变量当:
- 你需要一个可以修改的字符串。
- 你需要存储用户输入的文本。
- 你需要存储返回
std::string
的函数的返回值。
std::string_view的使用场景
使用 std::string_view
变量当:
- 你需要只读访问已经存在于其他地方的部分或全部字符串,并且该字符串在
std::string_view
使用完成之前不会被修改或销毁。 - 你需要一个 C 风格字符串的符号常量。
- 你需要继续观察返回 C 风格字符串或非悬空
std::string_view
的函数的返回值。
最佳实践总结
std::string使用要点
- 初始化和拷贝
std::string
是昂贵的,因此应尽可能避免。 - 避免按值传递
std::string
,因为这会进行拷贝。 - 如果可能,避免创建短寿命的
std::string
对象。 - 修改
std::string
会使指向该字符串的所有视图失效。 - 按值返回局部
std::string
是没问题的(得益于返回值优化/RVO 或移动语义)。
std::string_view使用要点
std::string_view
通常用于传递字符串函数参数和返回字符串字面量。- 因为 C 风格字符串字面量存在于整个程序的生命周期中,所以将
std::string_view
设置为 C 风格字符串字面量总是安全的。 - 当一个字符串被销毁时,指向该字符串的所有视图都会失效。
- 使用一个失效的视图(除了使用赋值使其重新生效)将导致未定义行为。
- 一个
std::string_view
可能以空字符结尾,也可能不是。