C++17 之 std::string_view
C++17 之 std::string_view
系列文章: C++ 新特性系列 · 第4篇
阅读时间: 约 15 分钟
适用标准: C++17 及以上
引言:字符串处理的隐形开销
在 C++ 的日常开发中,字符串无处不在——日志拼接、配置解析、文件路径处理……但你有没有注意过,那些看似无害的 std::string 参数传递,背后可能藏着巨大的性能开销?
看一个很常见的场景:
// 一个简单的日志函数
void logMessage(const std::string& msg) {
std::cout << "[LOG] " << msg << std::endl;
}
// 调用时传入一个字符串字面量
logMessage("Hello, world!");
这里有一个容易被忽略的问题:"Hello, world!" 本身只是一个 const char*(一个指针大小的值,64位系统上为8字节),但 const std::string& 参数会触发隐式构造——编译器需要创建一个临时的 std::string 对象,分配堆内存,把内容拷贝过去。函数调用结束后,临时对象被销毁,内存被释放。
如果这个函数被高频调用(比如在热循环中处理日志),这堆"隐性开销"就会累积成性能瓶颈。
更糟糕的是这些场景:
std::string substring = someString.substr(0, 10); // 又是一次堆分配
process(someString.substr(0, 5)); // 临时 string,用完即弃
每次 substr() 都会分配新的堆内存、拷贝内容。如果只是读一下某个子串,这些开销完全没必要。
C++17 给出了一个优雅的答案:std::string_view —— 一个不拥有数据、不做分配、不拷贝内容的字符串"视图"。
一、为什么需要 string_view?
核心思想:只读视图,零拷贝
std::string_view 本质上是一个**(指针 + 长度)对**,它只是"看向"已有的字符串数据,自身不拥有、不管理任何内存:
┌─────────────────────┐
│ std::string_view │
├────────────┬────────┤
│ const char* │ size_t │
│ (指针) │ (长度) │
└────────────┴────────┘
│
▼ 指向
┌─────────────────────┐
│ 实际的字符串数据 │ ← 可能在栈上、堆上、甚至静态区
└─────────────────────┘
这意味着:
- 零堆分配:不需要
new,不需要malloc - 零拷贝:只保存指针和长度,不复制字符内容
- 构造极快:一次指针赋值 + 一次整数赋值,O(1) 操作
- 统一接口:可以接受
std::string、const char*、字符数组等任意字符串来源
对比传统方案
| 方案 | 堆分配 | 字符拷贝 | 修改能力 | Null 终止保证 |
|---|---|---|---|---|
const std::string& |
构造时可能分配 | 可能拷贝 | 只读(语义上) | ✅ 是 |
const char* |
❌ 无 | ❌ 无 | 只读 | ⚠️ 取决于来源 |
std::string |
✅ 总是 | ✅ 总是 | 可读写 | ✅ 是 |
std::string_view |
❌ 无 | ❌ 无 | 只读 | ❌ 不保证 |
二、核心 API 详解
2.1 构造
std::string_view 的构造非常灵活,可以"指向"几乎任何形式的字符串数据:
#include <string_view>
#include <string>
#include <iostream>
int main() {
// 1. 从字符串字面量构造
std::string_view sv1 = "Hello, string_view!";
std::cout << "sv1: " << sv1 << std::endl;
// 2. 从 std::string 构造
std::string str = "Hello, std::string!";
std::string_view sv2 = str;
std::cout << "sv2: " << sv2 << std::endl;
// 3. 从字符数组构造
char arr[] = "Hello, char array!";
std::string_view sv3(arr);
std::cout << "sv3: " << sv3 << std::endl;
// 4. 从指针 + 长度构造(指定子串范围)
std::string_view sv4(str.data() + 7, 9); // "std::string"
std::cout << "sv4: " << sv4 << std::endl;
return 0;
}
输出:
sv1: Hello, string_view!
sv2: Hello, std::string!
sv3: Hello, char array!
sv4: std::string
2.2 基础查询
std::string_view sv = "Hello, world!";
sv.size(); // 13,字符数量
sv.length(); // 13,同 size()
sv.empty(); // false
sv.data(); // const char*,指向底层数据
sv[0]; // 'H'
sv.front(); // 'H'
sv.back(); // '!'
2.3 子串操作:substr
substr() 返回一个新的 string_view,指向原数据的子范围——同样是零拷贝:
std::string_view sv = "Hello, world!";
auto sub1 = sv.substr(0, 5); // "Hello"
auto sub2 = sv.substr(7); // "world!"
auto sub3 = sv.substr(7, 3); // "wor"
std::cout << sub1 << std::endl; // Hello
std::cout << sub2 << std::endl; // world!
std::cout << sub3 << std::endl; // wor
与 std::string::substr() 的对比:
std::string str = "Hello, world!";
// std::string::substr() —— 分配新内存 + 拷贝内容
std::string subStr = str.substr(0, 5); // 堆分配发生在这里
// std::string_view::substr() —— 只调整指针和长度
std::string_view subSv = sv.substr(0, 5); // 零开销
2.4 原地裁剪:remove_prefix / remove_suffix
这两个方法直接修改 string_view 本身,去掉前缀或后缀字符:
std::string_view sv = " Hello, world! ";
sv.remove_prefix(2); // 去掉前2个空格 → "Hello, world! "
sv.remove_suffix(2); // 去掉后2个空格 → "Hello, world!"
std::cout << "[" << sv << "]" << std::endl; // [Hello, world!]
这在解析字符串时非常有用,比如逐段读取用分隔符隔开的数据:
std::string_view csv = "apple,banana,cherry";
// 逐个提取字段
while (!csv.empty()) {
auto pos = csv.find(',');
if (pos == std::string_view::npos) {
std::cout << "Field: " << csv << std::endl; // 最后一个
break;
}
std::cout << "Field: " << csv.substr(0, pos) << std::endl;
csv.remove_prefix(pos + 1); // 跳过字段和逗号
}
输出:
Field: apple
Field: banana
Field: cherry
2.5 查找操作
string_view 提供了与 std::string 一致的查找接口:
std::string_view sv = "Hello, world! Hello, C++17!";
sv.find("Hello"); // 0,首次出现的位置
sv.find("Hello", 1); // 14,从位置1开始找
sv.rfind("Hello"); // 14,最后一次出现
sv.find_first_of("aeiou"); // 1,第一个元音字母的位置
sv.find_last_of("!"); // 26,最后一个 '!' 的位置
三、实战使用场景
场景一:函数参数——告别不必要的拷贝
这是 string_view 最常见也最实用的场景:
#include <string_view>
#include <string>
#include <iostream>
#include <chrono>
#include <vector>
// 旧写法:const std::string& —— 传入 const char* 时会隐式构造临时 string
void processOld(const std::string& str) {
volatile size_t len = str.size(); // 防止编译器优化掉
}
// 新写法:std::string_view —— 零拷贝,零分配
void processNew(std::string_view str) {
volatile size_t len = str.size(); // 同样只读
}
int main() {
const int ITERATIONS = 1000000;
// 测试 const std::string&
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
processOld("This is a test string for benchmarking");
}
auto end1 = std::chrono::high_resolution_clock::now();
// 测试 std::string_view
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
processNew("This is a test string for benchmarking");
}
auto end2 = std::chrono::high_resolution_clock::now();
auto ms1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1).count();
auto ms2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2).count();
std::cout << "const std::string&: " << ms1 << " ms" << std::endl;
std::cout << "std::string_view: " << ms2 << " ms" << std::endl;
return 0;
}
典型输出(100万次调用):
const std::string&: 45 ms
std::string_view: 2 ms
性能差距可以达到 10-20 倍,尤其当传入的字符串较长时更加明显。
场景二:字符串解析与分割
string_view 配合 find + substr + remove_prefix 可以实现高效的字符串分割:
#include <string_view>
#include <vector>
#include <iostream>
std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
while (!str.empty()) {
auto pos = str.find(delimiter);
if (pos == std::string_view::npos) {
result.push_back(str);
break;
}
result.push_back(str.substr(0, pos));
str.remove_prefix(pos + 1);
}
return result;
}
int main() {
std::string_view csv = "Alice,25,Beijing,Engineer";
auto fields = split(csv, ',');
for (const auto& field : fields) {
std::cout << " [" << field << "]" << std::endl;
}
// 输出:
// [Alice]
// [25]
// [Beijing]
// [Engineer]
// 甚至可以从另一个字符串中解析
std::string record = "Bob,30,Shanghai,Designer";
auto fields2 = split(record, ','); // std::string 隐式转换为 string_view
std::cout << "\nName: " << fields2[0] << std::endl;
return 0;
}
整个分割过程中没有分配任何新的字符串内存,所有 string_view 都指向原始数据。
场景三:统一的只读接口
当你的库需要同时接受 std::string 和 const char* 时,string_view 是最佳的通用参数类型:
#include <string_view>
#include <string>
#include <iostream>
// 统一接口:一个函数搞定所有字符串来源
std::string_view toUpperView(std::string_view input) {
// 注意:string_view 本身不可修改底层数据
// 这里用返回值的方式示意,实际会返回新内容
// 这里仅作 API 演示
return input;
}
class Logger {
public:
// 一个参数类型,兼容所有字符串来源
void log(std::string_view category, std::string_view message) {
std::cout << "[" << category << "] " << message << std::endl;
}
};
int main() {
Logger logger;
// 以下所有调用都是零拷贝的:
logger.log("INFO", "Application started"); // 字符串字面量
logger.log("WARN", std::string("Memory usage high")); // std::string
std::string errCode = "E001";
logger.log("ERROR", errCode.c_str()); // const char*
return 0;
}
输出:
[INFO] Application started
[WARN] Memory usage high
[ERROR] E001
关键点: 使用 std::string 的 .c_str() 传入 string_view 参数,避免了不必要的 const char* → std::string 隐式构造。
四、注意事项:string_view 的三大陷阱
⚠️ 陷阱一:生命周期问题(悬空引用)
这是 string_view 最危险的坑——它不拥有数据,所以必须确保底层数据的生命周期覆盖 string_view 的使用范围:
#include <string_view>
#include <iostream>
std::string_view getSV() {
std::string temp = "Hello, dangling!";
return temp; // ⚠️ temp 在函数结束时被销毁
} // temp 的内存在此释放
int main() {
std::string_view sv = getSV();
// sv 指向已释放的内存 → 未定义行为!
std::cout << sv << std::endl; // 💥 可能崩溃、乱码、或"碰巧正确"
return 0;
}
对比: 如果返回 std::string,拷贝语义会保证数据的独立性;但 string_view 只保存指针,指针悬空了就是 UB。
安全做法:
// ✅ 正确:确保数据生命周期
std::string data = "Hello, safe!";
std::string_view sv = data; // data 的生命周期由调用者管理
// ✅ 正确:临时字符串的字面量是安全的
std::string_view sv2 = "String literals have static storage duration";
// ✅ 正确:string_view 的典型安全用法——函数参数默认用 string_view
void process(std::string_view sv) {
// 只读操作,安全
std::cout << sv.substr(0, 5) << std::endl;
}
⚠️ 陷阱二:不保证 Null 终止
std::string_view 不保证以 \0 结尾,直接传给 C 风格 API 可能出问题:
std::string_view sv = "Hello"; // 不保证以 \0 结尾
const char* cstr = sv.data(); // 可能指向没有 '\0' 的数据!
// ❌ 危险:C 风格字符串函数依赖 '\0' 定界
printf("%s\n", cstr); // 可能读越界
strlen(cstr); // 可能越界
安全做法:
// ✅ 安全:构造 std::string 再取 c_str()
std::string str(sv);
printf("%s\n", str.c_str());
// ✅ 安全:使用 string_view 自己的长度
std::cout.write(sv.data(), sv.size());
// ✅ 安全:如果需要 Null 终止的副本
std::string safe(sv.data(), sv.size());
printf("%s\n", safe.c_str());
⚠️ 陷阱三:与 const std::string& 的权衡
string_view 并不总是优于 const std::string&。选择依据如下:
| 场景 | 推荐使用 | 原因 |
|---|---|---|
| 只读访问 | string_view |
零拷贝 |
| 需要 Null 终止的 C 接口 | const std::string& |
保证 \0 结尾 |
| 需要存储/延迟使用 | std::string |
拥有数据所有权 |
| 高频调用的热路径 | string_view |
极致性能 |
| 不确定调用频率 | string_view |
默认选它,性能不会更差 |
经验法则: 函数参数如果是只读的,默认用 std::string_view。只有当你确实需要 std::string 的保证(如 null 终止、数据所有权)时,才用 const std::string&。
五、性能对比
我们用一个更全面的 benchmark 来展示 string_view 的优势:
#include <string_view>
#include <string>
#include <iostream>
#include <chrono>
#include <vector>
#include <numeric>
// 模拟一个字符串处理函数:读取每个字符并计算长度
size_t countVowels_string(const std::string& str) {
size_t count = 0;
for (char c : str) {
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
++count;
}
return count;
}
size_t countVowels_view(std::string_view sv) {
size_t count = 0;
for (char c : sv) {
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
++count;
}
return count;
}
int main() {
const int ITERATIONS = 1000000;
std::string text = "The quick brown fox jumps over the lazy dog";
// --- 测试1:从 std::string 传入 ---
auto t1_start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
volatile size_t v = countVowels_string(text);
}
auto t1_end = std::chrono::high_resolution_clock::now();
auto t2_start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
volatile size_t v = countVowels_view(text);
}
auto t2_end = std::chrono::high_resolution_clock::now();
// --- 测试2:从 const char* 传入(string_view 的主战场)---
const char* literal = "The quick brown fox jumps over the lazy dog";
auto t3_start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
volatile size_t v = countVowels_string(literal);
// ⬆️ 这里每次调用都会隐式构造 std::string
}
auto t3_end = std::chrono::high_resolution_clock::now();
auto t4_start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < ITERATIONS; ++i) {
volatile size_t v = countVowels_view(literal);
// ⬆️ 零拷贝,直接使用指针
}
auto t4_end = std::chrono::high_resolution_clock::now();
auto ms = [](auto start, auto end) {
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
};
std::cout << "=== 从 std::string 传入 ===" << std::endl;
std::cout << "const std::string&: " << ms(t1_start, t1_end) << " ms" << std::endl;
std::cout << "std::string_view: " << ms(t2_start, t2_end) << " ms" << std::endl;
std::cout << "\n=== 从 const char* 传入(性能差距最大)===" << std::endl;
std::cout << "const std::string&: " << ms(t3_start, t3_end) << " ms" << std::endl;
std::cout << "std::string_view: " << ms(t4_start, t4_end) << " ms" << std::endl;
return 0;
}
典型输出(100万次调用):
=== 从 std::string 传入 ===
const std::string&: 12 ms
std::string_view: 10 ms
=== 从 const char* 传入(性能差距最大)===
const std::string&: 52 ms
std::string_view: 1 ms
分析:
- 从
std::string传入时,两者差距不大(因为std::string可以直接绑定到const std::string&,无需额外操作) - 从
const char*传入时,差距巨大——const std::string&每次都要构造临时std::string(堆分配 + 拷贝),而string_view只是指针赋值
结论: 如果你的函数可能接收 const char*(如字符串字面量),优先使用 std::string_view 作为参数类型。
六、编译器支持
std::string_view 是 C++17 标准的一部分,主流编译器均已完整支持:
| 编译器 | 最低版本 | 备注 |
|---|---|---|
| GCC | 7.0+ | 完整支持 |
| Clang | 4.0+ | 完整支持 |
| MSVC | 19.10+ (VS 2017 15.3+) | 完整支持 |
编译命令示例:
# GCC / Clang
g++ -std=c++17 -o main main.cpp
clang++ -std=c++17 -o main main.cpp
# MSVC (Developer Command Prompt)
cl /std:c++17 main.cpp
C++17 之前的替代方案:
- nonstd::string_view —— 一个轻量级的 header-only 实现
- boost::string_ref —— Boost 提供的类似类型(已弃用,建议迁移到 C++17)
总结
std::string_view 是 C++17 中最实用的性能优化工具之一:
- ✅ 零拷贝:不分配内存、不拷贝数据,只保存指针和长度
- ✅ 接口统一:一个参数类型兼容
std::string、const char*、字符数组 - ✅ 解析利器:
substr、find、remove_prefix全部零开销 - ✅ 性能碾压:在处理
const char*场景下,比const std::string&快一个数量级
但也要牢记它的限制:
- ⚠️ 不拥有数据:底层数据的生命周期必须由调用者管理
- ⚠️ 不保证 Null 终止:传给 C 风格 API 前要特别小心
- ⚠️ 只读视图:不能修改底层数据
一句话总结: 把 const std::string& 参数改成 std::string_view,是现代 C++ 中投入产出比最高的优化之一。
📌 下一篇预告: C++14 引入的两个小而美的语法糖——二进制字面量(
0b前缀) 和 数字分隔符(_)。它们让你写出更清晰的位运算代码和更易读的大数字字面量。代码可读性,有时候就差一个_的距离。
💬 觉得有帮助? 点赞 + 收藏,下次找得到!有问题欢迎评论区交流~
更多推荐



所有评论(0)