5-9 std::string_view(第二部分)

在之前的课程中,我们介绍了两种字符串类型: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 会复制初始化器,它无需担心初始化完成后初始化器的状态变化。初始化对象被销毁或修改时,不会影响 std::string。其代价是这种独立性需要付出昂贵的复制成本。

在类比中,std::string 是所有者——它负责从初始化对象获取字符串数据,管理字符串数据的访问权限,并在 std::string 对象销毁时正确处置字符串数据。

关键洞见
在编程中,当我们称某个对象为所有者时,通常意味着它是该数据的唯一所有者(除非另有说明)。唯一所有权(也称单一所有权)确保了数据责任归属的明确性。


我们并不总是需要副本

让我们重新审视上一课中的这个例子:

#include <iostream>
#include <string>

void printString(std::string str) // str makes a copy of its initializer
{
    std::cout << str << '\n';
}

int main()
{
    std::string s{ "Hello, world!" };
    printString(s);

    return 0;
}

当调用 printString(s) 时,str 会创建 s 的昂贵副本。该函数打印复制的字符串后即销毁它。

请注意 s 本身已持有待打印的字符串。我们能否直接使用 s 持有的字符串,而无需创建副本?答案是可能的——我们需要评估三个条件:

  • s 是否可能在 str 仍在使用时被销毁?不会,str 在函数结束时销毁,而 s 存在于调用者作用域内,函数返回前无法被销毁。
  • s 是否可能在 str 仍在使用时被修改?不会,str 在函数结束时销毁,调用者在函数返回前没有机会修改 s。
  • str是否会以调用方无法预期的方式修改字符串?不会,该函数完全不会修改字符串。

由于上述三个条件均不成立,直接使用s持有的字符串而非创建副本是安全的。况且字符串复制成本高昂,何必为不必要的操作付出代价?


std::string_view 是一个观察者

std::string_view 采用不同的初始化方式。它不会创建初始化字符串的昂贵副本,而是创建一个低成本的初始化字符串视图。此后,当需要访问该字符串时,即可使用 std::string_view。

在我们的类比中,std::string_view 相当于一个观察者。它仅能观察已存在的对象,无法修改该对象。当视图被销毁时,被观察的对象不受影响。多个观察者同时观察同一个对象是完全可行的。

需要特别注意的是,std::string_view 在其生命周期内始终依赖于初始化对象。若在视图被使用期间被观察的字符串被修改或销毁,将导致意外或未定义的行为。

使用视图时,必须由开发者确保避免此类情况发生。

警告
视图依赖于被观察对象。若在视图使用期间被观察对象遭修改或销毁,将导致意外或未定义行为。

观察已销毁字符串的 std::string_view 有时被称为悬空dangling视图。


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) // now a std::string_view, creates a view of the argument
{
    std::cout << str << '\n';
}

int main()
{
    printSV("Hello, world!"); // call with C-style string literal

    std::string s2{ "Hello, world!" };
    printSV(s2); // call with std::string

    std::string_view s3 { s2 };
    printSV(s3); // call with std::string_view

    return 0;
}

由于字符串函数形参在控制权返回调用方之前已完成创建、初始化、使用和销毁,因此被观察的字符串(函数形参)在我们的字符串参数之前被修改或销毁的风险不存在。


我应该优先选择 std::string_view 还是 const std::string& 作为函数参数?(进阶)

大多数情况下应优先选择 std::string_view。我们在第 12.6 课——按常量左值引用传递中将进一步探讨此主题。


错误使用 std::string_view

让我们看看几个因误用 std::string_view 而引发问题的案例。

以下是第一个示例:

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string_view sv{};

    { // create a nested block
        std::string s{ "Hello, world!" }; // create a std::string local to this nested block
        sv = s; // sv is now viewing s
    } // s is destroyed here, so sv is now viewing an invalid string

    std::cout << sv << '\n'; // undefined behavior

    return 0;
}

image

在此示例中,我们于嵌套代码块内创建了 std::string s(暂不必深究嵌套代码块的概念)。随后将 sv 设置为 s 的视图。当嵌套代码块结束时,s 被销毁。sv 并不知晓 s 已被销毁。此时使用 sv 时,我们正在访问无效对象,从而导致未定义行为。

以下是相同问题的另一种变体,其中我们用函数返回的 std::string 初始化 std::string_view:

#include <iostream>
#include <string>
#include <string_view>

std::string getName()
{
    std::string s { "Alex" };
    return s;
}

