WEBKT

C++ 程序员必看:std::string_view 的实战指南,优化你的代码!

18 0 0 0

1. 为什么需要 std::string_view?

1.1 const char* 的问题

1.2 std::string 的问题

1.3 std::string_view 的救星

2. std::string_view 的基本用法

3. 实战案例:std::string_view 的应用

3.1 解析配置文件

3.2 处理网络数据包

3.3 实现字符串搜索算法

4. std::string_view 的注意事项

4.1 生命周期管理

4.2 修改字符串的风险

4.3 隐式转换的陷阱

4.4 字符串字面量的处理

5. std::string_view 与其他字符串类型的比较

6. 如何更好地使用 std::string_view

6.1 优先使用 std::string_view 作为函数参数

6.2 谨慎使用 std::string_view 作为函数返回值

6.3 在需要修改字符串时,转换为 std::string

6.4 结合其他 C++ 特性使用

7. 总结

嘿,C++ 程序员们!👋

在日常的 C++ 开发中,字符串处理绝对是绕不开的话题。你是不是还在用 const char*std::string? 它们虽然好用,但有时候会遇到一些性能和内存上的小麻烦。今天,咱们就来聊聊 C++17 引入的 std::string_view,看看它如何以一种更优雅、更高效的方式处理字符串,让你写出更牛的代码!

1. 为什么需要 std::string_view

在深入实战之前,我们先来简单回顾一下 const char*std::string 的局限性,这样才能更好地理解 std::string_view 的优势。

1.1 const char* 的问题

  • 安全问题: const char* 只是一个指向字符的指针,你得自己负责内存管理,一不小心就可能出现野指针、内存泄漏等问题,让你掉头发。
  • 操作不便: 很多字符串操作函数都需要自己手动实现,或者使用 C 标准库的函数,不够方便。
  • 无法记录长度: 除非字符串是以 '\0' 结尾,否则你得额外传递字符串的长度,增加了出错的风险。

1.2 std::string 的问题

  • 拷贝开销: std::string 是一个类,当你传递或返回字符串时,可能会发生深拷贝,这在性能敏感的场景下是不可接受的。
  • 内存占用: std::string 需要维护字符串的内存,即使你只是想“看看”字符串的一部分,它也得先拷贝一份。这对于大型字符串来说,会占用额外的内存。

1.3 std::string_view 的救星

std::string_view 闪亮登场!它就像一个“只读的字符串观察者”,它不拥有字符串的内存,仅仅“观察”字符串。这意味着:

  • 零拷贝: 创建 std::string_view 时,不会发生字符串的拷贝,速度飞快!
  • 轻量级: 它只包含指向字符串起始位置的指针和字符串的长度,内存占用非常小。
  • 易于使用: 提供了丰富的字符串操作函数,用起来和 std::string 差不多。

2. std::string_view 的基本用法

std::string_view 位于 <string_view> 头文件中。下面是一些基本用法:

#include <iostream>
#include <string_view>
#include <string>
int main() {
std::string str = "Hello, world!";
// 从 std::string 创建 string_view
std::string_view sv1(str);
std::string_view sv2(str.c_str(), str.length());
// 从 const char* 创建 string_view
const char* cstr = "Hello, string_view!";
std::string_view sv3(cstr);
std::string_view sv4(cstr, 5); // 指定长度
// 使用 string_view
std::cout << sv1 << std::endl; // 输出: Hello, world!
std::cout << sv3 << std::endl; // 输出: Hello, string_view!
std::cout << sv4 << std::endl; // 输出: Hello
// 获取长度
std::cout << "sv1 的长度: " << sv1.length() << std::endl; // 输出: sv1 的长度: 13
// 获取子串
std::string_view sub_sv = sv1.substr(0, 5);
std::cout << "子串: " << sub_sv << std::endl; // 输出: 子串: Hello
return 0;
}

关键点:

  • std::string_view 并不拥有字符串的内存,它只是“借用”字符串的内存。
  • std::string_view 的生命周期要短于它所引用的字符串的生命周期,否则就会出现“悬挂”的 string_view,导致未定义的行为。

3. 实战案例:std::string_view 的应用

现在,让我们通过几个实际的案例,看看 std::string_view 怎么优化你的代码。

3.1 解析配置文件

假设你有一个配置文件,格式如下:

# This is a comment
key1 = value1
key2 = value2

你需要解析这个文件,提取 key 和 value。使用 std::string_view,可以避免不必要的字符串拷贝,提高解析效率。

