C++ string_view

在 C++ 中,std::string_view 是 C++17 在 <string_view> 头文件中引入的 “只读字符串视图”,用来在不复制底层字符的情况下直接观察现有的文本数据。

你可以把 std::string_view 看作一扇 “只读观察窗口”。底层字符依然存放在原来的 std::string 或字符串字面量里,它自己不保存副本,只记录两个轻量信息:① 从哪个内存地址开始看;② 一共看多少个字符。

C++ string_view 简介

在 C++ 中,使用 std::string_view 之前,我们需要先在代码顶部引入 <string_view> 头文件。

语法:

#include <string_view>

std::string_view 变量名 = 字符串;

说明:

std::string_view 既可以观察现有的 std::string 对象,也可以直接观察字符串字面量或 C 风格字符串。因为它是只读视图,所以我们只能通过下标 [] 读取字符,不能修改字符。比如写出 view[0] = 'M'; 时,编译器会直接报错。

示例 1:创建并读取 string_view

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

int main() {
    std::string lang = "Modern C++";
    std::string_view view1 = lang;
    std::string_view view2 = "Hello";

    std::cout << "view1:" << view1 << '\n';
    std::cout << "view2:" << view2 << '\n';
    std::cout << "view1 的长度:" << view1.size() << '\n';
    std::cout << "view2 的首字符:" << view2[0] << '\n';
    return 0;
}

运行结果如下。

view1:Modern C++
view2:Hello
view1 的长度:10
view2 的首字符:H

分析:

view1 观察的是变量 lang 里的字符,view2 则直接观察字符串字面量 "Hello",整个过程没有复制任何字符。和 std::string 一样,我们可以直接使用 std::cout 输出视图,调用 size() 查看字符个数,或者通过下标 [] 读取指定位置的字符。

不过小伙伴们要注意,因为 std::string_view 自己不分配内存保存新字符,所以我们不能像 std::string 那样直接使用加号(+)把两个视图拼在一起。下面这种写法就是错误的。

std::string_view a = "Hello, ";
std::string_view b = "C++";
auto res = a + b;    // 视图不支持 + 拼接

如果我们在程序中确实需要拼接生成新的字符串,还是应该使用 std::string 来完成。

截取子视图与收缩边界

以前我们调用 std::string 的 substr() 截取子串时,它会创建并返回一个全新的 std::string 对象,这需要重新分配内存并复制一遍字符。

而在 std::string_view 中,无论是截取子视图还是收缩左右边界,都只是调整了一下 “观察窗口” 的起始位置和长度,不会复制或修改底层的字符。常用的范围调整方法有以下 3 个:

  • substr(pos, count):从下标 pos 开始,生成并返回一个长度为 count 的新子视图。如果省略 count 则默认截取到末尾,原视图自身保持不变。
  • remove_prefix(n):直接把当前视图的左边界往右缩进 n 个字符,相当于跳过最前面的 n 个字符。
  • remove_suffix(n):直接把当前视图的右边界往左缩进 n 个字符,相当于去掉最后面的 n 个字符。

示例 2:截取与收缩视图范围

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

int main() {
    std::string rawUrl = "https://www.lvyenet.com";
    std::string_view url = rawUrl;

    std::string_view domain = url.substr(8, 15);
    std::cout << "截取域名:" << domain << '\n';

    domain.remove_prefix(4);    // 跳过开头的 "www."
    domain.remove_suffix(4);    // 去掉末尾的 ".com"

    std::cout << "收缩后视图:" << domain << '\n';
    std::cout << "原字符串:" << rawUrl << '\n';
    return 0;
}

运行结果如下。

截取域名:www.lvyenet.com
收缩后视图:lvyenet
原字符串:https://www.lvyenet.com

分析:

在原网址中,开头的 "https://" 刚好占 8 个字符,因此下标 8 对应的就是 'w'。url.substr(8, 15) 表示从下标 8 开始截取 15 个字符,返回的新视图 domain 观察的就是 "www.lvyenet.com" 这一段。

