std::string 与 std::string_view 的常见坑
标准与兼容性
- 引入版本:C++17(头文件
<string_view>,类型std::string_view)。 - 实验性前身:C++14 Library Fundamentals TS(2015)的
std::experimental::string_view。 - 编译器支持:主流编译器自 2017 年起均已支持;旧代码中若见
std::experimental::string_view,升级到 C++17+ 可直接替换为标准版。 - 字面量后缀:
"abc"sv在命名空间std::literals::string_view_literals中,示例:
cpp
using namespace std::literals::string_view_literals;
std::string_view sv = "hello"sv;
结论(安全用法)
- 只读入参:优先用
std::string_view(不持久化,不越过调用栈)。 - 需要持久化/跨线程/跨异步:存为
std::string(拥有所有权)。 - 与 C API 交互(需要以
\0结尾):要么复制到std::string,要么确保末尾有\0且没有内嵌\0。
高频坑点
1) 悬垂引用(最致命)
std::string_view bad() {
std::string s = "hello"; // 局部对象
return std::string_view{s}; // 返回后 s 析构 → 悬垂
}
void store(std::string_view sv); // 若内部持久化保存 sv → 危险
store(std::string("tmp")); // 临时 string 销毁后,sv 悬垂
不要把 string_view 存起来(成员变量/容器/异步回调),除非它引用的内存是静态存储期或你能严格保证其存续期更长(如引用“更大的 owning 字符串”生命周期相同)。
2) 非 \0 结尾与内嵌 \0
void c_api(const char*); // 期望以 \0 结尾
std::string_view sv = "abc\0def"; // size() == 7,包含内嵌 \0
c_api(sv.data()); // 仅看到 "abc",后半丢失
string_view 只是“指针+长度”,data() 不保证末尾 \0。传给 C API 前,需拷贝到 std::string 或手动补 \0(且确认无内嵌 \0)。
3) 底层存储变化导致视图失效
std::string s = "hello";
std::string_view v = s; // 指向 s 的缓冲区
s += " world"; // 可能触发 reallocate
// v 现在可能悬垂(指向旧缓冲)
修改 std::string 可能使所有指向它的 string_view 失效(特别是增长导致重分配时)。
4) 以为 string_view“更省内存所以 everywhere 用”
string_view 不拥有数据,不能延长生命周期;用错场景反而更危险。规则:
- 形参读-only → 用值传 std::string_view;
- 需要保存/跨边界 → 立即拷贝成 std::string。
5) 与字面量/缓冲区的生命周期
constexpr std::string_view ok = "literal"; // OK,静态存储期
std::string_view v = std::to_string(42); // 危险:临时 string 立刻销毁
对临时字符串、局部 std::string、std::vector<char> 的数据视图在对象销毁或重分配后都将失效。
6) 与 UTF-8/字符边界
std::string/std::string_view 都是“字节序列”,按字节切片可能破坏多字节字符边界;不要用 substr/remove_prefix 去“按字符”切多字节编码。
正确范式
- API 设计:读-only 文本参数用
std::string_view;需要持久化则在函数体内std::string owned(sv);拷贝一次。 - 与 C API 的桥接:
void use_c_api(std::string_view sv) {
std::string tmp(sv); // 保证 \0 结尾且无内嵌 \0 影响
c_api(tmp.c_str());
}
- 视图内切片不改原数据:
remove_prefix/remove_suffix/substr只移动“视窗”,不复制不修改原文。
std::string_view sv = " header: value ";
sv.remove_prefix(2); // "header: value "
sv = sv.substr(0, sv.size()-2);// "header: value"
与容器/哈希联动(异构查找)
std::map<std::string, T, std::less<>> m;可用string_view直接查找:m.find(std::string_view{"key"});std::unordered_map<std::string, T, /*透明 hasher*/, /*透明 equal*/>(C++20)既保留std::string作为拥有键,又支持用string_view零拷贝查找,避免构造临时std::string。
示例透明 hasher/equal:
struct SvHash {
using is_transparent = void;
size_t operator()(std::string_view s) const noexcept {
return std::hash<std::string_view>{}(s);
}
size_t operator()(const std::string& s) const noexcept { return (*this)(std::string_view{s}); }
size_t operator()(const char* s) const noexcept { return (*this)(std::string_view{s}); }
};
struct SvEq {
using is_transparent = void;
bool operator()(std::string_view a, std::string_view b) const noexcept { return a == b; }
};
// std::unordered_map<std::string, T, SvHash, SvEq> um;
// um.find(std::string_view{"key"}); // 零拷贝查找
注意:键类型依然是 std::string(拥有内存),string_view 仅用于查找时的临时视图,避免了悬垂问题。
小贴士
string_view::data()不保证\0;string::c_str()才保证。string_view::substr()若pos > size()会抛std::out_of_range;string::substr()也会,留意边界。- 想要“廉价前缀/后缀去除”优先用
remove_prefix/remove_suffix,零拷贝。 - 需要线程安全的长期保存,务必拷贝成
std::string。