#include <iostream>
#include <fstream>
#include <string>
#include <string_view>
#include <sstream>
#include <unordered_map>
std::unordered_map<std::string_view, std::string_view> parse_config(const std::string& filename) {
std::unordered_map<std::string_view, std::string_view> config;
std::ifstream file(filename);
std::string line;
while (std::getline(file, line)) {
// 忽略注释和空行
if (line.empty() || line[0] == '#') {
continue;
}
// 查找 '=' 分隔符
size_t pos = line.find('=');
if (pos == std::string::npos) {
continue; // 忽略无效行
}
// 使用 string_view 提取 key 和 value
std::string_view key(line.data(), pos); // 使用data和长度构建string_view,不拷贝字符串
std::string_view value(line.data() + pos + 1, line.length() - pos - 1); // 获取 value
// 去除 key 和 value 前后的空格
size_t key_start = key.find_first_not_of(' ');
if (key_start != std::string_view::npos) {
key = key.substr(key_start);
}
size_t key_end = key.find_last_not_of(' ');
if (key_end != std::string_view::npos) {
key = key.substr(0, key_end + 1);
}
size_t value_start = value.find_first_not_of(' ');
if (value_start != std::string_view::npos) {
value = value.substr(value_start);
}
size_t value_end = value.find_last_not_of(' ');
if (value_end != std::string_view::npos) {
value = value.substr(0, value_end + 1);
}
config[key] = value;
}
return config;
}
int main() {
// 创建一个测试配置文件
std::ofstream outfile("config.txt");
outfile << "# This is a comment\n";
outfile << "key1 = value1 \n";
outfile << " key2 = value2 \n";
outfile.close();
std::unordered_map<std::string_view, std::string_view> config = parse_config("config.txt");
// 打印解析结果
for (const auto& pair : config) {
std::cout << "Key: \"" << pair.first << "\", Value: \"" << pair.second << "\"" << std::endl;
}
return 0;
}

代码解析:

  1. parse_config 函数接收一个文件名,并返回一个 std::unordered_map,用于存储配置项。keyvalue 的类型都是 std::string_view,避免了字符串的拷贝。
  2. 使用 std::getline 逐行读取配置文件。
  3. 使用 find 查找 = 分隔符,然后使用 string_view 的构造函数提取 keyvalue。我们使用了 line.data(),这是一种安全的方式,因为line的生命周期是安全的。
  4. 使用 substr 去除 keyvalue 前后的空格。substr 同样返回 string_view,避免了拷贝。

对比:

如果不用 std::string_view,你可能需要使用 std::string 来存储 keyvalue,并在提取子串时进行拷贝。std::string_view 避免了这些拷贝操作,提高了性能。

3.2 处理网络数据包

在网络编程中,经常需要处理各种格式的数据包。std::string_view 可以方便地从接收到的字节流中提取数据,而无需进行拷贝。

#include <iostream>
#include <string>
#include <string_view>
#include <vector>
// 模拟网络数据包
struct Packet {
std::vector<char> data;
Packet(const std::string& str) : data(str.begin(), str.end()) {}
};
// 解析数据包中的字段
struct ParsedPacket {
std::string_view header;
std::string_view payload;
};
ParsedPacket parse_packet(const Packet& packet) {
// 假设数据包格式: header | payload
// header 的长度固定为 8 字节
if (packet.data.size() < 8) {
return {"", ""}; // 错误处理
}
std::string_view data_view(packet.data.data(), packet.data.size());
return {data_view.substr(0, 8), data_view.substr(8)};
}
int main() {
// 创建一个模拟数据包
Packet packet("HEADER12PAYLOADDATA");
// 解析数据包
ParsedPacket parsed_packet = parse_packet(packet);
// 打印解析结果
std::cout << "Header: " << parsed_packet.header << std::endl;
std::cout << "Payload: " << parsed_packet.payload << std::endl;
return 0;
}

代码解析:

  1. Packet 结构体模拟了一个网络数据包,使用 std::vector<char> 存储数据。
  2. parse_packet 函数接收一个 Packet,并返回一个 ParsedPacket,包含 headerpayload。同样,使用 string_view 来避免拷贝。
  3. data_view 通过packet.data.data() 和大小构建。我们使用了 substr 来提取 headerpayload

对比:

如果没有 std::string_view,你可能需要将 packet.data 拷贝到 std::string 中,再进行子串提取。std::string_view 直接“观察” packet.data,避免了拷贝,提高了效率。