接着我们调用 domain.remove_prefix(4) 把左边界往右移了 4 个字符,再调用 domain.remove_suffix(4) 把右边界往左缩了 4 个字符,最终观察窗口就落在了中间的 "lvyenet" 上。

很多初学的小伙伴看到函数名里的 remove(移除),容易误以为它把原字符串里的字符删掉了。其实从最后一行输出可以看出,底层的 rawUrl 依然完整保留着原内容,这两个方法只是把 domain 的观察窗口缩窄了。此外,remove_prefix() 和 remove_suffix() 的返回类型都是 void,它们直接修改当前视图自身,不能拿新变量去接收返回值。

注意 经过 substr() 或 remove_suffix() 缩小范围后,视图末尾通常不再是空字符 '\0'。比如上面 domain 缩窄到 "lvyenet" 后,它在原内存里紧挨着的下一个字符其实是 .com 里的 '.'。因此,我们千万不要通过 domain.data() 取出字符指针传给 printf("%s", ...) 或 strlen() 这类要求以 '\0' 结尾的函数,否则会一直往后读出多余的字符。

使用 string_view 作为函数参数

在实际开发中,std::string_view 最常用的场景就是充当 “只读取文本” 的函数形参。

如果一个函数只打算读取传入的字符串,以前我们通常会把形参写成 const std::string&。但当调用者直接传入双引号字符串字面量时,编译器还是得在后台临时创建一个 std::string 对象并复制一遍字符。

把函数形参改成 std::string_view 之后,无论是传入 std::string、字符串字面量,还是截取出来的子视图,都可以直接按零复制的方式接收。下面我们来看一个例子。

示例 3:接收只读字符串参数

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

void showTitle(std::string_view title) {
    std::cout << "课程标题:" << title << '\n';
}

int main() {
    std::string course = "C++ 教程";
    std::string_view view = course;

    showTitle(course);               // 传入 std::string
    showTitle("Python 教程");        // 传入字符串字面量
    showTitle(view.substr(0, 3));    // 传入截取的子视图
    return 0;
}

运行结果如下。

课程标题:C++ 教程
课程标题:Python 教程
课程标题:C++

分析:

在这个例子中,showTitle() 的形参直接写成了按值传递的 std::string_view title,而没有加上 & 引用符号。这是因为一个 std::string_view 对象内部只保存了 “一个地址指针 + 一个长度整数”,在 64 位系统上只占 16 个字节。直接按值传递比传引用还要轻便,因此我们不需要写成 const std::string_view&。

不过在使用 std::string_view 时,小伙伴们必须记牢一条规则:视图本身不拥有字符数据,使用期间底层的原字符串必须一直有效。如果我们在函数里返回局部字符串的视图就会出错,比如下面这样:

std::string_view getBadName() {
    std::string name = "Admin";
    return name;    // 局部变量即将销毁
}

在上面代码中,name 是函数内部的局部变量,函数结束时就会被销毁。此时返回的视图就像悬空指针一样指向了已释放的内存,继续读取可能输出乱码或导致程序崩溃。如果函数需要返回新生成的字符串,返回类型应该写成 std::string。

常见问题

1. 既然 std::string_view 这么轻便,能不能使用它全面替代 std::string?

不能。这两者的分工完全不同:std::string 负责 “拥有并保管” 字符串数据,而 std::string_view 只是临时 “借用观察”。凡是需要长期保存字符串、拼接修改内容,或者作为函数新生成的字符串返回值时,我们都必须使用 std::string。而在只读函数形参或临时切分现有文本时,我们优先使用 std::string_view。

2. 如何把 std::string_view 转换成独立的 std::string?

把视图转换成 std::string 需要重新分配内存并复制字符。为了防止在不知情时产生隐式复制开销,C++ 不允许我们直接写成 std::string text = view;。如果确实需要把视图 view 独立保存下来,只需要写成 std::string text(view); 显式构造一个新的 std::string 对象即可。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号