C++std::string_view完全指南:高性能字符串处理详解

在之前的课程中,我们介绍了两种字符串类型:std::string(5.7 – std::string简介)和 std::string_view(5.8 – std::string_view简介)。

因为 std::string_view 是我们首次接触视图类型,我们将花更多时间进一步讨论它。我们将重点讨论如何安全地使用 std::string_view,并提供一些说明如何错误使用它的示例。最后,我们将给出一些关于何时使用 std::stringstd::string_view 的指导原则。

理解所有权与观察者模式

所有权的概念与成本

让我们暂时引入一个类比。假设你决定要画一幅自行车的画。但你没有自行车!你该怎么办?

嗯,你可以去当地的自行车行买一辆。这样你就拥有了那辆自行车。这样做有一些好处:你现在拥有一辆可以骑的自行车。你可以保证这辆自行车在你需要时总是可用。你可以装饰它,或者移动它。但这个选择也有一些缺点:自行车很贵。而且如果你买了一辆,你现在就要对它负责。你必须定期维护它。当你最终决定不再需要它时,你必须妥善处理它。

所有权的关键洞见

所有权可能很昂贵。 作为所有者,你有责任获取、管理并妥善处置你所拥有的对象。

观察者模式的优势与限制

在你出门时,你瞥了一眼前窗。你注意到你的邻居把他们的自行车停在了你的窗户对面。你可以直接画一幅你邻居的自行车(从你窗户看到的景象)。这种选择有很多好处。你省去了自己获取一辆自行车的开销。你不需要维护它。你也不负责处理它。当你结束观察时,你只需拉上窗帘,继续你的生活。这结束了对该对象的观察,但对象本身不受此影响。这种选择也有一些潜在的缺点。你不能油漆或定制你邻居的自行车。而且在你观察自行车时,你的邻居可能会决定改变自行车的外观,或者把它完全移出你的视线。你最终可能会看到一些意想不到的东西。

观察者的关键洞见

观察是廉价的。 作为观察者,你对你正在观察的对象没有任何责任,但你也无法控制这些对象。

std::string的所有权机制

你可能想知道为什么 std::string 会对其初始化器进行昂贵的拷贝。当一个对象被实例化时,会为该对象分配内存,以存储其在整个生命周期中需要使用的任何数据。这块内存是为该对象保留的,并保证在该对象存在期间一直存在。这是一个安全的空间。std::string(以及大多数其他对象)会将其接收到的初始化值拷贝到这块内存中,这样它们以后就可以拥有自己独立的、可供访问和操作的值。一旦初始化值被拷贝,该对象就不再以任何方式依赖于初始化器。

初始化的关键洞见

初始化后的对象无法控制在初始化完成后初始化器会发生什么。

std::string_view的实践应用

最佳使用场景

std::string_view 的最佳用途是作为只读函数参数。这允许我们传入 C 风格字符串、std::stringstd::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 可能以空字符结尾,也可能不是。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"