3.3 实现字符串搜索算法

std::string_view 也可以用于实现高效的字符串搜索算法,例如 KMP 算法、BM 算法等。这里,我们以简单的子串查找为例:

#include <iostream>
#include <string>
#include <string_view>
size_t find_substring(std::string_view text, std::string_view pattern) {
return text.find(pattern);
}
int main() {
std::string text = "This is a test string.";
std::string pattern = "test";
// 使用 string_view 进行查找
size_t pos = find_substring(text, pattern);
if (pos != std::string::npos) {
std::cout << "子串在位置: " << pos << std::endl;
} else {
std::cout << "未找到子串" << std::endl;
}
return 0;
}

代码解析:

  1. find_substring 函数接收两个 std::string_view,分别表示文本和模式串。
  2. 使用 text.find(pattern) 进行子串查找。find 函数接受 string_view 作为参数,并返回子串的起始位置。

对比:

使用 std::string_view,你无需拷贝文本和模式串,直接进行查找。这在处理大型文本时,可以显著提高性能。

4. std::string_view 的注意事项

虽然 std::string_view 很好用,但也要注意一些事项,避免掉进坑里:

4.1 生命周期管理

这是 std::string_view 最重要的注意事项。由于 std::string_view 只是“观察”字符串,它不拥有字符串的内存。因此,你需要确保 std::string_view 的生命周期短于它所引用的字符串的生命周期。

#include <iostream>
#include <string>
#include <string_view>
std::string_view get_string_view() {
std::string str = "Hello";
return str; // 错误:str 在函数返回后被销毁,string_view 变成了悬挂指针
}
int main() {
std::string_view sv = get_string_view(); // 危险!
std::cout << sv << std::endl; // 未定义的行为
return 0;
}

正确做法:

  • 使用 const 引用: 如果你需要返回一个 string_view,并且它指向的字符串是函数参数或类的成员变量,可以使用 const 引用。

    #include <iostream>
    #include <string>
    #include <string_view>
    std::string_view get_string_view(const std::string& str) {
    return str;
    }
    int main() {
    std::string str = "Hello";
    std::string_view sv = get_string_view(str);
    std::cout << sv << std::endl; // 正常
    return 0;
    }
  • 确保数据在 string_view 的生命周期内有效: 确保 string_view 指向的字符串在 string_view 的生命周期内是有效的。

    #include <iostream>
    #include <string>
    #include <string_view>
    int main() {
    std::string str = "Hello";
    {
    std::string_view sv(str);
    std::cout << sv << std::endl;
    }
    // sv 在这里已经无效,但 str 仍然有效
    std::cout << str << std::endl; // 正常
    return 0;
    }

4.2 修改字符串的风险

std::string_view 本身是只读的,你不能通过它修改字符串的内容。如果你需要修改字符串,需要使用 std::string 或其他可修改的字符串类型。

#include <iostream>
#include <string>
#include <string_view>
int main() {
std::string str = "Hello";
std::string_view sv(str);
// sv[0] = 'h'; // 编译错误:string_view 不支持修改
str[0] = 'h'; // 可以通过 str 修改
std::cout << sv << std::endl; // 输出: hello
return 0;
}

4.3 隐式转换的陷阱

std::string_view 可以隐式地从 std::stringconst char* 转换而来。虽然方便,但也可能导致一些意想不到的问题。

#include <iostream>
#include <string>
#include <string_view>
void process(std::string_view sv) {
std::cout << "处理: " << sv << std::endl;
}
int main() {
const char* cstr = "World";
process(cstr); // 隐式转换为 string_view,没问题
std::string str = "Hello";
process(str); // 隐式转换为 string_view,也没问题
return 0;
}

虽然隐式转换很方便,但如果函数签名不明确,或者你需要在函数内部使用 std::string 的特性(例如修改字符串),那么隐式转换可能会导致混淆。为了避免混淆,建议在函数参数中使用 std::string_view,并在函数内部根据需要转换为 std::string

4.4 字符串字面量的处理

对于字符串字面量,std::string_view 也是一个很好的选择。

#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "Hello, string_view!";
std::cout << sv << std::endl;
return 0;
}

这里,"Hello, string_view!" 是一个字符串字面量,它被直接用于初始化 std::string_view。这避免了创建临时的 std::string 对象,提高了效率。

5. std::string_view 与其他字符串类型的比较

