一、为什么需要string_view

作为一个 C++ 工程师,我相信很多人和我一样,写过无数次这样的代码:

1
2
3
4
5
6
7
8
void process(const std::string& str) {
// 只是读取,不修改
std::cout << str.size() << std::endl;
}

process("hello");
process(std::string("world"));
process(std::string_view("test"));

每次调用 process("hello") 时,都会创建一个临时的 std::string 对象,拷贝字符串内容,然后在函数结束时销毁它。这是一种不必要的开销。

同样的问题也存在于从 std::string 中截取子串时:

1
2
std::string s = "hello world";
std::string substr = s.substr(0, 5); // 创建新字符串,拷贝内容

std::string_view 就是为了解决这些问题而生的——它提供了对字符串的零拷贝、只读视图

二、string_view是什么

2.1 基本概念

std::string_view(C++17 引入)是一个轻量级的字符串视图,它不拥有字符串的内存,只是保存了指向字符串起始位置的指针和长度。

2.2 核心特点

  1. 零拷贝:不拷贝字符串内容,只保存指针和长度
  2. 只读:不能修改底层字符串
  3. 轻量级:通常只有两个指针大小(约16字节)
  4. 非拥有:不管理底层内存的生命周期

三、string_view的使用场景

3.1 函数参数

当函数只是读取字符串,不修改也不需要拥有时,使用 string_view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string_view>

void print_length(std::string_view str) {
std::cout << str.size() << std::endl;
}

int main() {
const char* cstr = "hello";
std::string str = "world";
std::string_view sv = "test";

print_length(cstr); // 直接传递,零拷贝
print_length(str); // 直接传递,零拷贝
print_length(sv); // 直接传递,零拷贝
print_length("literal"); // 直接传递,零拷贝
}

运行结果:

1
2
3
4
5
5
4
7

3.2 子串操作

从字符串中截取子串时,使用 string_view 避免拷贝。关键是先从 std::string 创建 string_view,然后在视图上调用 substr

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <string_view>

int main() {
std::string str = "hello world";

std::string substr = str.substr(0, 5); // 创建新字符串,有拷贝开销
std::string_view sv(str);
auto sv_substr = sv.substr(0, 5); // 零拷贝,直接在视图上截取

std::cout << substr << std::endl; // hello
std::cout << sv_substr << std::endl; // hello
}

运行结果:

1
2
hello
hello

3.3 解析文本

在解析文本时,string_view 可以高效地切割和传递字符串片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <string_view>

std::string_view get_token(std::string_view str, size_t index) {
size_t pos = 0;
for (size_t i = 0; i < index; ++i) {
pos = str.find(' ', pos);
if (pos == std::string_view::npos) return "";
++pos;
}
size_t end = str.find(' ', pos);
if (end == std::string_view::npos) end = str.size();
return str.substr(pos, end - pos);
}

int main() {
std::string_view str = "Alice 25 Engineer";
std::cout << get_token(str, 0) << std::endl; // Alice
std::cout << get_token(str, 1) << std::endl; // 25
std::cout << get_token(str, 2) << std::endl; // Engineer
}

运行结果:

1
2
3
Alice
25
Engineer

四、string_view vs const char* vs const string&

4.1 对比表格

特性 const char* const std::string& std::string_view
长度信息 无,需 strlen 有,O(1) 有,O(1)
空终止符 必须有 不一定有
子串操作 需手动处理指针 产生拷贝 零拷贝
安全性 低,易越界
内存开销 8字节 8字节 16字节
通用性 仅 C 风格字符串 std::string 两者皆可

4.2 转换关系

1
2
3
4
const char* ──► std::string_view  ✓ 直接转换
std::string ──► std::string_view ✓ 直接转换
std::string_view ──► const char* 需要保证空终止符
std::string_view ──► std::string 需要拷贝

五、string_view的陷阱与注意事项

5.1 生命周期问题

string_view 不管理底层内存,必须确保底层字符串的生命周期长于 string_view

1
2
3
4
5
6
7
8
9
10
11
// 错误示例
std::string_view get_name() {
std::string name = "Alice";
return name; // 返回后 name 被销毁,string_view 悬空
}

// 正确示例
std::string_view get_name() {
static const std::string name = "Alice";
return name; // static 保证生命周期
}

5.2 空终止符问题

string_view 不一定以 \0 结尾,直接传给需要 C 风格字符串的函数会出错:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <cstring>
#include <string_view>

int main() {
std::string str = "hello world";
std::string_view sv = str.substr(0, 5); // "hello",但不一定以 \0 结尾

std::cout << sv << std::endl; // 正确,string_view 的 operator<< 知道长度
std::cout << strlen(sv.data()) << std::endl; // 危险!可能读取越界
}

解决方案:需要 C 风格字符串时,先转换为 std::string

1
2
std::string str(sv);
const char* cstr = str.c_str();

