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的核心逻辑:
实际项目中的应用
我在解析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