27、C++17 核心特性:std::string_view,高效字符串视图,避免拷贝

字符串操作,是C++里最频繁、也最容易出性能问题的地方之一。

我早年做后台服务时,就踩过一个坑:一个日志模块每秒要处理上万条字符串拼接和截取,结果内存分配成了瓶颈,CPU时间全花在malloc和free上。后来用上string_view,性能直接翻倍。

说白了,std::string_view就是一个“只读的字符串窗口”。它不拥有数据,只持有指针和长度。你不需要拷贝字符串,就能完成查找、比较、截取等操作。

为什么需要 string_view?

传统C++里,函数传字符串通常用const std::string&。这有什么问题?

  • 如果你传一个const char*,会隐式构造一个临时std::string,分配内存。
  • 如果你传一个子串,得先substr(),又拷贝一次。
  • 如果你只是读一下,根本不需要拥有数据。

你想想看,一个只读操作,却要触发堆内存分配,是不是很冤?

核心思想:string_view 不分配内存,不拷贝字符,只记录“从哪里看,看多长”。

基本用法

先看一个最简单的例子:

#include <string_view>
#include <iostream>

void print_length(std::string_view sv) {
    std::cout << "长度: " << sv.size() << '\n';
}

int main() {
    // 从 const char* 构造
    print_length("Hello, World!");

    // 从 std::string 构造
    std::string s = "C++17";
    print_length(s);

    // 从子串构造
    print_length(std::string_view(s.data() + 2, 3));
    return 0;
}

注意,这里没有一次内存分配。string_view只是“看着”已有的字符串数据。

常用操作

我整理了一下,日常最常用的操作就这些:

操作 说明 示例
sv.size() 获取长度 sv.size()
sv.empty() 是否为空 if (sv.empty())
sv[0] 下标访问 char c = sv[0];
sv.substr(pos, count) 取子串(返回string_view) auto sub = sv.substr(2, 3);
sv.find("needle") 查找子串 auto pos = sv.find("abc");
sv.compare(other) 比较 if (sv.compare("abc") == 0)
sv.data() 获取底层指针 const char* p = sv.data();

提示:substr()返回的是新的string_view,不拷贝字符。这在解析协议、处理大文本时特别有用。

避坑指南:生命周期问题

我曾经在代码review时,看到有人这样写:

std::string_view get_name() {
    std::string name = "Alice";
    return name;  // 危险!
}

为什么危险?string_view不拥有数据,它只是“看着”name。函数返回后,name被销毁,string_view就成了悬空指针。

嗯,这里要注意:string_view 的生命周期不能超过它指向的数据的生命周期

警告:永远不要返回指向局部变量的 string_view。如果你需要返回字符串,请用 std::string。

性能对比:string vs string_view

我写了个简单的测试,对比一下:

// 传统方式
void process_string(const std::string& s) {
    auto sub = s.substr(0, 100);  // 拷贝100个字符
    // 处理 sub...
}

// 使用 string_view
void process_view(std::string_view sv) {
    auto sub = sv.substr(0, 100);  // 不拷贝,只记录指针和长度
    // 处理 sub...
}

在大量调用时,string_view版本的内存分配次数是0,而传统版本每次substr都会分配内存。我实测过,处理10万次调用,性能差距在5倍以上。

与 const char* 的对比

有人可能会问:用const char*不也能避免拷贝吗?

没错,但const char*有几个痛点:

  • 不知道长度,得用strlen(),O(n)复杂度。
  • 不支持substr()find()等方便操作。
  • 不能处理含有'\0'的字符串。

string_view完美解决了这些问题。它知道长度,支持所有常用操作,而且能处理二进制数据。

核心知识结构图

下面这张图,帮你理清string_view的核心逻辑:

std::string_view 核心知识结构 std::string_view 不拥有数据 零拷贝操作 兼容多种源 只持有指针 + 长度 不管理内存 substr() 不拷贝 find() 不拷贝 const char* std::string ⚠ 注意:不能返回局部变量的 string_view

实际项目中的应用

我在解析HTTP请求头时,大量使用了string_view。比如:

std::string_view parse_header_value(std::string_view header) {
    auto colon_pos = header.find(':');
    if (colon_pos == std::string_view::npos) {
        return {};
    }
    // 跳过冒号和空格
    auto value_start = header.find_first_not_of(" :", colon_pos + 1);
    if (value_start == std::string_view::npos) {
        return {};
    }
    return header.substr(value_start);
}

整个解析过程,没有一次内存分配。这在处理高并发请求时,优势非常明显。

总结

  • string_view是只读的字符串视图,不拥有数据。
  • 所有操作都是零拷贝,性能极高。
  • 注意生命周期,不要返回局部变量的string_view
  • 适合作为函数参数、解析协议、处理大文本等场景。

个人建议:如果你的函数只需要读取字符串,不修改也不存储,优先用std::string_view。但如果你需要修改或长期持有,还是用std::string


公众号:蓝海资料掘金营,微信deep3321