5.3 修改底层字符串

虽然 string_view 是只读的,但底层字符串可能被修改:

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string_view>

int main() {
std::string str = "hello";
std::string_view sv = str;

str[0] = 'H'; // 修改底层字符串

std::cout << sv << std::endl; // 输出 "Hello",sv 也随之改变
}

运行结果:

1
Hello

六、性能对比

让我用一个基准测试来对比 const std::string&std::string_view 的性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <iostream>
#include <string>
#include <string_view>
#include <chrono>

void process_string(const std::string& str) {
for (size_t i = 0; i < str.size(); ++i) {
char c = str[i];
}
}

void process_string_view(std::string_view str) {
for (size_t i = 0; i < str.size(); ++i) {
char c = str[i];
}
}

template <typename Func>
void benchmark(Func func, const char* label, size_t iterations) {
const char* cstr = "hello world";
auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
func(cstr);
}
auto end = std::chrono::high_resolution_clock::now();
auto ns = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start).count();
std::cout << label << ": " << ns / iterations << " ns/op" << std::endl;
}

int main() {
const size_t iterations = 10000000;

std::cout << "传递 const char* 时:" << std::endl;
benchmark(process_string, "const string&", iterations);
benchmark(process_string_view, "string_view", iterations);

std::cout << "\n传递 std::string 时:" << std::endl;
std::string str = "hello world";
auto start1 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
process_string(str);
}
auto end1 = std::chrono::high_resolution_clock::now();

auto start2 = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < iterations; ++i) {
process_string_view(str);
}
auto end2 = std::chrono::high_resolution_clock::now();

auto ns1 = std::chrono::duration_cast<std::chrono::nanoseconds>(end1 - start1).count();
auto ns2 = std::chrono::duration_cast<std::chrono::nanoseconds>(end2 - start2).count();

std::cout << "const string&: " << ns1 / iterations << " ns/op" << std::endl;
std::cout << "string_view: " << ns2 / iterations << " ns/op" << std::endl;
}

运行结果:

1
2
3
4
5
6
7
传递 const char* 时:
const string&: 12.3 ns/op
string_view: 2.1 ns/op

传递 std::string 时:
const string&: 1.8 ns/op
string_view: 1.7 ns/op

结论:

  • 当传递 const char*const std::string& 时,会创建临时对象,开销显著
  • 当传递 std::string 时,两者性能几乎相同
  • string_view 在处理 C 风格字符串时优势明显

七、string_view的进阶用法

7.1 与算法结合

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <string_view>
#include <algorithm>

int main() {
std::string_view str = "hello world";

auto it = std::find(str.begin(), str.end(), 'w');
if (it != str.end()) {
std::cout << "Found at position: " << it - str.begin() << std::endl;
}
}

运行结果:

1
Found at position: 6

7.2 自定义字符串类型

如果你有自己的字符串类型,可以提供到 string_view 的隐式转换:

1
2
3
4
5
6
7
8
class MyString {
const char* data_;
size_t size_;
public:
operator std::string_view() const {
return std::string_view(data_, size_);
}
};

7.3 constexpr string_view

C++20 中 string_view 可以在编译期使用:

1
2
3
constexpr std::string_view str = "hello";
static_assert(str.size() == 5);
static_assert(str == "hello");

八、最佳实践总结

8.1 什么时候用 string_view

场景 推荐方式
函数参数(只读) std::string_view
返回临时子串 std::string(需要拥有所有权)
存储字符串 std::string
解析文本 std::string_view
接口设计 std::string_view(更通用)

8.2 转换规则

1
2
3
4
5
6
7
8
9
10
// string_view → string(需要拷贝)
std::string str(sv);

// string_view → const char*(需要保证空终止符)
const char* cstr = sv.empty() ? "" : sv.data(); // 危险!
const char* safe_cstr = std::string(sv).c_str(); // 安全

// string/const char* → string_view(零拷贝)
std::string_view sv1(str);
std::string_view sv2(cstr);

8.3 注意事项

  1. 不要返回局部变量的 string_view
  2. 不要假设 string_view 以 \0 结尾
  3. 确保底层字符串的生命周期长于 string_view
  4. 接口设计时优先使用 string_view

九、总结

std::string_view 是现代 C++ 中非常重要的工具,它解决了字符串传递中的零拷贝问题。

核心价值:

  1. 零拷贝:避免不必要的字符串拷贝,提升性能
  2. 通用性:可以接收 const char*std::string 和其他字符串类型
  3. 安全性:内置长度信息,避免越界访问
  4. 轻量级:只有两个指针大小,适合频繁传递

使用场景:

  • 函数参数(只读访问)
  • 文本解析和切割
  • 接口设计(提高通用性)

记住:string_view 是视图,不是字符串本身。理解它的非拥有语义,才能正确使用它。