⚡C++ STL 与迭代器

std::string_view 与字符串处理

难度:⭐ | 高频指数:🔥🔥🔥

面试回答

常见问法

  • std::string_view 是什么?解决什么问题?
  • string_viewconst 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_viewsubstrstringsubstr 有什么区别? 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_viewstd::span<char> 的区别? string_view 是只读的,专门用于字符串。span<char> 可以是可写的,更通用。string_view 提供了字符串特有的接口(findsubstr 等)。


原理展开

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 的替代品来存储数据,忘记它不拥有内存。
  • 返回指向局部 stringstring_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 引入的只读字符串视图,本质是一个指针加长度,不拥有内存。它解决的核心问题是避免不必要的字符串拷贝——无论传入 stringconst char* 还是字面量,都是零拷贝。和 const string& 的主要区别是:传入字面量或 const char* 时,const string& 会构造临时 string(可能堆分配),而 string_view 不会。最大的陷阱是生命周期问题:string_view 不拥有数据,如果原始字符串被销毁,string_view 就悬垂了。所以它最适合做函数参数,不适合做类成员变量或容器 key。另外它不保证 null-terminated,不能直接传给 C API。

Related · STL 与迭代器