std::string_view 与字符串处理
难度:⭐ | 高频指数:🔥🔥🔥
面试回答
常见问法
std::string_view是什么?解决什么问题?string_view和const std::string&有什么区别?string_view有什么生命周期陷阱?- 什么时候该用
string_view,什么时候不该用? string_view能修改字符串吗?
回答
std::string_view(C++17)是一个只读的、不拥有内存的字符串引用。它本质上就是一个 {const char* ptr, size_t len} 的轻量对象。
解决的核心问题: 避免不必要的字符串拷贝。
以前写一个接受字符串的函数,常见的选择是:
void f(const std::string& s)—— 如果传入的是字面量"hello",会隐式构造一个临时string(堆分配)void f(const char* s)—— 丢失了长度信息,不能直接用string的接口
string_view 统一了这两种情况:
void process(std::string_view sv) {
// 无论传入 string、const char*、字面量,都不会拷贝
std::cout << sv.substr(0, 5) << "\n";
}
std::string s = "hello world";
process(s); // 从 string 隐式转换,零拷贝
process("hello"); // 从字面量隐式转换,零拷贝
和 const std::string& 的关键区别:
| 对比项 | const std::string& | std::string_view |
|---|---|---|
| 传入字面量 | 构造临时 string(可能堆分配) | 零拷贝 |
传入 const char* | 构造临时 string | 零拷贝 |
传入 std::string | 零拷贝(引用) | 零拷贝 |
| 是否拥有内存 | 引用别人的 string 对象 | 不拥有,只是指针+长度 |
| 能否保证生命周期 | string 对象存在就安全 | 不保证,需要调用者确保 |
追问
1. string_view 能修改字符串内容吗?
不能。它是只读视图,没有任何修改接口。如果需要修改,必须拷贝到 std::string。
2. string_view 的 substr 和 string 的 substr 有什么区别?
string::substr 返回一个新的 string(有拷贝和堆分配)。
string_view::substr 返回一个新的 string_view(只是调整指针和长度,O(1) 操作,零拷贝)。
3. string_view 是否以 null 结尾?
不保证。 这是一个常见陷阱。string_view 只记录 {ptr, len},不要求末尾有 \0。所以不能直接把 sv.data() 传给需要 C 字符串的函数(如 fopen)。
4. 什么时候不该用 string_view?
- 需要存储字符串(
string_view不拥有内存,原始数据可能被销毁) - 需要传给 C API(需要 null-terminated)
- 函数内部需要修改字符串
- 作为类的成员变量(生命周期难以保证)
5. string_view 和 std::span<char> 的区别?
string_view 是只读的,专门用于字符串。span<char> 可以是可写的,更通用。string_view 提供了字符串特有的接口(find、substr 等)。
原理展开
1. string_view 的内部结构
// 简化的 string_view 实现
class string_view {
const char* data_;
size_t size_;
public:
constexpr string_view(const char* s, size_t len)
: data_(s), size_(len) {}
constexpr string_view(const char* s)
: data_(s), size_(strlen(s)) {} // 从 C 字符串构造
// 从 std::string 隐式转换
string_view(const std::string& s)
: data_(s.data()), size_(s.size()) {}
constexpr size_t size() const { return size_; }
constexpr const char* data() const { return data_; }
constexpr string_view substr(size_t pos, size_t len) const {
return {data_ + pos, len};
}
};
关键点:
- 大小固定:通常是 2 个指针大小(16 字节 on 64-bit)
- 拷贝代价极低:就是拷贝两个整数
- 所以按值传递就行,不需要
const string_view&
2. 生命周期陷阱(最重要的面试考点)
// ❌ 危险:返回指向局部变量的 string_view
std::string_view bad() {
std::string s = "hello";
return s; // s 被销毁,string_view 悬垂!
}
// ❌ 危险:存储 string_view 但原始数据被销毁
class Parser {
std::string_view token_; // 危险的成员变量
public:
void parse(const std::string& input) {
token_ = input.substr(0, 5); // 这里 substr 返回临时 string!
// token_ 指向已销毁的临时对象
}
};
// ✅ 安全:string_view 的生命周期短于原始数据
void safe(const std::string& input) {
std::string_view sv = input; // input 比 sv 活得久,安全
process(sv);
}
核心规则:string_view 的生命周期必须短于它所引用的数据。
3. 常见正确用法
函数参数(最推荐的用法)
// 推荐:接受任何字符串类型,零拷贝
bool startsWith(std::string_view text, std::string_view prefix) {
return text.size() >= prefix.size() &&
text.substr(0, prefix.size()) == prefix;
}
startsWith("hello world", "hello"); // 零拷贝
startsWith(myString, "he"); // 零拷贝
字符串解析
// 用 string_view 做零拷贝的分割
std::vector<std::string_view> split(std::string_view sv, char delim) {
std::vector<std::string_view> result;
while (!sv.empty()) {
auto pos = sv.find(delim);
if (pos == std::string_view::npos) {
result.push_back(sv);
break;
}
result.push_back(sv.substr(0, pos));
sv.remove_prefix(pos + 1);
}
return result;
}
// 注意:返回的 string_view 都指向原始字符串的内存
// 原始字符串必须在使用期间保持有效
避免重复子串拷贝
// 传统写法:每次 substr 都分配内存
std::string token = input.substr(start, len); // 堆分配
// string_view 写法:零拷贝
std::string_view token{input.data() + start, len}; // 只是指针运算
4. string_view vs 其他字符串类型的选择
需要存储/拥有字符串?
→ 是:用 std::string
→ 否:只是读取/传递?
→ 是:用 std::string_view
→ 需要传给 C API?
→ 是:用 const char*(或 string::c_str())
→ 否:用 string_view
5. 面试常见坑题
坑题 1:以下代码有什么问题?
std::string_view getName() {
return "Alice"; // ✅ 安全!字面量有静态存储期
}
这个其实是安全的,因为字符串字面量的生命周期是整个程序。很多人会误判为悬垂。
坑题 2:以下代码有什么问题?
std::string_view getName() {
std::string name = "Alice";
return name; // ❌ 悬垂!name 是局部变量
}
name 在函数结束时被销毁,返回的 string_view 指向已释放的内存。
坑题 3:以下代码有什么问题?
std::map<std::string_view, int> cache;
void add(const std::string& key, int val) {
cache[key] = val; // ❌ 如果 key 是临时对象或后续被修改/销毁,map 中的 key 就悬垂了
}
用 string_view 做容器的 key 非常危险,除非你能保证所有 key 的底层数据永远有效。
易错点
- 把
string_view当成string的替代品来存储数据,忘记它不拥有内存。 - 返回指向局部
string的string_view,导致悬垂引用。 - 把
string_view::data()直接传给需要 null-terminated 的 C 函数。 - 用
string_view做类成员变量或容器 key,生命周期难以管理。 - 误以为
string_view按引用传递更好——实际上它只有 16 字节,按值传递即可。 - 混淆
string::substr(返回 string,有拷贝)和string_view::substr(返回 view,零拷贝)。
记忆技巧
- 一句话定义:
string_view=const char*+size_t,带了长度的只读指针 - 核心价值:零拷贝传递字符串
- 核心风险:不拥有内存,可能悬垂
- 使用原则:
- 函数参数 ✅(最佳场景)
- 局部变量 ✅(生命周期可控)
- 返回值 ⚠️(必须确保数据存活)
- 成员变量 ❌(除非你非常清楚生命周期)
- 口诀:借来看看,不能带走;数据没了,我也完了
面试速答版
std::string_view 是 C++17 引入的只读字符串视图,本质是一个指针加长度,不拥有内存。它解决的核心问题是避免不必要的字符串拷贝——无论传入 string、const char* 还是字面量,都是零拷贝。和 const string& 的主要区别是:传入字面量或 const char* 时,const string& 会构造临时 string(可能堆分配),而 string_view 不会。最大的陷阱是生命周期问题:string_view 不拥有数据,如果原始字符串被销毁,string_view 就悬垂了。所以它最适合做函数参数,不适合做类成员变量或容器 key。另外它不保证 null-terminated,不能直接传给 C API。