在之前的课程中,我们介绍了两种字符串类型: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可能以空字符结尾,也可能不是。