int main()
{
  std::string_view name { getName() }; // name initialized with return value of function
  std::cout << name << '\n'; // undefined behavior

  return 0;
}

image

此行为与前例类似。getName()函数返回一个包含字符串“Alex”的std::string。返回值是临时对象,在包含函数调用的完整表达式结束时会被销毁。我们必须立即使用该返回值,或将其复制以供后续使用。

但 std::string_view 不会创建副本。它会创建指向临时返回值的视图,该返回值随后会被销毁。这导致我们的 std::string_view 成为悬空对象(指向无效对象),打印该视图将导致未定义行为。

以下是上述情况的一个不太明显的变体:

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    using namespace std::string_literals;
    std::string_view name { "Alex"s }; // "Alex"s creates a temporary std::string
    std::cout << name << '\n'; // undefined behavior

    return 0;
}

一个通过字符串后缀 s 创建的 std::string 字面量会生成一个临时 std::string 对象。因此在此例中,“Alex”s 创建了一个临时 std::string,随后我们将其用作 name 的初始化值。此时 name 指向该临时 std::string。随后临时 std::string 被销毁,导致 name 悬空。当我们继续使用 name 时,将引发未定义行为。

警告

请勿使用 std::string 字面量初始化 std::string_view,否则将导致 std::string_view 悬空。

使用C风格字符串常量或std::string_view常量初始化是允许的。使用C风格字符串对象、std::string对象或std::string_view对象初始化也允许,前提是该字符串对象的生命周期长于视图对象。

当底层字符串被修改时,我们也会遇到未定义行为:

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string s { "Hello, world!" };
    std::string_view sv { s }; // sv is now viewing s

    s = "Hello, a!";    // modifies s, which invalidates sv (s is still valid)
    std::cout << sv << '\n';   // undefined behavior

    return 0;
}

(此处测试结果有点差错, clang version 21.1.8 (Fedora 21.1.8-4.fc43)
打印sv并未出现失效, 见对于高级读者第二条)
image

在此示例中,sv 再次被设置为查看 s。随后 s 被修改。当修改 std::string 时,指向该字符串的所有视图都可能失效invalidated,这意味着这些视图现在已无效或不正确。使用失效的视图将导致行为未定义。

对于高级读者

  • 如果 std::string 为容纳新字符串数据而重新分配内存,它会将旧字符串数据使用的内存归还给操作系统。由于 std::string_view 仍在查看旧字符串数据,此时它便成为悬空指针(指向现已无效的对象)。

  • 若 std::string 未重新分配内存,则会将新字符串数据覆盖旧数据(起始于相同内存地址)。此时 std::string_view 虽指向新字符串数据(因其位于原地址),但无法察觉 std::string 的长度可能已改变。若新字符串长度大于旧字符串,std::string_view将仅能访问新字符串中与旧字符串长度相同的子字符串。若新字符串长度小于旧字符串,std::string_view将访问新字符串的超字符串(包含完整新字符串及字符串末尾之后内存中残留的任意垃圾字符)。

关键要点
修改 std::string 可能导致所有指向该字符串的视图失效。


重新验证无效的 std::string_view

失效的对象通常可通过将其恢复至已知良好状态来重新验证(使其重新有效)。对于失效的 std::string_view,我们可通过为该对象赋予有效的字符串视图来实现。

以下是与先前相同的示例,但我们将重新验证 sv:

#include <iostream>
#include <string>
#include <string_view>

int main()
{
    std::string s { "Hello, world!" };
    std::string_view sv { s }; // sv is now viewing s

    s = "Hello, universe!";    // modifies s, which invalidates sv (s is still valid)
    std::cout << sv << '\n';   // undefined behavior

    sv = s;                    // revalidate sv: sv is now viewing s again
    std::cout << sv << '\n';   // prints "Hello, universe!"

    return 0;
}

image

当sv因s的修改而失效后,我们通过语句sv = s重新验证sv,这使得sv再次成为s的有效视图。第二次打印sv时,它会输出“Hello, universe!”。


请谨慎使用 std::string_view 作为函数返回值

虽然 std::string_view 可用作函数的返回值,但此操作往往存在风险。

由于局部变量在函数结束时会被销毁,若返回的 std::string_view 指向局部变量,则该返回值将失效,后续使用该 std::string_view 可能导致未定义行为。例如:

#include <iostream>
#include <string>
#include <string_view>