为了更好地理解 std::string_view 的优势,我们来对比一下它与其他字符串类型的特点。

特性 const char* std::string std::string_view
内存管理 手动管理 自动管理 不管理
拷贝 无拷贝 深拷贝 零拷贝
长度 需要额外传递或以 '\0' 结尾 自动维护 自动维护
修改 不可修改 (const) 可修改 不可修改 (只读)
适用场景 简单字符串,性能敏感 复杂字符串操作,需要修改 只需要“观察”字符串,性能敏感
易用性 较低 较高 较高
内存占用 较低 较高 极低

总结:

  • const char*: 简单、高效,但容易出错,需要手动管理内存。
  • std::string: 安全、易用,但有拷贝开销,内存占用较高。
  • std::string_view: 零拷贝、轻量级,适用于只需要“观察”字符串的场景,可以显著提高性能和减少内存占用。

6. 如何更好地使用 std::string_view

为了最大限度地发挥 std::string_view 的优势,你可以遵循以下几点建议:

6.1 优先使用 std::string_view 作为函数参数

在函数参数中使用 std::string_view,可以避免不必要的拷贝,提高程序的性能。即使你传递的是 std::stringconst char*,编译器也会自动进行隐式转换。

#include <iostream>
#include <string>
#include <string_view>
void print_string(std::string_view str) {
std::cout << str << std::endl;
}
int main() {
std::string str = "Hello";
const char* cstr = "World";
print_string(str); // std::string -> std::string_view
print_string(cstr); // const char* -> std::string_view
return 0;
}

6.2 谨慎使用 std::string_view 作为函数返回值

如前所述,std::string_view 的生命周期必须短于它所引用的字符串的生命周期。因此,不要直接返回一个指向函数内部局部字符串的 string_view

#include <string>
#include <string_view>
std::string_view get_substring(const std::string& str) {
// 错误:返回了指向局部变量的 string_view
// 更好的方式是使用 const std::string& 作为参数
std::string sub = str.substr(0, 5);
return sub; // 危险!sub 在函数返回后被销毁
}

6.3 在需要修改字符串时,转换为 std::string

std::string_view 是只读的,如果你需要修改字符串,需要将其转换为 std::string

#include <iostream>
#include <string>
#include <string_view>
std::string to_upper(std::string_view str) {
std::string result(str);
for (char& c : result) {
c = toupper(c);
}
return result;
}
int main() {
std::string_view sv = "hello";
std::string upper_str = to_upper(sv);
std::cout << upper_str << std::endl; // 输出: HELLO
return 0;
}

6.4 结合其他 C++ 特性使用

std::string_view 可以很好地与其他 C++ 特性结合使用,例如:

  • 范围 for 循环: 方便地遍历字符串中的字符。

    #include <iostream>
    #include <string_view>
    int main() {
    std::string_view sv = "Hello";
    for (char c : sv) {
    std::cout << c << " ";
    }
    std::cout << std::endl;
    return 0;
    }
  • 算法库: 可以使用 <algorithm> 库中的算法对 string_view 进行操作。

    #include <iostream>
    #include <string_view>
    #include <algorithm>
    int main() {
    std::string_view sv = "Hello";
    if (std::any_of(sv.begin(), sv.end(), [](char c) { return isdigit(c); })) {
    std::cout << "包含数字" << std::endl;
    } else {
    std::cout << "不包含数字" << std::endl;
    }
    return 0;
    }

7. 总结

std::string_view 是一个非常有用的 C++ 特性,它可以让你在处理字符串时,既保证性能,又简化代码。记住以下几点:

  • 零拷贝,轻量级: 避免了不必要的拷贝,减少了内存占用。
  • 生命周期管理: 确保 string_view 的生命周期短于它所引用的字符串的生命周期。
  • 只读性: string_view 本身是只读的,修改需要转换为 std::string
  • 广泛应用: 可以用于解析配置文件、处理网络数据包、实现字符串搜索算法等。
  • 最佳实践: 优先使用 std::string_view 作为函数参数,谨慎作为返回值。

希望这篇文章能帮助你更好地理解和使用 std::string_view。赶紧在你的项目中尝试一下吧,相信你会爱上它的! 💪

如果你有任何问题或建议,欢迎在评论区留言! 💬 让我们一起学习,一起进步! 🚀

技术派老王 C++string_view性能优化字符串处理

评论点评

打赏赞助
sponsor

感谢您的支持让我们更好的前行

分享

QRcode

https://www.webkt.com/article/7561