跳到主要内容

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

cpp
#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 类似的操作:

cpp
#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_prefixremove_suffix 方法,可以调整视图范围而不需要分配新内存:

cpp
#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 的最大优势是避免了不必要的内存分配和复制操作,特别是在函数传参时。

性能对比示例

cpp
#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. 函数参数

最常见的用途是作为函数参数,特别是只需要读取不需要修改字符串的情况:

cpp
#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 可以有效避免创建多个子字符串的开销:

cpp
#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 在实际应用中的优势:

cpp
#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 仍在使用,将导致未定义行为。

cpp
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 创建,但反之则需要创建副本:

cpp
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 风格字符串的函数。

cpp
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 引入的重要特性,提供了一种高效、安全的方式来引用字符串数据,而无需进行复制。它特别适用于:

  • 需要处理大量字符串但不需要修改它们的场景
  • 函数参数,特别是只读字符串参数
  • 字符串解析和提取子字符串的操作

主要优势:

  1. 性能高效,避免不必要的内存分配和复制
  2. 方便的字符串操作 API
  3. 可以与各种字符串源无缝协作

使用时需要注意:

  1. 生命周期问题 - 确保原始字符串数据比 string_view 存在更长时间
  2. 无法保证 null 终止 - 不能直接用于期望 C 风格字符串的 API
  3. 只读访问 - 无法通过 string_view 修改原始字符串

练习

  1. 编写一个函数,使用 string_view 检查一个字符串是否是回文(正着读和倒着读相同)。

  2. 实现一个简单的 URL 解析器,使用 string_view 提取 URL 的各个组成部分(协议、主机名、路径等)。

  3. 修改上面的 split 函数,使其能够处理多个连续分隔符,例如将 "apple,,banana,cherry" 正确地分割为 ["apple", "", "banana", "cherry"]。

附加资源

通过掌握 std::string_view,你可以显著提升 C++ 程序的性能和可读性,尤其是在处理字符串数据时。这是 C++17 带来的最实用的特性之一,值得每个 C++ 程序员学习和使用。