C++ 17 std::string_view
什么是 std::string_view?
std::string_view
是 C++17 引入的一个新的标准库类型,位于 <string_view>
头文件中。它提供了一个字符串的只读视图,可以引用一个已存在的字符串或字符数组的一部分,而无需进行内存分配或复制操作。
简单来说,string_view
可以看作是对一段字符序列的引用,包含一个指向字符数据的指针和一个长度,不拥有这些数据。它特别适合需要引用字符串但不需要修改它的场景。
为什么需要 std::string_view?
在 C++17 之前,处理字符串时我们有这些选择:
- C 风格字符串 (
const char*
) std::string
对象
这些方式各有问题:
- 使用
const char*
不能方便地获取字符串长度,且容易出现安全问题 - 使用
std::string
在传递或复制时会分配新内存,带来性能开销
而 std::string_view
则完美地解决了这些问题:
- 不拥有数据,不会分配内存
- 可以访问字符串长度和内容
- 适用于任何连续字符序列
基本用法
创建 string_view
#include <iostream>
#include <string_view>
#include <string>
int main() {
// 从字符串字面量创建
std::string_view sv1 = "Hello, world!";
// 从 std::string 创建
std::string str = "Hello, C++17!";
std::string_view sv2 = str;
// 从字符数组创建
char arr[] = {'H', 'e', 'l', 'l', 'o', '\0'};
std::string_view sv3(arr, 5); // 注意指定长度,不依赖于 null 终止符
std::cout << "sv1: " << sv1 << std::endl;
std::cout << "sv2: " << sv2 << std::endl;
std::cout << "sv3: " << sv3 << std::endl;
return 0;
}
输出:
sv1: Hello, world!
sv2: Hello, C++17!
sv3: Hello
常用操作
string_view
提供了许多与 std::string
类似的操作:
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "Hello, world!";
// 获取长度
std::cout << "Length: " << sv.length() << std::endl;
std::cout << "Size: " << sv.size() << std::endl;
// 访问字符
std::cout << "First character: " << sv[0] << std::endl;
std::cout << "Last character: " << sv[sv.length() - 1] << std::endl;
// 子视图
std::string_view hello = sv.substr(0, 5);
std::cout << "Substring: " << hello << std::endl;
// 检查是否为空
std::cout << "Is empty: " << (sv.empty() ? "yes" : "no") << std::endl;
return 0;
}
输出:
Length: 13
Size: 13
First character: H
Last character: !
Substring: Hello
Is empty: no
移除前后字符
string_view
提供了非常有用的 remove_prefix
和 remove_suffix
方法,可以调整视图范围而不需要分配新内存:
#include <iostream>
#include <string_view>
int main() {
std::string_view sv = "Hello, world!";
std::cout << "Original: " << sv << std::endl;
// 移除前5个字符
sv.remove_prefix(7);
std::cout << "After remove_prefix(7): " << sv << std::endl;
// 移除后1个字符
sv.remove_suffix(1);
std::cout << "After remove_suffix(1): " << sv << std::endl;
return 0;
}
输出:
Original: Hello, world!
After remove_prefix(7): world!
After remove_suffix(1): world
性能优势
string_view
相比 std::string
的最大优势是避免了不必要的内存分配和复制操作,特别是在函数传参时。
性能对比示例
#include <iostream>
#include <string>
#include <string_view>
#include <chrono>
// 使用 string 参数
void processString(const std::string& str) {
// 假设做一些处理,但不修改字符串
volatile char c = str[0]; // 防止编译器优化
}
// 使用 string_view 参数
void processStringView(std::string_view sv) {
// 做同样的处理
volatile char c = sv[0]; // 防止编译器优化
}
int main() {
const int iterations = 10000000;
const char* cstr = "Hello, this is a test string for performance comparison";
// 测试 std::string
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::string s(cstr);
processString(s);
}
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) {
std::string_view sv(cstr);
processStringView(sv);
}
auto end2 = std::chrono::high_resolution_clock::now();
auto duration1 = std::chrono::duration_cast<std::chrono::milliseconds>(end1 - start1);
auto duration2 = std::chrono::duration_cast<std::chrono::milliseconds>(end2 - start2);
std::cout << "Time with std::string: " << duration1.count() << " ms\n";
std::cout << "Time with std::string_view: " << duration2.count() << " ms\n";
return 0;
}
在大多数系统上,string_view
版本的运行速度会快得多,特别是在处理大字符串或进行大量操作时。
实际应用场景
1. 函数参数
最常见的用途是作为函数参数,特别是只需要读取不需要修改字符串的情况:
#include <iostream>
#include <string_view>
#include <algorithm>
// 计算字符串中某个字符出现的次数
int countOccurrences(std::string_view text, char c) {
return std::count(text.begin(), text.end(), c);
}
int main() {
const char* cstr = "Hello, world!";
std::string str = "Hello, C++17!";
std::cout << "Occurrences of 'l' in \"Hello, world!\": "
<< countOccurrences(cstr, 'l') << std::endl;
std::cout << "Occurrences of 'l' in \"Hello, C++17!\": "
<< countOccurrences(str, 'l') << std::endl;
return 0;
}
输出:
Occurrences of 'l' in "Hello, world!": 3
Occurrences of 'l' in "Hello, C++17!": 2
2. 字符串解析
当需要分析或解析字符串时,string_view
可以有效避免创建多个子字符串的开销:
#include <iostream>
#include <string_view>
#include <vector>
std::vector<std::string_view> split(std::string_view str, char delimiter) {
std::vector<std::string_view> result;
size_t start = 0;
size_t end = str.find(delimiter);
while (end != std::string_view::npos) {
result.push_back(str.substr(start, end - start));
start = end + 1;
end = str.find(delimiter, start);
}
// 添加最后一个部分
result.push_back(str.substr(start));
return result;
}
int main() {
std::string_view csv = "apple,banana,cherry,date,elderberry";
auto fruits = split(csv, ',');
std::cout << "Found " << fruits.size() << " fruits:" << std::endl;
for (const auto& fruit : fruits) {
std::cout << "- " << fruit << std::endl;
}
return 0;
}
输出:
Found 5 fruits:
- apple
- banana
- cherry
- date
- elderberry
3. 配置解析器示例
下面是一个简单的配置文件解析器,展示了 string_view
在实际应用中的优势:
#include <iostream>
#include <string_view>
#include <string>
#include <unordered_map>
#include <fstream>
#include <sstream>
class ConfigParser {
public:
bool loadFromString(std::string_view config) {
std::istringstream stream(std::string(config));
std::string line;
while (std::getline(stream, line)) {
std::string_view line_view = line;
// 去除前后空格
line_view = trimView(line_view);
// 跳过空行和注释
if (line_view.empty() || line_view[0] == '#') {
continue;
}
// 查找键值分隔符
auto pos = line_view.find('=');
if (pos == std::string_view::npos) {
continue; // 无效行
}
// 提取键和值
std::string_view key = trimView(line_view.substr(0, pos));
std::string_view value = trimView(line_view.substr(pos + 1));
// 存储到映射表中 (这里需要转换为 std::string)
m_config[std::string(key)] = std::string(value);
}
return true;
}
std::string getValue(const std::string& key, const std::string& defaultValue = "") const {
auto it = m_config.find(key);
if (it != m_config.end()) {
return it->second;
}
return defaultValue;
}
private:
std::string_view trimView(std::string_view sv) {
// 移除前导空格
auto start = sv.find_first_not_of(" \t\r\n");
if (start == std::string_view::npos) {
return std::string_view();
}
// 移除尾部空格
auto end = sv.find_last_not_of(" \t\r\n");
return sv.substr(start, end - start + 1);
}
std::unordered_map<std::string, std::string> m_config;
};
int main() {
const char* config_text = R"(
# 这是一个配置文件示例
server_name = MyServer
port = 8080
max_connections = 1000
debug_mode = true
)";
ConfigParser parser;
parser.loadFromString(config_text);
std::cout << "Server name: " << parser.getValue("server_name") << std::endl;
std::cout << "Port: " << parser.getValue("port") << std::endl;
std::cout << "Max connections: " << parser.getValue("max_connections") << std::endl;
std::cout << "Debug mode: " << parser.getValue("debug_mode") << std::endl;
std::cout << "Timeout: " << parser.getValue("timeout", "30") << std::endl;
return 0;
}
输出:
Server name: MyServer
Port: 8080
Max connections: 1000
Debug mode: true
Timeout: 30
注意事项和陷阱
虽然 string_view
很强大,但使用时有一些需要注意的问题:
1. 生命周期问题
string_view
不拥有字符串数据,只是引用它。如果原始字符串被销毁,而 string_view
仍在使用,将导致未定义行为。
std::string_view getDangerousView() {
std::string local = "This is dangerous!";
return local; // 危险!返回的是临时变量的视图
} // 函数结束,local 销毁,但视图仍然存在
// 正确的做法
std::string getSafeString() {
std::string result = "This is safe!";
return result; // 返回字符串的副本
}
std::string_view getViewFromParam(const std::string& str) {
return str; // 安全,只要调用者确保 str 的生命周期足够长
}
2. 与 std::string 的转换
string_view
可以从 std::string
创建,但反之则需要创建副本:
std::string str = "Hello";
std::string_view sv = str; // 简单高效
// 从 string_view 创建 string 会创建副本
std::string new_str(sv); // 或者 std::string new_str = std::string(sv);
3. 不能保证 null 终止
string_view
不保证字符串以 null 字符结尾,因此不能安全地传递给期望 C 风格字符串的函数。
std::string_view sv = "Hello";
sv.remove_suffix(1); // 现在 sv 是 "Hell"
// 错误用法
printf("%s\n", sv.data()); // 危险!data() 可能不是以 null 结尾的
// 正确做法
std::string str(sv);
printf("%s\n", str.c_str());
总结
std::string_view
是 C++17 引入的重要特性,提供了一种高效、安全的方式来引用字符串数据,而无需进行复制。它特别适用于:
- 需要处理大量字符串但不需要修改它们的场景
- 函数参数,特别是只读字符串参数
- 字符串解析和提取子字符串的操作
主要优势:
- 性能高效,避免不必要的内存分配和复制
- 方便的字符串操作 API
- 可以与各种字符串源无缝协作
使用时需要注意:
- 生命周期问题 - 确保原始字符串数据比
string_view
存在更长时间 - 无法保证 null 终止 - 不能直接用于期望 C 风格字符串的 API
- 只读访问 - 无法通过
string_view
修改原始字符串
练习
-
编写一个函数,使用
string_view
检查一个字符串是否是回文(正着读和倒着读相同)。 -
实现一个简单的 URL 解析器,使用
string_view
提取 URL 的各个组成部分(协议、主机名、路径等)。 -
修改上面的
split
函数,使其能够处理多个连续分隔符,例如将 "apple,,banana,cherry" 正确地分割为 ["apple", "", "banana", "cherry"]。
附加资源
- C++ 参考文档: std::string_view
- C++17 标准: ISO/IEC 14882:2017
通过掌握 std::string_view
,你可以显著提升 C++ 程序的性能和可读性,尤其是在处理字符串数据时。这是 C++17 带来的最实用的特性之一,值得每个 C++ 程序员学习和使用。