std::string_view getBoolName(bool b)
{
    std::string t { "true" };  // local variable
    std::string f { "false" }; // local variable

    if (b)
        return t;  // return a std::string_view viewing t

    return f; // return a std::string_view viewing f
} // t and f are destroyed at the end of the function

int main()
{
    std::cout << getBoolName(true) << ' ' << getBoolName(false) << '\n'; // undefined behavior

    return 0;
}

image

在上例中,当调用 getBoolName(true) 时,该函数返回一个指向 t 的 std::string_view。然而 t 在函数结束时被销毁。这意味着返回的 std::string_view 指向的对象已被销毁。因此当打印该返回的 std::string_view 时,将导致未定义行为。

编译器可能对此类情况发出警告,也可能不会。

安全返回 std::string_view 的主要情形有两种:首先,由于 C 风格字符串常量贯穿整个程序,因此从返回类型为 std::string_view 的函数中返回 C 风格字符串常量是可行的(且有用的)。

#include <iostream>
#include <string_view>

std::string_view getBoolName(bool b)
{
    if (b)
        return "true";  // return a std::string_view viewing "true"

    return "false"; // return a std::string_view viewing "false"
} // "true" and "false" are not destroyed at the end of the function

int main()
{
    std::cout << getBoolName(true) << ' ' << getBoolName(false) << '\n'; // ok

    return 0;
}

这将输出:

image

当调用 getBoolName(true) 时,该函数将返回一个指向 C 风格字符串 “true” 的 std::string_view。由于 ‘true’ 在整个程序中都存在,因此在 main() 中使用返回的 std::string_view 打印 “true” 不会有问题。

其次,返回 std::string_view 类型的函数形参通常是可行的:

#include <iostream>
#include <string>
#include <string_view>

std::string_view firstAlphabetical(std::string_view s1, std::string_view s2)
{
    if (s1 < s2)
        return s1;
    return s2;
}

int main()
{
    std::string a { "World" };
    std::string b { "Hello" };

    std::cout << firstAlphabetical(a, b) << '\n'; // prints "Hello"

    return 0;
}

image

这为何可行可能不太明显。首先需注意,实参 a 和 b 存在于调用者的作用域中。当函数被调用时,函数形参 s1 是对 a 的视图,函数形参 s2 是对 b 的视图。当函数返回 s1 或 s2 时,它实际上是将对 a 或 b 的视图返回给调用者。由于此时 a 和 b 仍存在,使用返回的指向 a 或 b 的 std::string_view 完全合理。

这里存在一个关键细节:若实参是临时对象(将在包含函数调用的完整表达式结束时被销毁),则 std::string_view 的返回值必须在同一表达式中使用。一旦超出该表达式作用域,临时对象被销毁后,std::string_view 将成为悬空引用。

警告
若参数是将在包含函数调用的完整表达式结束时销毁的临时对象,则必须立即使用返回的 std::string_view,否则该引用将在临时对象销毁后成为悬空引用。


视图修改函数

想象你家的一扇窗户,正对着街道上停放的电动汽车。透过窗户你能看见汽车,却无法触碰或移动它。这扇窗户仅提供观察汽车的视角,而汽车本身是完全独立的对象。

许多窗户装有窗帘,让我们能调整视野范围。拉上左右任一侧的窗帘即可缩小可见区域——我们并未改变窗外景物,只是缩减了可视范围。

由于 std::string_view 是视图类,它提供了通过“拉窗帘”来调整视野的功能。这些操作不会修改被查看的字符串本身,仅改变视图的呈现方式。

  • remove_prefix() 成员函数从视图左侧移除字符。
  • remove_suffix() 成员函数从视图右侧移除字符。
#include <iostream>
#include <string_view>

int main()
{
	std::string_view str{ "Peach" };
	std::cout << str << '\n';

	// Remove 1 character from the left side of the view
	str.remove_prefix(1);
	std::cout << str << '\n';

	// Remove 2 characters from the right side of the view
	str.remove_suffix(2);
	std::cout << str << '\n';

	str = "Peach"; // reset the view
	std::cout << str << '\n';

	return 0;
}

该程序产生以下输出:

image

与真实窗帘不同,一旦调用了 remove_prefix() 和 remove_suffix() 方法,重置视图的唯一方式就是重新将源字符串赋值给它。


std::string_view 可用于查看子字符串

这引出了 std::string_view 的一个重要用途。虽然 std::string_view 可用于查看整个字符串而不创建副本,但当我们需要查看子字符串substring而不创建副本时,它同样非常有用。子字符串是指现有字符串中连续的字符序列。例如,在字符串“snowball”中,“snow”、“all”和“now”都是子字符串。而“owl”并非“snowball”的子字符串,因为这些字符在“snowball”中不连续出现。


