字符串视图(非拥有视图)- string_view

字符串视图(非拥有视图)- string_view
苏丙榅1. std::string_view 的登场
在 C++17 之前,当我们需要编写一个函数来处理字符串时,通常会面临一个艰难的选择:
- 使用
const std::string&- 优点:安全,避免拷贝。
- 缺点:如果参数是一个字符串字面量(如
"hello")或者是一个 C 风格字符串(如char*),编译器必须隐式地创建一个临时的std::string对象。这意味着会发生内存分配,这是非常昂贵的操作!
- 使用
const char*- 优点:速度快,没有内存分配,可以接受字面量。
- 缺点:不安全,不能获取字符串长度(必须调用
strlen),不能直接使用 STL 算法,一旦涉及到std::string成员就麻烦了。
std::string_view 是 C++17 引入的一个只读引用包装器。我们可以把它想象成指向字符串的窗户:它只负责观察字符串的数据,而不拥有字符串的数据。
std::string_view 的构造函数设计得非常宽容,下面这些类型都可以隐式转换为 string_view:
- 标准字符串:
std::string - C 风格字符串:
const char* - 字符串字面量:
"hello world" - 字符数组:
char buffer[100] - 迭代器范围:任意指向字符的
begin/end迭代器对(如std::vector<char>的子集)。 - 其他 string_view:从另一个
string_view构造。
如果编写一个函数需要接收这些不同类型的字符串,我们可以将函数参数声明为 std::string_view,这样就可以直接传入上述任何一种类型,而无需修改调用代码。
1 | // 以前:你需要三个重载 |
零开销是 C++ 的核心哲学,这意味着:不使用某个特性时,不需要为它付出代价;当使用它时,不会付出比手动实现更高的代价。string_view 做到了几乎零开销。所谓的几乎,仅仅是指它本身作为对象存在时占用极小的栈内存(通常是指针和大小共 16 字节),除此之外几乎没有额外代价。具体体现在 以下三个方面:
没有内存分配:这是
string_view最显著的优势,也是与std::string最大的区别- 当构造一个
std::string时(例如从const char*),通常会涉及到动态内存分配(堆内存申请),以便存储字符数据的副本。如果频繁发生(如在循环中创建临时对象),会严重拖累性能,并造成内存碎片。 - 当构造一个
string_view时:它只读不拥有。它只是简单地记录了原始字符串的起始地址和一个长度。底层的数据仍然留在原来的地方(静态区、堆上或栈上),没有任何malloc或new操作。
- 当构造一个
避免原始数据拷贝
std::string的拷贝:为了拥有数据所有权,拷贝一个字符串需要将每一个字符从源内存复制到目标内存。如果字符串很长,这个开销是巨大的。string_view的拷贝:拷贝一个string_view只需要拷贝两个标量值(指针ptr和长度len)。无论它指向的字符串是 10 个字符还是 10GB,拷贝string_view的时间复杂度永远是 **O(1)**,且速度极快(等价于拷贝两个整数)。
零虚函数开销
string_view是一个轻量级的类模板(类似于struct),它内部没有任何虚函数。这意味着它既没有虚函数表指针带来的内存占用,也没有通过虚函数表进行间接函数调用的运行时开销。现代编译器可以对其进行极其激进的优化,甚至完全优化掉其存在。
下面的示例代码演示了std::string_view 对象的创建与初始化:
1 |
|
重要提示:
std::string_view不拥有数据,如果它指向的原始数据被销毁了,string_view就变成了野指针,访问它会导致未定义行为。所以原始数据必须比string_view寿命更长!
1 | // 危险!不要这样做! |
2. 核心特性
2.1 只读视图
string_view 提供了和 std::string 类似的接口,但都是只读的:
1 |
|
string_view 只是一个观察者,它并不保证你看到的数据末尾有一个 \0(空字符)。接下来我们对比一下 std::string 和 string_view 的区别:
C++ 字符串
- 在 C 和 C++ 中,标准的 C 风格字符串(
const char*)和std::string总是以一个空字符\0结尾。 - 因为末尾有
\0,像strlen、printf或std::cout << char*这样的函数只要拿到一个指针,就可以一直向后读,直到遇到\0停下。
- 在 C 和 C++ 中,标准的 C 风格字符串(
std::string_view:string_view是由一个指针(起始位置)和一个长度组成的,它不像std::string那样强制要求字符串必须有\0。它只关心从指针开始,往后数 N 个字节都是我的数据。这就导致了两种危险情况:
子字符串切片:从一个字符串中取子串时,
string_view仅仅是指向了中间的某一段。1
2
3std::string s = "Hello, World!";
std::string_view sv(s.data(), 5); // 指向 "Hello" 这一部分
// sv 内部只记录了 长度=5sv的末尾是逗号,再往后紧跟着空格,根本没有\0。非空终止的缓冲区:可以直接从字符数组构造
string_view,那个数组可能压根就没放\0。1
2char buffer[] = {'H', 'i', '!'}; // 注意:没有 \0
std::string_view sv(buffer, 3);
再看示例中的这行代码:
1 | std::cout << "Data pointer: " << static_cast<const void*>(sv.data()) << std::endl; |
sv.data():返回指向字符串起始位置的const char*。static_cast<const void*>(...):这是一个关键的类型转换。它强行把这个指针当成无类型指针输出。
为什么要转成 void*?
如果不转,直接写 std::cout << sv.data(),std::cout 会以为我们传给它一个 C 风格字符串(const char*),它就会尝试从那个地址开始打印字符,直到遇到 \0 才停止。
结合上面描述的两种危险情况,如果我们打印的是 sv("Hello", 5):
sv.data()返回指向'H'的指针。- 如果不转
void*,cout会打印:Hello, World!(它不知道sv认为字符串在第5个字符结束了,它会一路读下去,直到原来的string结尾)。 - 如果转成
void*,cout就只会打印这个内存地址(例如0x7ffee4),而不会试图去读取里面的内容。
2.2 零开销子串操作
子串操作这是 std::string_view 最强大的功能之一。std::string 的 substr 操作会返回一个新的 std::string 对象,这意味着新的内存分配和数据拷贝。如果你只需要读取字符串的一部分,这纯粹是浪费。而 std::string_view 的 substr 操作只调整指针和长度,完全不拷贝数据。
1 |
|
std::string_view 内部通常只包含两个成员(类似于 struct { const char* ptr; size_t len; }):
- 指针:指向字符串数据的起始位置。
- 长度:字符串的长度。
当我们调用 std::string_view sub1 = sv.substr(4, 5); 时:
- 没有内存分配:完全不涉及
new或malloc。 - 没有内存复制:CPU 不需要搬运任何字符数据。
- 仅修改指针和长度:编译器只需要生成几条极其简单的指令:
- 计算新指针:
新指针 = 旧指针 + 4 - 设定新长度:
新长度 = 5
- 计算新指针:
2.3 移除前缀和后缀
std::string_view 非常适合解析和分割操作。它提供了两个极其强大的成员函数来修改视图范围:
remove_prefix(size_t n):从视图的起始位置移除n个字符。视图向后滑动。remove_suffix(size_t n):从视图的末尾移除n个字符。视图向前收缩。
这两个方法都是 O(1) 操作,它们只修改内部存储的 指针 和 长度 变量。
1 |
|
std::string_view 的原地修改能力,使其成为了无拷贝解析器的理想构建块。
- 它把 字符串处理 变成了 指针游走。
- 它把 数据拷贝 变成了 整数加减。
在写高性能代码时,只要发现自己在不断地 substr 并扔掉旧字符串,请立即考虑使用 remove_prefix/remove_suffix 来重构你的逻辑!
2.4 与 std::string 的互转
std::string_view 是只读的,核心原因是它不拥有数据。因此不能通过它修改字符串的内容。如果需要修改,可以把它转换回 std::string。
std::string_view 提供的接口全是 const 的。例如:
front()返回的是const char&operator[]返回的是const char&- 没有
data()的 非 const 重载版本(这与 C++17 的std::string::data()不同)。
C++17 后,std::string 增加了接受 string_view 版本的构造函数和函数:
1 |
|
3. 实战案例
在很多高级编程中,我们需要拆分字符串。传统做法会生成很多小 string 对象,导致大量内存分配。使用 string_view 可以实现零拷贝拆分。
1 |
|
std::string_view::npos 是 C++ 标准库中定义的一个非常关键的静态常量,用于处理字符串查找操作中的“未找到”或“最大长度”的情况。简单来说,它是 std::string_view 内部类型 size_t 的最大值。含义有两种:
- 表示 无效位置 或 未找到。
- 表示 直到字符串末尾。
所有其核心用途也是有两种:
查找操作的返回值
std::string_view::npos最常见的用途是检查查找函数(如find,rfind,find_first_of等)是否成功。截取剩余所有字符
利用
npos作为计数参数,可以方便地表示从某处开始,一直到字符串结束。这在使用substr时非常常见。通常substr的用法是:substr(pos = 0, len = npos)。substr函数内部的逻辑大致是:实际长度 = min(请求长度, 剩余长度)请求长度是npos(巨大无比),所以min操作会自动选取剩余长度。这样就实现了取到末尾。

















