C++ STL 之 string_view 详解
C++ STL 之 string_view 详解
一、问题:字符串参数传递的隐形成本
在 C++ 中传递字符串最常见的方式是const std::string&,但每次传入const char*或字符串字面量时,都会隐式构造一个临时std::string对象——这意味着堆分配和字符拷贝。对于日志、配置读取、参数解析等高频路径,这个开销不可忽视。
voidparse(conststd::string&s);// 每次传 "hello" 都会构造临时 stringparse("hello world");// 隐式构造,O(n) 分配二、方案:string_view 的设计与原理
std::string_view(C++17)是一个非 owning 的字符串视图,内部仅保存两个成员:指向外部字符数组的指针和长度。构造和拷贝都是 O(1),不涉及任何堆分配或字符拷贝。
#include<string_view>voidparse(std::string_view sv);// 零拷贝parse("hello world");// O(1),直接指向字面量常用接口
std::string_view sv="hello world";sv.data();// 原始指针,不一定以 '\0' 结尾sv.size();// 长度,O(1)sv.empty();// 判空sv.front();sv.back();// 首尾字符sv[0];// 下标访问,无边界检查sv.at(0);// 下标访问,有边界检查(抛异常)sv.substr(0,5);// 返回另一个 string_view,O(1)sv.remove_prefix(6);// 去掉前 N 个字符sv.remove_suffix(5);// 去掉后 N 个字符sv.compare("hello");// 比较C++20 新增:starts_with / ends_with
std::string_view sv="hello.cpp";if(sv.starts_with("hello")){/* 匹配 */}if(sv.ends_with(".cpp")){/* 匹配 */}这两个函数在 C++20 之前需要手写sv.substr(0, 5) == "hello",既不直观也有性能浪费。对string_view和std::string均可用。
sv 字面量后缀
usingnamespacestd::string_view_literals;autosv="hello world"sv;// std::string_view,而非 const char*"..."sv字面量在编译期直接构造string_view,零运行时开销。
三、string_view vs const string& 参数选型
函数形参一律用string_view,仅当:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 只读访问字符串内容 | string_view | 零拷贝,兼容所有字符串类型 |
| 需要存储字符串副本 | std::string | string_view 不 owning,拷贝后悬垂 |
需要\\0结尾的 C 风格接口 | const std::string& | data() 不保证以\\0结尾 |
| 接口被广泛 ABI 暴露 | 视情况 | string_view 按值传 16 字节,可能比 const ref 大 |
// 推荐:函数只读访问字符串voidlog(std::string_view msg);voidparse(std::string_view config);voidfind(std::string_view haystack,std::string_view needle);// 不推荐:函数需要持有副本voidset_name(std::string_view name){name_=std::string(name);// 必须显式转换}经验法则:面向新代码时,函数形参中所有const std::string&都应当改为std::string_view,除非函数需要把字符串存入成员变量或需要\\0结尾。
四、悬垂指针风险
string_view不拥有字符数据,若底层字符数组被销毁,访问string_view就是悬垂指针。
std::string_view sv;{std::string s="temporary";sv=s;// sv 指向 s 的内部缓冲区}// s 销毁,sv 悬垂std::cout<<sv;// 未定义行为,可能崩溃或乱码安全使用原则
// 正确:函数内临时使用,不跨语句存储voidsafe(std::string_view sv){autopos=sv.find('.');// 安全std::cout<<sv.substr(0,pos);// 安全}// 正确:传入静态数据staticconstexprautokConfig="version=1.0"sv;process(kConfig);// 安全// 错误:函数返回 string_view 指向局部 stringstd::string_viewbad(){std::string s="local";returns;// 悬垂!}// 正确:函数返回 string_view 指向静态数据std::string_viewgood(){return"static literal"sv;// 安全}五、面试题(6 道)
1. string_view 的拷贝是深拷贝还是浅拷贝?
浅拷贝。仅复制指针和长度两个成员,O(1)。不复制字符数据。
2. string_view 的 substr 返回什么?是否分配内存?
返回一个新的string_view,指向原视图的子区间。O(1),无分配。
3. 以下代码的输出是什么?
std::string_view sv="hello";sv.remove_prefix(2);sv.remove_suffix(2);std::cout<<sv;输出l。remove_prefix(2)去掉 “he”(span 变为 “llo”),remove_suffix(2)去掉 “lo”(span 变为 “l”)。
4. 为什么 string_view 不能作为 unordered_map 的 key?
string_view不 owning,无法保证 key 的生命周期。若底层字符串被销毁,map 中的 key 悬垂。需要用std::string作为 key。
5. const char* 转 string_view 是否安全?
安全,只要const char*指向的字符串在string_view使用期间有效。但要注意string_view不要求以\0结尾,sv.data()可能不是合法的 C 字符串。
6. string_view 能否替代 const std::string& 作为返回值?
不能。函数返回string_view时必须保证返回的字符串数据在函数返回后仍然存活——这极大限制使用场景。作为函数参数接收外部数据才是指定用法。
六、总结
| 特性 | string_view | const string& |
|---|---|---|
| C++ 版本 | C++17 | C++98 |
| 构造复杂度 | O(1) | O(n)(从const char*) |
| 堆分配 | 从不 | 可能 |
| 生命周期跟踪 | 不 owning | owning |
data() 以\0结尾 | 不保证 | 保证 |
| substr 开销 | O(1) | O(n) |
string_view是现代 C++ 中字符串参数传递的首选类型。函数形参一律用string_view,返回值不用string_view返回局部字符串,记住这两条就能用好它。