std::string_view 可能为空终止也可能不是

能够仅查看更大字符串的子字符串这一特性带来一个值得注意的后果:std::string_view 可能为空终止也可能不是。

相关内容
我们在第5.2课——字面量中介绍了什么是空终止字符串。

考虑字符串“snowball”,它是一个空终止字符串(因为它是C风格字符串字面量,这类字符串总是以空字符终止)。如果std::string_view查看整个字符串,那么它查看的就是一个空终止字符串。但若 std::string_view 仅指向子字符串“now”,则该子字符串不带空终止符(其后紧接字符‘b’)。

核心要点
C 风格字符串常量与 std::string 始终带空终止符。
std::string_view 可能带空终止符,也可能不带。

在绝大多数情况下这无关紧要——std::string_view会自行记录所查看字符串或子字符串的长度,因此无需依赖空终止符。将std::string_view转换为std::string时,无论其是否为空终止字符串均可正常转换。

警告
请避免编写任何假设 std::string_view 为空终止的代码。

提示
若您持有非空终止的 std::string_view,且因某种需求需要空终止字符串,请将 std::string_view 转换为 std::string。


关于何时使用 std::string 与 std::string_view 的快速指南

本指南并非详尽无遗,旨在突出最常见的场景:

变量

在以下情况下使用 std::string 变量:

  • 需要可修改的字符串。
  • 需要存储用户输入的文本。
  • 需要存储返回 std::string 的函数的返回值。

使用 std::string_view 变量的情形:

  • 需要对已存在于其他位置的字符串进行只读访问,且在使用 std::string_view 期间该字符串不会被修改或销毁。
  • 需要为 C 风格字符串定义符号常量。
  • 需要继续查看返回 C 风格字符串或非悬空 std::string_view 的函数的返回值。

函数形参
在以下情况下使用 std::string 函数参数:

  • 函数需要修改作为参数传递的字符串而不影响调用方(此情况较少见)。
  • 使用 C++14 或更早版本语言标准且尚未熟悉引用机制。

在以下情况下使用 std::string_view 函数参数:

  • 函数需要只读字符串。
  • 函数需要处理非空终止字符串。

对于进阶读者

12.6节——常量左值引用传递

使用const std::string&函数参数的情形:

  • 当使用C++14及更早版本语言标准,且函数需要操作只读字符串时(因std::string_view需C++17才可用)。
  • 当调用其他需要 const std::string、const std::string& 或 const C 风格字符串的函数时(因 std::string_view 可能未以空字符终止)。

使用 std::string& 函数参数的情形:


返回类型
在以下情况下使用 std::string 返回类型:

  • 返回值是 std::string 局部变量或函数参数时。
  • 返回值是按值返回 std::string 的函数调用或运算符时。

使用 std::string_view 返回类型的情况:

  • 函数返回 C 风格字符串常量或通过 C 风格字符串常量初始化的局部 std::string_view。
  • 函数返回 std::string_view 参数。

对于进阶读者

参见第 12.12 节——按引用返回与地址返回,了解引用类型的返回机制。
使用 std::string_view 返回类型的情形:

  • 为 std::string_view 成员编写访问器。

使用 std::string& 返回类型的情形:

  • 函数返回 std::string& 参数。

使用 const std::string& 返回类型的情形:

  • 函数返回 const std::string& 参数。
  • 为 std::string 或 const std::string 成员编写访问器时。
  • 函数返回静态(局部或全局)const std::string 时。

要点
关于 std::string 的注意事项:

  • 初始化与复制 std::string 成本高昂,应尽可能避免。
  • 避免按值传递 std::string,这会生成副本。
  • 尽可能避免创建短寿命的 std::string 对象。
  • 修改 std::string 将导致所有指向该字符串的视图失效。
  • 返回局部 std::string 时按值传递是安全的。

关于 std::string_view 的注意事项:

  • std::string_view 通常用于传递字符串函数参数和返回字符串常量。
  • 由于C风格字符串常量在整个程序中存在,将std::string_view赋值给C风格字符串常量始终安全。
  • 字符串销毁时,所有指向该字符串的视图均失效。
  • 使用失效视图(通过赋值重新验证视图除外)将导致未定义行为。
  • std::string_view可能为空终止,也可能不是。
posted @ 2026-02-17 16:22  游翔  阅读(1)  评论(0)    收藏  举报