
🎁std::string_view
std::string_view 是 C++17 标准库中引入的非拥有(non-owning)的字符视图类 。它提供了一种轻量级的方式来 "查看" 一个已有的字符串(或字符数组),而无需复制其内容。你可以把它想象成一个指向现有字符串的 "望远镜" 或 "观察窗口",它本身不分配或管理内存。
🔗 参考:https://en.cppreference.com/w/cpp/header/string_view.html
std::string_view 提供和 std::string 完全类似的接口,它的设计思路是非拥有的视图,只保存原始位置的指针和长度 ;无所有权,不负责字符串的生命周期管理,const 视图只能观察,不能修改底层字符串。
💻 基础使用示例
cpp
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <cctype>
// 基础打印函数 - 展示 string_view 的核心能力
void print(std::string_view sv, const std::string& title = "string_view") {
std::cout << "\n=== " << title << " ===\n";
std::cout << "内容: '" << sv << "'\n";
std::cout << "长度: " << sv.size() << " 字符\n";
std::cout << "是否为空: " << (sv.empty() ? "是" : "否") << "\n";
// 遍历方式1:下标访问(O(1) 随机访问)
std::cout << "字符(下标): ";
for (size_t i = 0; i < sv.size(); ++i) {
std::cout << sv[i] << " ";
}
std::cout << "\n";
// 遍历方式2:范围 for 循环(现代 C++ 风格)
std::cout << "字符(范围): ";
for (char ch : sv) {
std::cout << ch << " ";
}
std::cout << "\n";
// 遍历方式3:迭代器(算法友好)
std::cout << "字符(迭代器): ";
for (auto it = sv.begin(); it != sv.end(); ++it) {
std::cout << *it << " ";
}
std::cout << "\n";
}
// 实际应用:字符串分割函数(不复制数据,高效)
std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
while (start < str.size()) {
size_t end = str.find(delimiter, start);
if (end == std::string_view::npos) {
// 最后一部分
result.push_back(str.substr(start));
break;
}
// 添加子串(零拷贝)
result.push_back(str.substr(start, end - start));
start = end + 1;
}
return result;
}
// 实际应用:移除空格(延迟处理,不修改原字符串)
std::string_view trim(std::string_view str) {
// 移除前导空格
while (!str.empty() && std::isspace(static_cast<unsigned char>(str.front()))) {
str.remove_prefix(1);
}
// 移除尾随空格
while (!str.empty() && std::isspace(static_cast<unsigned char>(str.back()))) {
str.remove_suffix(1);
}
return str;
}
// 实际应用:判断是否为前缀
bool starts_with(std::string_view str, std::string_view prefix) {
return str.size() >= prefix.size() && str.substr(0, prefix.size()) == prefix;
}
// 实际应用:判断是否为后缀
bool ends_with(std::string_view str, std::string_view suffix) {
return str.size() >= suffix.size() &&
str.substr(str.size() - suffix.size()) == suffix;
}
// 实际应用:大小写不敏感比较
bool iequals(std::string_view a, std::string_view b) {
if (a.size() != b.size()) return false;
auto to_lower = [](char c) {
return std::tolower(static_cast<unsigned char>(c));
};
return std::equal(a.begin(), a.end(), b.begin(), to_lower);
}
// 性能对比演示
void performance_demo() {
std::cout << "\n=== 性能优化演示 ===\n";
// ❌ 低效:不必要的字符串拷贝
std::string huge_string(1000000, 'A');
auto start = std::chrono::high_resolution_clock::now();
std::string copy = huge_string.substr(100, 500); // 复制数据
auto end = std::chrono::high_resolution_clock::now();
auto copy_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "字符串拷贝耗时: " << copy_time.count() << " 微秒\n";
// ✅ 高效:string_view 零拷贝
start = std::chrono::high_resolution_clock::now();
std::string_view view = std::string_view(huge_string).substr(100, 500); // 不复制
end = std::chrono::high_resolution_clock::now();
auto view_time = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "string_view 耗时: " << view_time.count() << " 微秒\n";
std::cout << "性能提升: " << (double)copy_time.count() / view_time.count() << " 倍\n";
}
int main() {
// ========== 1. 基础构造方式 ==========
std::cout << "1. 基础构造方式\n" << std::string(50, '=') << "\n";
// 从字符串字面量构造
std::string_view sv1 = "Hello, world!";
print(sv1, "从字面量构造");
// 从 std::string 构造
std::string str = "C++17 string_view is powerful!";
std::string_view sv2 = str;
print(sv2, "从 std::string 构造");
// 从部分字符串构造(指针+长度)
std::string_view sv3(str.c_str() + 6, 11); // "string_view"
print(sv3, "从部分字符串构造");
// 使用字面量后缀(需要 using namespace std::literals)
using namespace std::literals;
std::string_view sv4 = "Literal string_view"sv;
print(sv4, "字面量后缀");
// ========== 2. 实际应用场景 ==========
std::cout << "\n2. 实际应用场景\n" << std::string(50, '=') << "\n";
// 字符串分割
std::string csv = "apple,banana,cherry,date";
auto fruits = split(csv, ',');
std::cout << "CSV 分割: ";
for (auto fruit : fruits) {
std::cout << "'" << fruit << "' ";
}
std::cout << "\n";
// 移除空格
std::string messy = " \t Hello, World! \n";
std::string_view cleaned = trim(messy);
std::cout << "原字符串: '" << messy << "'\n";
std::cout << "去除空格后: '" << cleaned << "'\n";
std::cout << "原字符串未修改: '" << messy << "' (证明零拷贝)\n";
// 前缀/后缀检查
std::string filename = "document.pdf";
std::string_view fv = filename;
std::cout << "文件名: " << fv << "\n";
std::cout << "以 'doc' 开头? " << (starts_with(fv, "doc") ? "是" : "否") << "\n";
std::cout << "以 '.pdf' 结尾? " << (ends_with(fv, ".pdf") ? "是" : "否") << "\n";
// 大小写不敏感比较
std::string_view str1 = "Hello";
std::string_view str2 = "HELLO";
std::cout << "忽略大小写比较: '" << str1 << "' vs '" << str2 << "' = "
<< (iequals(str1, str2) ? "相等" : "不等") << "\n";
// ========== 3. 边界情况和注意事项 ==========
std::cout << "\n3. 边界情况和注意事项\n" << std::string(50, '=') << "\n";
// 空字符串视图
std::string_view empty;
print(empty, "空 string_view");
// 临时字符串的危险用法(演示)
std::cout << "⚠️ 警告:string_view 不拥有数据!\n";
std::string_view dangerous;
{
std::string temp = "临时字符串";
dangerous = temp; // 危险!temp 即将销毁
std::cout << "临时对象存在时: " << dangerous << "\n";
}
// dangerous 现在是悬空引用!不要取消注释下一行
// std::cout << "临时对象销毁后: " << dangerous << "\n"; // 未定义行为!
// 安全做法:确保底层数据生命周期足够长
std::string safe_str = "安全字符串";
std::string_view safe_view = safe_str;
std::cout << "安全使用: " << safe_view << "\n";
// ========== 4. find 系列操作 ==========
std::cout << "\n4. 查找操作\n" << std::string(50, '=') << "\n";
std::string_view text = "The quick brown fox jumps over the lazy dog";
std::cout << "文本: '" << text << "'\n";
auto pos = text.find("fox");
if (pos != std::string_view::npos) {
std::cout << "找到 'fox' 在位置 " << pos << "\n";
std::cout << "子串: " << text.substr(pos, 3) << "\n";
}
pos = text.find_first_of("aeiou");
if (pos != std::string_view::npos) {
std::cout << "第一个元音 '" << text[pos] << "' 在位置 " << pos << "\n";
}
// ========== 5. 性能演示 ==========
performance_demo();
// ========== 6. 现代 C++ 最佳实践 ==========
std::cout << "\n5. 最佳实践建议\n" << std::string(50, '=') << "\n";
// 函数参数使用 string_view
auto process = [](std::string_view data) {
// 不拷贝,只读取
return data.size();
};
// 可以传字符串字面量、std::string、另一个 string_view
std::cout << "字符串字面量长度: " << process("Hello") << "\n";
std::cout << "std::string 长度: " << process(std::string("World")) << "\n";
// 避免 string_view 悬空
std::cout << "\n✅ 核心原则:\n";
std::cout << "1. string_view 像指针一样,不拥有数据\n";
std::cout << "2. 确保底层数据生命周期覆盖 string_view 的使用\n";
std::cout << "3. 函数参数用 string_view,返回值用 string(如果需要拥有数据)\n";
std::cout << "4. 修改原字符串会使 string_view 失效\n";
std::cout << "5. string_view 不保证以 \\0 结尾,使用 .data() 要小心\n";
return 0;
}
✨ 核心优势与典型场景
std::string_view 几乎是零开销,它不会复制字符串数据,构造和析构成本极低。并且提供了与 std::string 非常相似的接口,几乎提供了所有的读相关接口,并且兼容性强,它可以高效地从 std::string、字符串字面量、char* 等任何字符序列构造而来。这样在很多场景下可以减少构造和拷贝,又可以利用 string 类似的相关接口处理串。
cpp
/**
* @file string_view_core_demo.cpp
* @brief std::string_view 核心优势与典型场景演示
* @author Code Reviewer
* @date 2026-05-12
*
* @details 本文件展示 std::string_view 的核心特性:
* 1. 零拷贝(不复制字符串数据)
* 2. 只读访问(非拥有型视图)
* 3. 轻量级(仅存储指针+长度,16字节)
* 4. 高效子串操作(无需复制)
* 5. 兼容多种字符串类型(字面量、std::string、C字符串)
*
* 编译运行:
* g++ -std=c++17 -O2 string_view_core_demo.cpp -o core_demo
* ./core_demo
*/
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <algorithm>
#include <cctype>
/**
* @brief 打印 string_view 的基本信息(辅助函数)
* @param sv 要打印的字符串视图
* @param title 可选的标题
*/
void print_info(std::string_view sv, const std::string& title = "string_view") {
std::cout << "\n┌─────────────────────────────────────────┐\n";
std::cout << "│ " << title << "\n";
std::cout << "├─────────────────────────────────────────┤\n";
std::cout << "│ 内容: '" << sv << "'\n";
std::cout << "│ 长度: " << sv.size() << " 字符\n";
std::cout << "│ 是否为空: " << (sv.empty() ? "是" : "否") << "\n";
std::cout << "│ sizeof(sv): " << sizeof(sv) << " 字节\n";
std::cout << "└─────────────────────────────────────────┘\n";
}
/**
* @brief 【典型场景1】字符串分割(零拷贝)
* @param str 要分割的字符串视图
* @param delimiter 分隔符
* @return 分割后的子串视图列表(不复制任何字符数据)
*
* @note 这是 string_view 最经典的用法之一
* 传统方式:会为每个子串分配内存并复制字符
* 本方式:只记录每个子串在原字符串中的位置和长度
*/
std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
while (start < str.size()) {
// 查找下一个分隔符
size_t end = str.find(delimiter, start);
if (end == std::string_view::npos) {
// 最后一部分(或没有分隔符)
result.push_back(str.substr(start));
break;
}
// 添加子串视图(零拷贝!只记录位置和长度)
result.push_back(str.substr(start, end - start));
start = end + 1;
}
return result;
}
/**
* @brief 【典型场景2】去除首尾空格(非破坏性)
* @param str 需要处理的字符串视图
* @return 去除首尾空格后的新视图
*
* @note 不修改原字符串,不复制数据,只是调整视图的边界
* 时间复杂度 O(n),但空间复杂度 O(1)
*/
std::string_view trim(std::string_view str) {
// 移除前导空白字符(空格、制表符、换行等)
while (!str.empty() && std::isspace(static_cast<unsigned char>(str.front()))) {
str.remove_prefix(1); // 将视图起始位置右移1
}
// 移除尾随空白字符
while (!str.empty() && std::isspace(static_cast<unsigned char>(str.back()))) {
str.remove_suffix(1); // 将视图结束位置左移1
}
return str;
}
/**
* @brief 【典型场景3】检查字符串前缀
* @param str 待检查的字符串
* @param prefix 可能的前缀
* @return 是否是前缀
*/
bool starts_with(std::string_view str, std::string_view prefix) {
// 首先确保原字符串长度足够
if (str.size() < prefix.size()) {
return false;
}
// substr 是 O(1) 操作,不会复制数据
return str.substr(0, prefix.size()) == prefix;
}
/**
* @brief 【典型场景4】检查字符串后缀
* @param str 待检查的字符串
* @param suffix 可能的后缀
* @return 是否是后缀
*/
bool ends_with(std::string_view str, std::string_view suffix) {
if (str.size() < suffix.size()) {
return false;
}
// 从尾部取子串进行对比,仍然是 O(1)
return str.substr(str.size() - suffix.size()) == suffix;
}
/**
* @brief 【典型场景5】大小写不敏感的比较
* @param a 字符串A
* @param b 字符串B
* @return 是否相等(忽略大小写)
*/
bool iequals(std::string_view a, std::string_view b) {
if (a.size() != b.size()) {
return false;
}
// 双参数 lambda:比较两个字符是否相等(忽略大小写)
auto char_iequal = [](char c1, char c2) {
return std::tolower(static_cast<unsigned char>(c1)) ==
std::tolower(static_cast<unsigned char>(c2));
};
return std::equal(a.begin(), a.end(), b.begin(), char_iequal);
}
/**
* @brief 【典型场景6】提取文件扩展名
* @param filename 文件名
* @return 扩展名视图(不含点号)
*/
std::string_view get_extension(std::string_view filename) {
// 查找最后一个点号
size_t dot_pos = filename.rfind('.');
if (dot_pos == std::string_view::npos) {
return {}; // 没有扩展名,返回空视图
}
// 返回点号之后的部分(零拷贝)
return filename.substr(dot_pos + 1);
}
/**
* @brief 【典型场景7】解析键值对
* @param kv_pair 格式为 "key=value" 的字符串
* @param key 输出参数:存储键的视图
* @param value 输出参数:存储值的视图
* @return 是否解析成功
*/
bool parse_key_value(std::string_view kv_pair, std::string_view& key, std::string_view& value) {
size_t eq_pos = kv_pair.find('=');
if (eq_pos == std::string_view::npos) {
return false;
}
// 直接使用视图,不复制数据
key = trim(kv_pair.substr(0, eq_pos));
value = trim(kv_pair.substr(eq_pos + 1));
return !key.empty(); // 键不能为空
}
/**
* @brief 【典型场景8】灵活的函数参数
* @param data 可以接受任何字符串类型(字面量、std::string、C字符串)
* @return 字符串长度
*
* @note 使用 string_view 作为函数参数是最佳实践:
* 1. 不需要重载多个版本
* 2. 避免不必要的字符串拷贝
* 3. 调用者无需修改代码
*/
size_t get_length(std::string_view data) {
// 这里只读取,不修改,完美发挥 string_view 的优势
return data.size();
}
// ==================== 主函数 ====================
int main() {
std::cout << "\n";
std::cout << "╔══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ std::string_view 核心优势与典型场景演示 ║\n";
std::cout << "╚══════════════════════════════════════════════════════════════╝\n";
// ==================== 一、核心优势演示 ====================
std::cout << "\n【一、核心优势】\n";
std::cout << "─────────────────────────────────────────────────────────────\n";
// 优势1:极轻量(只占16字节)
std::cout << "\n✓ 优势1:轻量级(仅16字节)\n";
std::cout << " sizeof(std::string_view) = " << sizeof(std::string_view) << " 字节\n";
std::cout << " sizeof(std::string) = " << sizeof(std::string) << " 字节\n";
// 优势2:零拷贝构造
std::cout << "\n✓ 优势2:零拷贝构造\n";
std::string original = "这是一个很长的字符串,构造视图时不会复制任何字符数据";
std::string_view view = original; // 只复制指针和长度,不复制字符
std::cout << " 原字符串地址: " << (void*)original.data() << "\n";
std::cout << " 视图存储地址: " << (void*)view.data() << " (指向同一块内存)\n";
// 优势3:零拷贝子串
std::cout << "\n✓ 优势3:零拷贝子串操作\n";
std::string_view text = "The quick brown fox jumps over the lazy dog";
std::string_view sub = text.substr(10, 9); // "brown fox"
std::cout << " 原字符串: '" << text << "'\n";
std::cout << " 子串视图: '" << sub << "'\n";
std::cout << " 地址比较: 原:" << (void*)text.data()
<< " vs 子:" << (void*)sub.data() << " (相同)\n";
std::cout << " 说明: substr 只记录了新的起始位置,没有复制数据\n";
// ==================== 二、典型应用场景 ====================
std::cout << "\n【二、典型应用场景】\n";
std::cout << "─────────────────────────────────────────────────────────────\n";
// 场景1:字符串分割
std::cout << "\n✓ 场景1:高效字符串分割\n";
std::string csv = "apple,banana,cherry,date,elderberry";
std::cout << " CSV 数据: " << csv << "\n";
auto fruits = split(csv, ',');
std::cout << " 分割结果(零拷贝视图): ";
for (size_t i = 0; i < fruits.size(); ++i) {
std::cout << "'" << fruits[i] << "'";
if (i < fruits.size() - 1) std::cout << ", ";
}
std::cout << "\n";
std::cout << " 注意:所有子串视图都直接指向原字符串内存\n";
// 场景2:去除首尾空格
std::cout << "\n✓ 场景2:非破坏性空格去除\n";
std::string messy = " \t Hello, World! \n\r";
std::string_view trimmed = trim(messy);
std::cout << " 原字符串: '" << messy << "'\n";
std::cout << " 去除空格: '" << trimmed << "'\n";
std::cout << " 原字符串未被修改: '" << messy << "'\n";
// 场景3:前缀/后缀检查
std::cout << "\n✓ 场景3:高效前缀/后缀检查\n";
std::string filename = "document.pdf";
std::string_view fv = filename;
std::cout << " 文件名: " << fv << "\n";
std::cout << " 以 'doc' 开头? " << (starts_with(fv, "doc") ? "✓ 是" : "✗ 否") << "\n";
std::cout << " 以 '.pdf' 结尾? " << (ends_with(fv, ".pdf") ? "✓ 是" : "✗ 否") << "\n";
// 场景4:文件扩展名提取
std::cout << "\n✓ 场景4:文件扩展名提取\n";
std::vector<std::string> files = { "image.jpg", "document.pdf", "script.py", "Makefile" };
for (const auto& file : files) {
std::string_view ext = get_extension(file);
std::cout << " " << file << " → 扩展名: " << (ext.empty() ? "(无)" : ext.data()) << "\n";
}
// 场景5:解析键值对
std::cout << "\n✓ 场景5:解析键值对\n";
std::vector<std::string> kv_strings = {
"name=John Doe",
" age = 25 ",
"email=john@example.com"
};
for (const auto& kv : kv_strings) {
std::string_view key, value;
if (parse_key_value(kv, key, value)) {
std::cout << " 解析: |" << key << "| → |" << value << "|\n";
}
}
// 场景6:大小写不敏感比较
std::cout << "\n✓ 场景6:大小写不敏感比较\n";
std::string_view hello1 = "Hello";
std::string_view hello2 = "HELLO";
std::string_view hello3 = "World";
std::cout << " 'Hello' vs 'HELLO': " << (iequals(hello1, hello2) ? "相等" : "不等") << "\n";
std::cout << " 'Hello' vs 'World': " << (iequals(hello1, hello3) ? "相等" : "不等") << "\n";
// 场景7:灵活的字符串类型兼容性
std::cout << "\n✓ 场景7:统一处理多种字符串类型\n";
const char* c_str = "C字符串";
std::string cpp_str = "C++字符串";
std::string_view view_str = "字符串视图";
// 同一个函数可以接受所有类型
std::cout << " C字符串长度: " << get_length(c_str) << "\n";
std::cout << " C++字符串长度: " << get_length(cpp_str) << "\n";
std::cout << " 字符串视图长度: " << get_length(view_str) << "\n";
// ==================== 三、输出格式演示 ====================
std::cout << "\n【三、多种遍历方式】\n";
std::cout << "─────────────────────────────────────────────────────────────\n";
std::string_view demo = "C++17";
print_info(demo, "遍历示例");
std::cout << "\n 遍历方式1(下标): ";
for (size_t i = 0; i < demo.size(); ++i) {
std::cout << demo[i] << " ";
}
std::cout << "\n 遍历方式2(范围for): ";
for (char ch : demo) {
std::cout << ch << " ";
}
std::cout << "\n 遍历方式3(迭代器): ";
for (auto it = demo.begin(); it != demo.end(); ++it) {
std::cout << *it << " ";
}
std::cout << "\n";
std::cout << "\n═══════════════════════════════════════════════════════════════\n";
std::cout << "✅ 演示完成!string_view 的核心优势:\n";
std::cout << " • 零拷贝(不分配内存)\n";
std::cout << " • 轻量级(只占16字节)\n";
std::cout << " • 兼容多种字符串类型\n";
std::cout << " • 高效子串操作\n";
std::cout << " • 作为函数参数的最佳实践\n";
std::cout << "═══════════════════════════════════════════════════════════════\n";
return 0;
}
bash
╔══════════════════════════════════════════════════════════════╗
║ std::string_view 核心优势与典型场景演示 ║
╚══════════════════════════════════════════════════════════════╝
【一、核心优势】
─────────────────────────────────────────────────────────────
✓ 优势1:轻量级(仅16字节)
sizeof(std::string_view) = 16 字节
sizeof(std::string) = 40 字节
✓ 优势2:零拷贝构造
原字符串地址: 000001E96A6DBE30
视图存储地址: 000001E96A6DBE30 (指向同一块内存)
✓ 优势3:零拷贝子串操作
原字符串: 'The quick brown fox jumps over the lazy dog'
子串视图: 'brown fox'
地址比较: 原:00007FF6F8803950 vs 子:00007FF6F880395A (相同)
说明: substr 只记录了新的起始位置,没有复制数据
【二、典型应用场景】
─────────────────────────────────────────────────────────────
✓ 场景1:高效字符串分割
CSV 数据: apple,banana,cherry,date,elderberry
分割结果(零拷贝视图): 'apple', 'banana', 'cherry', 'date', 'elderberry'
注意:所有子串视图都直接指向原字符串内存
✓ 场景2:非破坏性空格去除
原字符串: ' Hello, World!
'
去除空格: 'Hello, World!'
原字符串未被修改: ' Hello, World!
'
✓ 场景3:高效前缀/后缀检查
文件名: document.pdf
以 'doc' 开头? ✓ 是
以 '.pdf' 结尾? ✓ 是
✓ 场景4:文件扩展名提取
image.jpg → 扩展名: jpg
document.pdf → 扩展名: pdf
script.py → 扩展名: py
Makefile → 扩展名: (无)
✓ 场景5:解析键值对
解析: |name| → |John Doe|
解析: |age| → |25|
解析: |email| → |john@example.com|
✓ 场景6:大小写不敏感比较
'Hello' vs 'HELLO': 相等
'Hello' vs 'World': 不等
✓ 场景7:统一处理多种字符串类型
C字符串长度: 10
C++字符串长度: 12
字符串视图长度: 15
【三、多种遍历方式】
─────────────────────────────────────────────────────────────
┌─────────────────────────────────────────┐
│ 遍历示例
├─────────────────────────────────────────┤
│ 内容: 'C++17'
│ 长度: 5 字符
│ 是否为空: 否
│ sizeof(sv): 16 字节
└─────────────────────────────────────────┘
遍历方式1(下标): C + + 1 7
遍历方式2(范围for): C + + 1 7
遍历方式3(迭代器): C + + 1 7
═══════════════════════════════════════════════════════════════
✅ 演示完成!string_view 的核心优势:
• 零拷贝(不分配内存)
• 轻量级(只占16字节)
• 兼容多种字符串类型
• 高效子串操作
• 作为函数参数的最佳实践
═══════════════════════════════════════════════════════════════
E:\myLearn\StringView\x64\Debug\StringView.exe (进程 25764)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
⚡ 性能对比测试
cpp
/**
* @file string_view_performance_demo.cpp
* @brief std::string_view 性能对比测试
* @author Code Reviewer
* @date 2026-05-12
*
* @details 本文件通过基准测试对比 string_view 和传统 string 的性能差异:
* 1. 构造性能对比(拷贝 vs 视图)
* 2. 子串操作性能对比
* 3. 函数参数传递性能对比
* 4. 字符串分割性能对比
* 5. 大规模数据处理测试
*
* 编译运行:
* g++ -std=c++17 -O2 string_view_performance_demo.cpp -o perf_demo
* ./perf_demo
*/
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <chrono>
#include <algorithm>
#include <numeric>
#include <iomanip>
/**
* @brief 高性能计时器类
* @details 用于精确测量代码块执行时间
*/
class Timer {
private:
std::chrono::high_resolution_clock::time_point start_time;
std::string name;
public:
/**
* @brief 构造函数,开始计时
* @param test_name 测试名称
*/
Timer(const std::string& test_name) : name(test_name) {
start_time = std::chrono::high_resolution_clock::now();
}
/**
* @brief 析构函数,自动输出耗时
*/
~Timer() {
auto end_time = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end_time - start_time);
std::cout << " ⏱️ " << name << ": " << std::setw(8) << duration.count() << " 微秒\n";
}
/**
* @brief 手动获取耗时(不结束计时)
*/
long long elapsed() const {
auto now = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::microseconds>(now - start_time).count();
}
};
/**
* @brief 使用 std::string 进行字符串分割(传统方式)
* @param str 字符串
* @param delimiter 分隔符
* @return 分割后的子串列表(每个子串都有独立的内存)
*
* @note 这种方式会为每个子串分配内存并复制字符数据
*/
std::vector<std::string> split_string(const std::string& str, char delimiter) {
std::vector<std::string> result;
size_t start = 0;
while (start < str.size()) {
size_t end = str.find(delimiter, start);
if (end == std::string::npos) {
result.push_back(str.substr(start)); // 复制数据
break;
}
result.push_back(str.substr(start, end - start)); // 复制数据
start = end + 1;
}
return result;
}
/**
* @brief 使用 std::string_view 进行字符串分割(现代方式)
* @param str 字符串视图
* @param delimiter 分隔符
* @return 分割后的子串视图列表(零拷贝)
*
* @note 这种方式不复制任何字符数据,只记录位置和长度
*/
std::vector<std::string_view> split_string_view(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
while (start < str.size()) {
size_t end = str.find(delimiter, start);
if (end == std::string_view::npos) {
result.push_back(str.substr(start)); // 零拷贝
break;
}
result.push_back(str.substr(start, end - start)); // 零拷贝
start = end + 1;
}
return result;
}
/**
* @brief 性能测试1:构造性能对比
* @param iterations 迭代次数
*
* @details 对比创建 1000 个字符串子串的耗时差异
* 字符串视图的构造只是复制指针,远快于实际复制字符数据
*/
void test_construction_performance(int iterations = 1000) {
std::cout << "\n┌─────────────────────────────────────────────────────────────┐\n";
std::cout << "│ 测试1:构造性能对比(创建 " << iterations << " 个子串) │\n";
std::cout << "└─────────────────────────────────────────────────────────────┘\n";
// 准备一个长字符串
std::string long_string(10000, 'A');
for (int i = 0; i < 100; ++i) {
long_string[i * 100] = ','; // 添加分隔符
}
// 测试 std::string 构造(会复制字符)
{
Timer timer("std::string 构造(复制 " + std::to_string(iterations) + " 次)");
for (int i = 0; i < iterations; ++i) {
std::string copy = long_string; // 复制整个字符串
(void)copy;
}
}
// 测试 std::string_view 构造(只复制指针)
{
Timer timer("std::string_view 构造(" + std::to_string(iterations) + " 次)");
for (int i = 0; i < iterations; ++i) {
std::string_view view = long_string; // 只复制16字节
(void)view;
}
}
}
/**
* @brief 性能测试2:子串操作性能对比
* @param iterations 迭代次数
*
* @details 对比提取 10000 个子串的耗时
* string::substr 会复制数据,string_view::substr 只调整视图边界
*/
void test_substr_performance(int iterations = 10000) {
std::cout << "\n┌─────────────────────────────────────────────────────────────┐\n";
std::cout << "│ 测试2:子串操作性能对比(提取 " << iterations << " 个子串) │\n";
std::cout << "└─────────────────────────────────────────────────────────────┘\n";
std::string data = "https://www.example.com/very/long/path/to/resource";
// 测试 std::string::substr(复制数据)
{
Timer timer("std::string::substr(复制数据)");
for (int i = 0; i < iterations; ++i) {
std::string sub = data.substr(8, 10); // 复制 "www.exampl"
(void)sub;
}
}
// 测试 std::string_view::substr(零拷贝)
{
Timer timer("std::string_view::substr(零拷贝)");
std::string_view view = data;
for (int i = 0; i < iterations; ++i) {
std::string_view sub = view.substr(8, 10); // 只调整边界
(void)sub;
}
}
}
/**
* @brief 测试3:字符串分割性能对比
* @param iterations 重复测试次数
*
* @details 对比传统方式和视图方式在字符串分割上的性能差异
* 处理 1MB 的 CSV 数据,分割成约 200,000 个字段
*/
void test_split_performance(int iterations = 10) {
std::cout << "\n┌─────────────────────────────────────────────────────────────┐\n";
std::cout << "│ 测试3:字符串分割性能对比(1MB CSV 数据,重复 " << iterations << " 次) │\n";
std::cout << "└─────────────────────────────────────────────────────────────┘\n";
// 生成测试数据:1MB 的 CSV 格式字符串
std::string csv_data;
csv_data.reserve(1024 * 1024);
for (int i = 0; i < 100000; ++i) {
csv_data += "field" + std::to_string(i) + ",";
}
csv_data.back() = '\n'; // 替换最后一个逗号
// 测试 std::string 分割(会复制每个字段)
{
long long total_time = 0;
for (int i = 0; i < iterations; ++i) {
auto start = std::chrono::high_resolution_clock::now();
auto result = split_string(csv_data, ',');
auto end = std::chrono::high_resolution_clock::now();
total_time += std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
// 验证结果
if (i == 0) {
std::cout << " 📊 std::string 分割: " << result.size() << " 个字段\n";
}
}
std::cout << " ⏱️ std::string 分割(平均): "
<< std::setw(8) << total_time / iterations << " 微秒\n";
}
// 测试 std::string_view 分割(零拷贝)
{
long long total_time = 0;
for (int i = 0; i < iterations; ++i) {
auto start = std::chrono::high_resolution_clock::now();
auto result = split_string_view(csv_data, ',');
auto end = std::chrono::high_resolution_clock::now();
total_time += std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();
if (i == 0) {
std::cout << " 📊 string_view 分割: " << result.size() << " 个视图\n";
}
}
std::cout << " ⏱️ string_view 分割(平均): "
<< std::setw(8) << total_time / iterations << " 微秒\n";
}
}
/**
* @brief 测试4:函数参数传递性能对比
* @param iterations 迭代次数
*
* @details 对比传递 string 引用 vs string_view 的性能
* string_view 更轻量,且不会引起不必要的分配
*/
void test_parameter_passing(int iterations = 1000000) {
std::cout << "\n┌─────────────────────────────────────────────────────────────┐\n";
std::cout << "│ 测试4:函数参数传递性能对比(调用 " << iterations << " 次) │\n";
std::cout << "└─────────────────────────────────────────────────────────────┘\n";
std::string data = "这是一个测试字符串,用于性能对比";
// 定义测试函数
auto process_by_const_ref = [](const std::string& s) -> size_t {
return s.size();
};
auto process_by_string_view = [](std::string_view sv) -> size_t {
return sv.size();
};
// 测试 const std::string& 传递
{
Timer timer("const std::string& 传递");
size_t sum = 0;
for (int i = 0; i < iterations; ++i) {
sum += process_by_const_ref(data);
}
(void)sum;
}
// 测试 std::string_view 传递
{
Timer timer("std::string_view 传递");
size_t sum = 0;
for (int i = 0; i < iterations; ++i) {
sum += process_by_string_view(data);
}
(void)sum;
}
// 测试从字面量传递的情况(string_view 优势更明显)
{
Timer timer("string_view + 字面量(避免临时 string)");
size_t sum = 0;
for (int i = 0; i < iterations; ++i) {
sum += process_by_string_view("Hello, World!");
}
(void)sum;
}
}
/**
* @brief 测试5:大规模数据处理性能对比
* @param data_size_mb 数据大小(MB)
*
* @details 处理 10MB 的日志数据,模拟实际场景
* 统计以 'ERROR' 开头的行数
*/
void test_large_data_processing(int data_size_mb = 10) {
std::cout << "\n┌─────────────────────────────────────────────────────────────┐\n";
std::cout << "│ 测试5:大规模数据处理(" << data_size_mb << " MB 日志数据) │\n";
std::cout << "└─────────────────────────────────────────────────────────────┘\n";
// 生成测试日志数据
std::string log_data;
log_data.reserve(data_size_mb * 1024 * 1024);
for (int i = 0; i < 500000; ++i) {
if (i % 100 == 0) {
log_data += "ERROR: Something went wrong at step " + std::to_string(i) + "\n";
} else {
log_data += "INFO: Processing step " + std::to_string(i) + "\n";
}
}
// 使用 std::string 的方式(复制并分割)
{
Timer timer("std::string 方式(复制行数据)");
int error_count = 0;
size_t start = 0;
std::string data_copy = log_data; // 先复制整个数据
while (start < data_copy.size()) {
size_t end = data_copy.find('\n', start);
if (end == std::string::npos) break;
std::string line = data_copy.substr(start, end - start); // 复制每一行
if (line.size() >= 5 && line.substr(0, 5) == "ERROR") {
++error_count;
}
start = end + 1;
}
std::cout << " 📊 找到 " << error_count << " 条 ERROR 日志\n";
}
// 使用 std::string_view 的方式(只读视图)
{
Timer timer("string_view 方式(零拷贝)");
int error_count = 0;
size_t start = 0;
std::string_view view = log_data; // 不复制
while (start < view.size()) {
size_t end = view.find('\n', start);
if (end == std::string_view::npos) break;
std::string_view line = view.substr(start, end - start); // 零拷贝视图
if (line.size() >= 5 && line.substr(0, 5) == "ERROR") {
++error_count;
}
start = end + 1;
}
std::cout << " 📊 找到 " << error_count << " 条 ERROR 日志\n";
}
}
/**
* @brief 显示速度对比总结
*/
void show_summary() {
std::cout << "\n\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 性能测试总结 ║\n";
std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
std::cout << "║ std::string_view 的核心性能优势: ║\n";
std::cout << "║ ║\n";
std::cout << "║ 1. 零拷贝构造 → 只复制16字节,与字符串长度无关 ║\n";
std::cout << "║ 2. 零拷贝子串 → O(1) 时间,不随子串长度增加 ║\n";
std::cout << "║ 3. 减少内存分配 → 避免频繁的 malloc/free ║\n";
std::cout << "║ 4. 更好的缓存局部性 → 数据在原位置,不产生内存碎片 ║\n";
std::cout << "║ 5. 无额外内存开销 → 适合嵌入式/高性能环境 ║\n";
std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
std::cout << "║ 适用场景: ║\n";
std::cout << "║ • 函数参数(避免不必要的拷贝) ║\n";
std::cout << "║ • 字符串解析(配置文件、日志、网络协议) ║\n";
std::cout << "║ • 大规模文本处理(日志分析、数据清洗) ║\n";
std::cout << "║ • 只读访问场景(查找、比较、过滤) ║\n";
std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
std::cout << "║ 注意事项: ║\n";
std::cout << "║ • 不保证以 '\\0' 结尾,使用 .data() 需谨慎 ║\n";
std::cout << "║ • 不拥有数据,需确保底层数据生命周期更长 ║\n";
std::cout << "║ • 修改原字符串会使视图失效 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
}
// ==================== 主函数 ====================
int main() {
std::cout << "\n";
std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ std::string_view 性能对比测试 ║\n";
std::cout << "║ 编译优化: -O2 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// 运行所有性能测试
test_construction_performance(1000);
test_substr_performance(10000);
test_split_performance(10);
test_parameter_passing(1000000);
test_large_data_processing(5);
// 显示总结
show_summary();
return 0;
}
bash
╔═══════════════════════════════════════════════════════════════╗
║ std::string_view 性能对比测试 ║
║ 编译优化: -O2 ║
╚═══════════════════════════════════════════════════════════════╝
┌─────────────────────────────────────────────────────────────┐
│ 测试1:构造性能对比(创建 1000 个子串) │
└─────────────────────────────────────────────────────────────┘
⏱️ std::string 构造(复制 1000 次): 813 微秒
⏱️ std::string_view 构造(1000 次): 16 微秒
┌─────────────────────────────────────────────────────────────┐
│ 测试2:子串操作性能对比(提取 10000 个子串) │
└─────────────────────────────────────────────────────────────┘
⏱️ std::string::substr(复制数据): 3824 微秒
⏱️ std::string_view::substr(零拷贝): 342 微秒
┌─────────────────────────────────────────────────────────────┐
│ 测试3:字符串分割性能对比(1MB CSV 数据,重复 10 次) │
└─────────────────────────────────────────────────────────────┘
📊 std::string 分割: 100000 个字段
⏱️ std::string 分割(平均): 188841 微秒
📊 string_view 分割: 100000 个视图
⏱️ string_view 分割(平均): 11620 微秒
┌─────────────────────────────────────────────────────────────┐
│ 测试4:函数参数传递性能对比(调用 1000000 次) │
└─────────────────────────────────────────────────────────────┘
⏱️ const std::string& 传递: 3061 微秒
⏱️ std::string_view 传递: 18145 微秒
⏱️ string_view + 字面量(避免临时 string): 13493 微秒
┌─────────────────────────────────────────────────────────────┐
│ 测试5:大规模数据处理(5 MB 日志数据) │
└─────────────────────────────────────────────────────────────┘
📊 找到 5000 条 ERROR 日志
⏱️ std::string 方式(复制行数据): 589907 微秒
📊 找到 5000 条 ERROR 日志
⏱️ string_view 方式(零拷贝): 54217 微秒
╔═══════════════════════════════════════════════════════════════╗
║ 性能测试总结 ║
╠═══════════════════════════════════════════════════════════════╣
║ std::string_view 的核心性能优势: ║
║ ║
║ 1. 零拷贝构造 → 只复制16字节,与字符串长度无关 ║
║ 2. 零拷贝子串 → O(1) 时间,不随子串长度增加 ║
║ 3. 减少内存分配 → 避免频繁的 malloc/free ║
║ 4. 更好的缓存局部性 → 数据在原位置,不产生内存碎片 ║
║ 5. 无额外内存开销 → 适合嵌入式/高性能环境 ║
╠═══════════════════════════════════════════════════════════════╣
║ 适用场景: ║
║ • 函数参数(避免不必要的拷贝) ║
║ • 字符串解析(配置文件、日志、网络协议) ║
║ • 大规模文本处理(日志分析、数据清洗) ║
║ • 只读访问场景(查找、比较、过滤) ║
╠═══════════════════════════════════════════════════════════════╣
║ 注意事项: ║
║ • 不保证以 '\0' 结尾,使用 .data() 需谨慎 ║
║ • 不拥有数据,需确保底层数据生命周期更长 ║
║ • 修改原字符串会使视图失效 ║
╚═══════════════════════════════════════════════════════════════╝
E:\myLearn\StringView\x64\Debug\StringView.exe (进程 24704)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .
⚠️ 注意事项
- 生命周期管理 :
std::string_view不管理底层字符串的生命周期,string_view对象的生命周期不能超过指向的字符串。如果你需要传递一个string_view,必须确保它指向的字符串在整个使用期间都有效。 \0结尾问题 :string_view不一定以\0结尾,如果你需要传递给一个以\0结尾的 C API(如fopen),必须确保它是\0结尾的,或者手动创建一个以\0结尾的副本(例如使用std::string(sv))。- 底层字符串修改 :如果底层字符串被修改或销毁,
string_view会变成 "悬空" 状态,访问它会导致未定义行为。
cpp
#define _CRT_SECURE_NO_WARNINGS
/**
* @file string_view_lifetime_demo.cpp
* @brief std::string_view 生命周期管理、\0结尾、底层修改 三大注意事项验证
* @author Code Reviewer
* @date 2026-05-12
*
* @details 本文件演示 std::string_view 的三个关键陷阱:
* 1. 生命周期管理:string_view 不拥有数据,不能比源字符串活得久
* 2. \0 结尾问题:string_view 不一定以 \0 结尾,传给 C API 需小心
* 3. 底层字符串修改:修改原字符串会使 string_view 失效
*
* 编译运行:
* g++ -std=c++17 -O2 string_view_lifetime_demo.cpp -o demo
* ./demo
*/
#include <iostream>
#include <string>
#include <string_view>
#include <vector>
#include <cstring>
#include <fstream>
// ==================== 辅助函数 ====================
/**
* @brief 安全打印 string_view(带边界标识)
*/
void safe_print(std::string_view sv, const std::string& title = "") {
if (!title.empty()) {
std::cout << "\n【" << title << "】\n";
}
std::cout << " 内容: '" << sv << "'\n";
std::cout << " 长度: " << sv.size() << "\n";
std::cout << " 是否以\\0结尾: " << (sv.data()[sv.size()] == '\0' ? "是" : "否") << "\n";
}
// ==================== 注意事项1:生命周期管理 ====================
/**
* @brief 危险示例:返回局部字符串的 string_view
* @return 悬空的 string_view(未定义行为)
*
* ⚠️ 这是典型的错误用法!
* 局部字符串在函数返回时被销毁,返回的 string_view 变成悬空引用。
*/
std::string_view dangerous_return() {
std::string local = "这个字符串马上会被销毁";
std::string_view sv = local;
std::cout << " [函数内部] sv 有效: '" << sv << "'\n";
// ⚠️ 返回 sv,但 local 即将被销毁
return sv; // 危险!返回悬空引用
}
/**
* @brief 正确示例:返回拥有所有权的字符串
* @return 安全的 std::string
*
* ✅ 如果需要返回字符串,应该返回 std::string 而不是 string_view
*/
std::string safe_return() {
std::string local = "这个字符串会安全返回";
return local; // 移动语义,高效且安全
}
/**
* @brief 正确示例:从已有数据创建 string_view
* @param data 生命周期足够长的数据
* @return 安全的 string_view
*
* ✅ string_view 只作为观察者,不拥有数据
*/
std::string_view safe_parameter(const std::string& data) {
return data; // 安全:调用者保证 data 生命周期足够长
}
/**
* @brief 验证生命周期管理的正确与错误用法
*/
void verify_lifetime() {
std::cout << "\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 注意事项1:生命周期管理 ║\n";
std::cout << "║ string_view 不管理生命周期,不能比源字符串活得久 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// ✅ 正确用法1:string_view 生命周期在源字符串内
std::cout << "\n✅ 正确示例:生命周期内使用\n";
{
std::string data = "持久存在的字符串";
std::string_view view = data; // view 指向 data
std::cout << " view: '" << view << "'\n";
// data 和 view 都在作用域内,安全
std::cout << " → 安全:view 和 data 在同一作用域\n";
}
// ✅ 正确用法2:作为函数参数
std::cout << "\n✅ 正确示例:作为函数参数\n";
{
std::string data = "作为参数传递";
auto process = [](std::string_view sv) {
std::cout << " 函数内接收到: '" << sv << "'\n";
};
process(data); // 调用者保证 data 在函数执行期间有效
std::cout << " → 安全:调用者保证生命周期\n";
}
// ❌ 危险示例:返回局部字符串的视图
std::cout << "\n❌ 危险示例:返回局部变量的 view\n";
std::cout << " ⚠️ 下面的代码会触发未定义行为,已被注释\n";
// std::string_view dangling = dangerous_return(); // 未定义行为!
// std::cout << dangling << "\n"; // 可能崩溃或输出乱码
std::cout << " → 危险:view 指向已销毁的内存,访问会导致未定义行为\n";
// ✅ 正确示例:如果需要返回,返回 std::string
std::cout << "\n✅ 正确做法:需要返回时使用 std::string\n";
{
std::string result = safe_return();
std::cout << " 返回的字符串: '" << result << "'\n";
std::cout << " → 安全:返回拥有所有权的 std::string\n";
}
// 可视化生命周期问题
std::cout << "\n📊 生命周期图解:\n";
std::cout << " ┌─────────────────────────────────────────────────────────┐\n";
std::cout << " │ std::string data = \"Hello\"; // data 拥有内存 │\n";
std::cout << " │ std::string_view sv = data; // sv 指向 data │\n";
std::cout << " │ // data 和 sv 都在作用域内 // ✅ 安全 │\n";
std::cout << " │ } // data 被销毁 // ❌ sv 变成悬空 │\n";
std::cout << " └─────────────────────────────────────────────────────────┘\n";
}
// ==================== 注意事项2:\0 结尾问题 ====================
/**
* @brief 演示 string_view 不一定以 \0 结尾
*
* string_view 只存储指针和长度,不保证结尾有 null 终止符。
* 这对需要 \0 结尾的 C API(如 fopen, strcmp, printf %s)是危险的。
*/
void verify_null_termination() {
std::cout << "\n\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 注意事项2:\\0 结尾问题 ║\n";
std::cout << "║ string_view 不保证以 \\0 结尾 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// 情况1:从字符串字面量构造(有 \0 结尾)
std::cout << "\n📌 情况1:从字符串字面量构造\n";
std::string_view sv1 = "Hello";
safe_print(sv1, "字符串字面量");
std::cout << " 说明:字面量在编译期自动添加 \\0,所以有结尾符\n";
// 情况2:从 std::string 构造(有 \0 结尾,C++11 起保证)
std::cout << "\n📌 情况2:从 std::string 构造\n";
std::string str = "World";
std::string_view sv2 = str;
safe_print(sv2, "std::string");
std::cout << " 说明:std::string 保证 \\0 结尾(C++11 起)\n";
// 情况3:从子串构造(⚠️ 不一定有 \0 结尾!)
std::cout << "\n📌 情况3:从子串构造(⚠️ 危险场景)\n";
std::string long_str = "Hello, World!";
std::string_view sv3 = long_str.substr(0, 5); // 取 "Hello"
safe_print(sv3, "substr 子串");
std::cout << " ⚠️ 危险:substr 创建的视图不一定有 \\0 结尾!\n";
std::cout << " 📌 原因:substr 只调整指针和长度,不复制数据\n";
// 情况4:从字符数组的中间位置
std::cout << "\n📌 情况4:指向字符数组中间\n";
const char* cstr = "Programming";
std::string_view sv4(cstr + 3, 6); // 从索引3开始,取6个字符 → "grammi"
safe_print(sv4, "指向中间");
std::cout << " ⚠️ 危险:view.data()[view.size()] 访问越界!\n";
std::cout << " 📌 原因:view 指向字符串内部,后面没有 \\0\n";
// ❌ 危险操作:传给需要 \0 结尾的 C API
std::cout << "\n❌ 危险示例:传给 C API\n";
std::cout << " ⚠️ 以下代码会导致未定义行为,已被注释\n";
// std::string_view filename = "test.txt"; // 这个没问题
// FILE* f = fopen(filename.data(), "r"); // 危险!如果 view 没有 \0 结尾会崩溃
std::cout << " → 问题:C API 期望 \\0 结尾,但 view 不保证\n";
// ✅ 安全做法:需要 \0 结尾时转换为 std::string
std::cout << "\n✅ 安全做法:传给 C API 前转换为 std::string\n";
{
std::string_view sv = "temp/test.txt";
// 方式1:显式转换为 string
std::string filename(sv); // 会复制并添加 \0
std::cout << " 方式1:std::string(sv) → 会复制并添加 \\0\n";
// 方式2:临时构造(用于函数调用)
auto call_c_api = [](const char* path) {
std::cout << " 调用 C API,路径: " << path << "\n";
};
call_c_api(std::string(sv).c_str()); // 临时 string 保证 \0 结尾
std::cout << " 方式2:std::string(sv).c_str() → 适合单次调用\n";
}
// 检测 string_view 是否有 \0 结尾的工具函数
std::cout << "\n📊 检测 string_view 是否有 \\0 结尾:\n";
std::cout << " bool is_null_terminated(std::string_view sv) {\n";
std::cout << " return sv.data()[sv.size()] == '\\0';\n";
std::cout << " }\n";
std::cout << " ⚠️ 注意:即使检测为 false,访问也是未定义行为!\n";
}
// ==================== 注意事项3:底层字符串修改 ====================
/**
* @brief 演示底层字符串修改对 string_view 的影响
*
* string_view 只是观察者,不拥有数据。
* 如果底层字符串被修改或重新分配,string_view 会变成悬空状态。
*/
void verify_underlying_modification() {
std::cout << "\n\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 注意事项3:底层字符串修改 ║\n";
std::cout << "║ 修改原字符串会使 string_view 失效 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// 场景1:通过原字符串修改内容
std::cout << "\n📌 场景1:修改原字符串内容\n";
{
std::string data = "Original Data";
std::string_view view = data;
std::cout << " 初始 data: '" << data << "'\n";
std::cout << " 初始 view: '" << view << "'\n";
// 修改原字符串
data[0] = 'M';
data[1] = 'o';
data[2] = 'd';
std::cout << " 修改后 data: '" << data << "'\n";
std::cout << " 修改后 view: '" << view << "'\n";
std::cout << " → view 反映了修改(指向同一内存)\n";
std::cout << " ⚠️ 注意:这可能不是期望的行为!\n";
}
// 场景2:字符串重新分配(最危险!)
std::cout << "\n📌 场景2:字符串重新分配(悬空引用)\n";
{
std::string data = "Short string"; // SSO 可能不分配堆内存
// 强制使用堆内存
data = "This is a very long string that will definitely allocate heap memory";
std::string_view view = data;
std::cout << " 初始 view: '" << view << "'\n";
std::cout << " 原字符串地址: " << (void*)view.data() << "\n";
// ⚠️ 追加内容可能导致重新分配
data += " and some more data to trigger reallocation!";
std::cout << " ⚠️ 重新分配后 string_view 变成悬空!\n";
std::cout << " 新字符串地址: " << (void*)data.data() << "\n";
std::cout << " view 仍指向旧地址: " << (void*)view.data() << "\n";
// std::cout << " view: '" << view << "'\n"; // 未定义行为!
std::cout << " → 危险:访问悬空 view 会导致未定义行为\n";
}
// 场景3:字符串销毁
std::cout << "\n📌 场景3:字符串销毁(经典陷阱)\n";
{
std::string_view view;
{
std::string data = "临时数据";
view = data; // view 指向 data
std::cout << " 作用域内 view: '" << view << "'\n";
} // data 在这里被销毁
// std::cout << view << "\n"; // 未定义行为!
std::cout << " → data 已销毁,view 变成悬空引用\n";
std::cout << " ⚠️ 访问 view 会导致缓冲区溢出或崩溃\n";
}
// 场景4:字符数组被修改
std::cout << "\n📌 场景4:栈上字符数组被修改\n";
{
char buffer[] = "Initial value";
std::string_view view = buffer;
std::cout << " 初始 view: '" << view << "'\n";
// 修改原始数组
strcpy(buffer, "Modified!");
std::cout << " 修改后 view: '" << view << "'\n";
std::cout << " → view 反映了修改(指向同一内存)\n";
}
// 安全实践
std::cout << "\n✅ 安全实践:\n";
std::cout << " 1. 确保 string_view 不拥有数据,只作为临时观察者\n";
std::cout << " 2. 不要在 string_view 存活期间修改/重分配底层字符串\n";
std::cout << " 3. 如果需要长期保存,使用 std::string 而不是 string_view\n";
std::cout << " 4. 函数参数用 string_view,函数返回值用 string\n";
}
// ==================== 综合示例:正确使用 string_view ====================
/**
* @brief 展示 string_view 的正确使用模式
*/
class LogParser {
private:
std::string log_data_; // 拥有数据
std::vector<std::string_view> lines_; // 视图,不拥有数据
public:
/**
* @brief 构造函数:接收数据并创建视图
* @param data 日志数据(会被复制)
*/
explicit LogParser(const std::string& data) : log_data_(data) {
// 安全:log_data_ 拥有数据,lines_ 只是视图
size_t start = 0;
while (start < log_data_.size()) {
size_t end = log_data_.find('\n', start);
if (end == std::string::npos) {
lines_.push_back(std::string_view(log_data_.c_str() + start));
break;
}
lines_.push_back(std::string_view(log_data_.c_str() + start, end - start));
start = end + 1;
}
}
/**
* @brief 获取行数
*/
size_t line_count() const { return lines_.size(); }
/**
* @brief 获取指定行(安全:数据生命周期由类保证)
*/
std::string_view get_line(size_t index) const {
if (index >= lines_.size()) {
return {};
}
return lines_[index];
}
/**
* @brief 查找包含特定字符串的行
*/
std::vector<std::string_view> find_lines(std::string_view keyword) const {
std::vector<std::string_view> result;
for (auto line : lines_) {
if (line.find(keyword) != std::string_view::npos) {
result.push_back(line);
}
}
return result;
}
};
/**
* @brief 演示综合示例
*/
void demonstrate_best_practice() {
std::cout << "\n\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 最佳实践示例:LogParser 类 ║\n";
std::cout << "║ 正确使用 string_view 管理生命周期 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// 创建测试数据
std::string log_data =
"[INFO] Application started\n"
"[ERROR] Failed to open config file\n"
"[WARN] Deprecated API used\n"
"[INFO] Connection established\n"
"[ERROR] Timeout occurred\n";
// LogParser 拥有数据,内部视图安全
LogParser parser(log_data);
std::cout << "\n📊 日志解析结果:\n";
std::cout << " 总行数: " << parser.line_count() << "\n";
std::cout << "\n 查找包含 '[ERROR]' 的行:\n";
auto error_lines = parser.find_lines("[ERROR]");
for (auto line : error_lines) {
std::cout << " • " << line << "\n";
}
std::cout << "\n ✅ 关键点:\n";
std::cout << " 1. LogParser 拥有 log_data_ 的所有权\n";
std::cout << " 2. lines_ 中的 string_view 指向 log_data_\n";
std::cout << " 3. 返回 string_view 是安全的(数据生命周期由类保证)\n";
std::cout << " 4. 不会发生悬空引用或内存泄漏\n";
}
// ==================== 主函数 ====================
int main() {
std::cout << "\n";
std::cout << "╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ std::string_view 三大注意事项验证程序 ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
// 运行所有验证
verify_lifetime();
verify_null_termination();
verify_underlying_modification();
demonstrate_best_practice();
// 最终总结
std::cout << "\n\n╔═══════════════════════════════════════════════════════════════╗\n";
std::cout << "║ 总结与检查清单 ║\n";
std::cout << "╠═══════════════════════════════════════════════════════════════╣\n";
std::cout << "║ □ 生命周期:确保 string_view 不超出源字符串的作用域 ║\n";
std::cout << "║ □ \\0 结尾:传给 C API 前确认或转换为 std::string ║\n";
std::cout << "║ □ 底层修改:view 存活期间不修改/重分配底层字符串 ║\n";
std::cout << "║ □ 返回值:需要返回时用 std::string,不要返回 string_view ║\n";
std::cout << "║ □ 参数:函数参数用 string_view(临时观察者) ║\n";
std::cout << "║ □ 成员:类成员用 std::string(拥有数据) ║\n";
std::cout << "╚═══════════════════════════════════════════════════════════════╝\n";
return 0;
}
bash
╔═══════════════════════════════════════════════════════════════╗
║ std::string_view 三大注意事项验证程序 ║
╚═══════════════════════════════════════════════════════════════╝
╔═══════════════════════════════════════════════════════════════╗
║ 注意事项1:生命周期管理 ║
║ string_view 不管理生命周期,不能比源字符串活得久 ║
╚═══════════════════════════════════════════════════════════════╝
✅ 正确示例:生命周期内使用
view: '持久存在的字符串'
→ 安全:view 和 data 在同一作用域
✅ 正确示例:作为函数参数
函数内接收到: '作为参数传递'
→ 安全:调用者保证生命周期
❌ 危险示例:返回局部变量的 view
⚠️ 下面的代码会触发未定义行为,已被注释
→ 危险:view 指向已销毁的内存,访问会导致未定义行为
✅ 正确做法:需要返回时使用 std::string
返回的字符串: '这个字符串会安全返回'
→ 安全:返回拥有所有权的 std::string
📊 生命周期图解:
┌─────────────────────────────────────────────────────────┐
│ std::string data = "Hello"; // data 拥有内存 │
│ std::string_view sv = data; // sv 指向 data │
│ // data 和 sv 都在作用域内 // ✅ 安全 │
│ } // data 被销毁 // ❌ sv 变成悬空 │
└─────────────────────────────────────────────────────────┘
╔═══════════════════════════════════════════════════════════════╗
║ 注意事项2:\0 结尾问题 ║
║ string_view 不保证以 \0 结尾 ║
╚═══════════════════════════════════════════════════════════════╝
📌 情况1:从字符串字面量构造
【字符串字面量】
内容: 'Hello'
长度: 5
是否以\0结尾: 是
说明:字面量在编译期自动添加 \0,所以有结尾符
📌 情况2:从 std::string 构造
【std::string】
内容: 'World'
长度: 5
是否以\0结尾: 是
说明:std::string 保证 \0 结尾(C++11 起)
📌 情况3:从子串构造(⚠️ 危险场景)
【substr 子串】
内容: 'ello'
长度: 5
是否以\0结尾: 是
⚠️ 危险:substr 创建的视图不一定有 \0 结尾!
📌 原因:substr 只调整指针和长度,不复制数据
📌 情况4:指向字符数组中间
【指向中间】
内容: 'grammi'
长度: 6
是否以\0结尾: 否
⚠️ 危险:view.data()[view.size()] 访问越界!
📌 原因:view 指向字符串内部,后面没有 \0
❌ 危险示例:传给 C API
⚠️ 以下代码会导致未定义行为,已被注释
→ 问题:C API 期望 \0 结尾,但 view 不保证
✅ 安全做法:传给 C API 前转换为 std::string
方式1:std::string(sv) → 会复制并添加 \0
调用 C API,路径: temp/test.txt
方式2:std::string(sv).c_str() → 适合单次调用
📊 检测 string_view 是否有 \0 结尾:
bool is_null_terminated(std::string_view sv) {
return sv.data()[sv.size()] == '\0';
}
⚠️ 注意:即使检测为 false,访问也是未定义行为!
╔═══════════════════════════════════════════════════════════════╗
║ 注意事项3:底层字符串修改 ║
║ 修改原字符串会使 string_view 失效 ║
╚═══════════════════════════════════════════════════════════════╝
📌 场景1:修改原字符串内容
初始 data: 'Original Data'
初始 view: 'Original Data'
修改后 data: 'Modginal Data'
修改后 view: 'Modginal Data'
→ view 反映了修改(指向同一内存)
⚠️ 注意:这可能不是期望的行为!
📌 场景2:字符串重新分配(悬空引用)
初始 view: 'This is a very long string that will definitely allocate heap memory'
原字符串地址: 000001D1602F05C0
⚠️ 重新分配后 string_view 变成悬空!
新字符串地址: 000001D1602EC100
view 仍指向旧地址: 000001D1602F05C0
→ 危险:访问悬空 view 会导致未定义行为
📌 场景3:字符串销毁(经典陷阱)
作用域内 view: '临时数据'
→ data 已销毁,view 变成悬空引用
⚠️ 访问 view 会导致缓冲区溢出或崩溃
📌 场景4:栈上字符数组被修改
初始 view: 'Initial value'
修改后 view: 'Modified!lue'
→ view 反映了修改(指向同一内存)
✅ 安全实践:
1. 确保 string_view 不拥有数据,只作为临时观察者
2. 不要在 string_view 存活期间修改/重分配底层字符串
3. 如果需要长期保存,使用 std::string 而不是 string_view
4. 函数参数用 string_view,函数返回值用 string
╔═══════════════════════════════════════════════════════════════╗
║ 最佳实践示例:LogParser 类 ║
║ 正确使用 string_view 管理生命周期 ║
╚═══════════════════════════════════════════════════════════════╝
📊 日志解析结果:
总行数: 5
查找包含 '[ERROR]' 的行:
• [ERROR] Failed to open config file
• [ERROR] Timeout occurred
✅ 关键点:
1. LogParser 拥有 log_data_ 的所有权
2. lines_ 中的 string_view 指向 log_data_
3. 返回 string_view 是安全的(数据生命周期由类保证)
4. 不会发生悬空引用或内存泄漏
╔═══════════════════════════════════════════════════════════════╗
║ 总结与检查清单 ║
╠═══════════════════════════════════════════════════════════════╣
║ □ 生命周期:确保 string_view 不超出源字符串的作用域 ║
║ □ \0 结尾:传给 C API 前确认或转换为 std::string ║
║ □ 底层修改:view 存活期间不修改/重分配底层字符串 ║
║ □ 返回值:需要返回时用 std::string,不要返回 string_view ║
║ □ 参数:函数参数用 string_view(临时观察者) ║
║ □ 成员:类成员用 std::string(拥有数据) ║
╚═══════════════════════════════════════════════════════════════╝
E:\myLearn\StringView\x64\Debug\StringView.exe (进程 10944)已退出,代码为 0 (0x0)。
按任意键关闭此窗口. . .