更新时间: 2026-08-27
33 篇讲 std::string 时提过:把字符串传来传去,const std::string& 已经能避免拷贝。那 std::string_view(C++17)还有什么存在价值?答案是——连"构造 string"这一步都省了。它是"只读地看一段字符",不管这段字符属于谁。
本文要回答:string_view 和 const string& 差在哪?函数参数什么时候该用它?它最大的坑(悬垂)怎么防?
一、string_view 是什么
一句话:string_view 是"字符串的只读窗口"——它存了一个 const char* 指针和一个长度,指向"别人拥有的字符序列",自己不拥有、不复制、也不能改。
#include <string_view>
#include <iostream>
std::string_view sv("hello world"); // 指向字符串字面量
std::cout << sv.substr(0, 5); // hello
std::string s = "cpp";
std::string_view sv2 = s; // 指向 s 的数据,不复制看内存模型就清楚了:
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "std::string_view" as sv #E8F1FF {
node "const char* 指针" as ptr #DCE9FF
node "size_t 长度" as len #DCE9FF
}
rectangle "字符数组(别人拥有)" as buf #D9FFE2 {
node "'h' 'e' 'l' 'l' 'o' ' ' 'w' ..." as chars #D9FFE2
}
ptr --> chars : "只读引用"
note bottom of sv
没有拷贝、没有所有权,
只有"指哪看哪"的信息
end note
@enduml创建 string_view 是 O(1):就存一个指针 + 一个长度。对比 std::string 的拷贝构造——要分配内存、逐个字符复制,是 O(n)。这就是 string_view 的卖点:看字符串,不搬字符串。
二、为什么函数参数推荐 string_view
看一个典型函数:
#include <string>
#include <string_view>
// 老写法:const std::string&
size_t count_a(const std::string& s) {
size_t n = 0;
for (char c : s) if (c == 'a') ++n;
return n;
}
// 问题:传入 "hello"(字面量)时,会先构造一个临时 std::string(拷贝!)
count_a("hello"); // 隐式构造临时 string,有开销
// 新写法:std::string_view
size_t count_a(std::string_view s) { // 零拷贝
size_t n = 0;
for (char c : s) if (c == 'a') ++n;
return n;
}
count_a("hello"); // 直接指向字面量,不拷贝
std::string str = "world";
count_a(str); // 也直接指向 str 的数据传 const std::string& 时,如果调用方给的是字面量或 char*,编译器要隐式构造一个临时 std::string——分配内存 + 拷贝字符。传 string_view 则完全不拷贝,字面量也好、std::string 也好、char* 也好,一视同仁地"只看不搬"。
参数类型的选择惯例:
| 参数场景 | 推荐类型 |
|---|---|
| 只读处理字符串(查找、解析、比较) | std::string_view |
| 需要复制/存储这个字符串 | std::string(拷贝)或移动 |
| 需要修改原字符串 | std::string& |
C 接口边界(要 const char*) | 传 sv.data() 或直接用 const char* |
核心原则:只读借用用 string_view,要持有才用 string。不过要注意,string_view 只能用于"处理"的接口;如果函数内部要存下来(成员变量、放进容器),必须转成 std::string。
三、string_view 的操作
它支持的"只读操作"和 string 差不多:
std::string_view sv = "hello world";
sv.size(); // 11
sv.length(); // 同上
sv.empty(); // 是否为空
sv[0]; // 'h'(可读不可写)
sv.at(1); // 'e'(越界抛 out_of_range)
sv.substr(6, 5); // "world"(注意:返回的也是 string_view,零拷贝)
sv.find('o'); // 4
sv.compare("hello"); // 比较
sv.data(); // const char*(不一定以 \0 结尾!)重点提醒:
substr返回的也是string_view——零拷贝,但也意味着它仍然指向原数据。把substr结果存到函数外面用,前提是原字符串还活着data()不保证以\0结尾——string_view 用"指针+长度"表示,可能指向字符串中间。把它当const char*传给 C 接口(期待\0结尾)会踩坑。真需要,先std::string(sv).c_str()- 不能修改:
sv[0] = 'x'编译错误(只有 const 访问)
四、最大陷阱:悬垂
string_view 不拥有数据,所以指向的数据死了,它就成了悬垂视图——和裸指针的 use-after-free 同性质:
#include <string>
#include <string_view>
std::string_view dangling() {
std::string local = "temporary";
return local; // 危险!local 销毁后,返回的 view 悬垂
}
// 更隐蔽的:
std::string make_string() { return "abc"; }
std::string_view sv = make_string(); // 临时 string 销毁 → sv 悬垂!
// 用 sv → 未定义行为后一个例子特别阴:make_string() 返回的临时 std::string 在语句结束后就销毁了,sv 指向的字符序列随之消失。编译器在部分场景能警告(-Wdangling-reference),但最好自己记住规则:
string_view 的生命周期不能超过它指向的数据。
判断方法很简单:问自己"它指着谁?那个人还活着吗?"——指着函数局部变量/临时对象,就是高危;指着字面量或生命周期明确的成员/参数,安全。
@startuml
left to right direction
skinparam nodeFontSize 13
skinparam backgroundColor #FFFFFF
rectangle "安全" as safe #D9FFE2 {
node "字面量(静态存储期)" as s1 #D9FFE2
node "参数 std::string&(调用方持有)" as s2 #D9FFE2
}
rectangle "危险" as danger #FFD9D9 {
node "函数局部 std::string" as d1 #FFD9D9
node "临时对象(语句结束即销毁)" as d2 #FFD9D9
}
safe -[hidden]down- danger
@enduml五、C 对照
C 里最接近 string_view 的形态是"const char* + 长度"两个参数:
size_t count_a(const char* s, size_t n) {
size_t cnt = 0;
for (size_t i = 0; i < n; ++i) if (s[i] == 'a') cnt++;
return cnt;
}| 需求 | C(const char* + 长度) | C++(std::string_view) |
|---|---|---|
| 零拷贝只读 | 手动传两个参数 | 一个类型搞定 |
| 长度管理 | 手动跟踪(容易错) | 自动 |
| 与 std::string 互操作 | 手动 .c_str() + .size() | 隐式转换 |
| 空串 | 手写约定 | .empty() |
| 子串视图 | 指针+长度偏移 | substr() 返回新 view |
C 里"指针+长度"本来就是常见范式,string_view 把它封装成了类型,顺手解决了"长度忘了传""长度传错"这类低级错误。
六、与本站主线衔接
- 33 篇(string 深入):string 的拷贝成本、SSO、
c_str()悬垂——string_view 是"避免这些成本"的方案 - 39 篇(算法库):
std::string_view作为算法输入,配合 find/count 等 - 49 篇(lambda):函数参数用 string_view + lambda 组合是解析类代码的标配
- 性能视角:零拷贝字符串在日志、解析、网络协议等热点路径上的意义,见
concepts/性能相关章节
七、一句话总结
std::string_view 是"指针 + 长度"的零拷贝只读字符串视图:创建 O(1)、不拥有数据、能直接接收字面量和 string;函数参数"只读借用"优先用它,但记住两条红线——data() 不保证 \0 结尾、视图生命周期不能超过它指向的数据。