跳到主要内容

C++ 字符串视图

引言

在C++编程中,字符串处理是一项常见操作。传统上,我们使用std::string或C风格字符串(字符数组)来处理文本数据。然而,这些方法在某些场景下会带来性能问题,尤其是当我们只需要临时访问字符串而不需要修改时。为解决这一问题,C++17引入了std::string_view类型。

std::string_view提供了一种轻量级、非拥有型的字符串引用,它可以查看(但不能修改)现有字符序列,而不需要进行内存分配或复制操作。

基本概念

什么是字符串视图?

字符串视图(std::string_view)本质上是一个轻量级对象,只包含两个成员变量:

  • 指向字符数据的指针
  • 字符序列的长度

它不拥有所指向的字符数据,只是"借用"或者说"查看"现有的字符串数据。

备注

std::string_view定义在<string_view>头文件中,并位于std命名空间下。

字符串视图 vs. 字符串

特性std::string_viewstd::string
拥有内存
可修改
内存开销低(通常只有两个指针大小)较高
性能传递和复制代价低传递和复制可能产生内存分配
生命周期管理依赖于原始字符串自我管理

使用字符串视图

创建字符串视图

cpp
#include <iostream>
#include <string>
#include <string_view>

int main() {
// 从字面量创建
std::string_view sv1 = "Hello, World!";

// 从std::string创建
std::string str = "C++ Programming";
std::string_view sv2 = str;

// 从字符数组创建
char arr[] = "Array String";
std::string_view sv3(arr, 5); // 只查看前5个字符:"Array"

std::cout << "sv1: " << sv1 << std::endl;
std::cout << "sv2: " << sv2 << std::endl;
std::cout << "sv3: " << sv3 << std::endl;

return 0;
}

输出:

sv1: Hello, World!
sv2: C++ Programming
sv3: Array

基本操作

std::string_view提供了许多与std::string类似的操作:

cpp
#include <iostream>
#include <string_view>

int main() {
std::string_view sv = "Hello, C++ Programming!";

// 获取长度
std::cout << "长度: " << sv.length() << std::endl;
std::cout << "大小: " << sv.size() << std::endl;

// 访问单个字符
std::cout << "第一个字符: " << sv[0] << std::endl;
std::cout << "最后一个字符: " << sv[sv.length() - 1] << std::endl;

// 子字符串
std::cout << "子串(7, 3): " << sv.substr(7, 3) << std::endl;

// 查找
std::cout << "找到'C++': " << sv.find("C++") << std::endl;

// 检查前缀和后缀
std::cout << "以'Hello'开头: " << std::boolalpha << sv.starts_with("Hello") << std::endl;
std::cout << "以'ing!'结尾: " << sv.ends_with("ing!") << std::endl;

return 0;
}

输出:

长度: 23
大小: 23
第一个字符: H
最后一个字符: !
子串(7, 3): C++
找到'C++': 7
以'Hello'开头: true
以'ing!'结尾: true
警告

在C++20之前,starts_with()ends_with()方法不可用。如果你使用的是C++17,你需要自己实现类似功能。

修改视图(不修改原始数据)

std::string_view允许你"修改"视图本身(而不是底层数据),调整它所查看的范围:

cpp
#include <iostream>
#include <string_view>

int main() {
std::string_view sv = "Hello, C++ Programming!";

std::cout << "原始视图: " << sv << std::endl;

// 移除前缀
sv.remove_prefix(7); // 移除 "Hello, "
std::cout << "移除前缀后: " << sv << std::endl;

// 移除后缀
sv.remove_suffix(1); // 移除 "!"
std::cout << "移除后缀后: " << sv << std::endl;

return 0;
}

输出:

原始视图: Hello, C++ Programming!
移除前缀后: C++ Programming!
移除后缀后: C++ Programming

字符串视图的优势

性能优势

最大的优势是避免了不必要的内存分配和复制,特别是在函数参数传递时:

cpp
#include <iostream>
#include <string>
#include <string_view>
#include <chrono>

// 使用std::string作为参数
void processString(const std::string& str) {
// 模拟处理操作
volatile char c = str[0];
}

// 使用std::string_view作为参数
void processStringView(std::string_view sv) {
// 完全相同的处理操作
volatile char c = sv[0];
}

int main() {
const int iterations = 10000000;
const char* text = "这是一个长字符串,用于测试std::string和std::string_view的性能差异";

// 测试std::string
auto start1 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::string s = text;
processString(s);
}
auto end1 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> ms1 = end1 - start1;

// 测试std::string_view
auto start2 = std::chrono::high_resolution_clock::now();
for (int i = 0; i < iterations; ++i) {
std::string_view sv = text;
processStringView(sv);
}
auto end2 = std::chrono::high_resolution_clock::now();
std::chrono::duration<double, std::milli> ms2 = end2 - start2;

std::cout << "使用std::string耗时: " << ms1.count() << " ms\n";
std::cout << "使用std::string_view耗时: " << ms2.count() << " ms\n";
std::cout << "性能提升: " << (ms1.count() / ms2.count()) << "倍\n";

return 0;
}

在这个例子中,std::string_view通常会表现出明显更好的性能,特别是当字符串很长或迭代次数很多时。

接口灵活性

使用std::string_view作为函数参数可以让你的函数更加灵活,能够接受多种形式的字符串:

cpp
#include <iostream>
#include <string>
#include <string_view>

void printUppercase(std::string_view sv) {
for (char c : sv) {
std::cout << static_cast<char>(std::toupper(c));
}
std::cout << std::endl;
}

int main() {
// 可接受字面量
printUppercase("hello, world");

// 可接受std::string
std::string s = "c++ programming";
printUppercase(s);

// 可接受部分数组
char arr[] = "array string";
printUppercase(std::string_view(arr + 6, 6)); // "string"

return 0;
}

输出:

HELLO, WORLD
C++ PROGRAMMING
STRING

字符串视图的注意事项

生命周期问题

使用std::string_view时最重要的考虑是生命周期管理。由于它只是一个"视图",不拥有数据,所以你必须确保被引用的原始字符串在string_view使用期间保持有效:

cpp
#include <iostream>
#include <string>
#include <string_view>

std::string_view dangerous() {
std::string local = "这个字符串会被销毁";
return local; // 危险!返回引用临时对象的视图
} // local被销毁,返回的string_view指向已释放的内存

void safe(std::string_view sv) {
std::cout << sv << std::endl; // 安全,因为原始字符串在函数调用期间仍然有效
}

int main() {
std::string str = "安全的字符串";

// 安全用法: 原始字符串在使用视图时依然存在
std::string_view sv = str;
std::cout << sv << std::endl;

// 安全用法: 传递给函数
safe(str);

// 危险用法: 使用指向已销毁对象的视图
std::string_view dangerous_sv = dangerous();
// std::cout << dangerous_sv << std::endl; // 取消注释会导致未定义行为!

return 0;
}
注意

永远不要返回指向局部变量的std::string_view,这会导致悬挂引用(dangling reference)和未定义行为。

不能用于需要空终止符的API

std::string_view可能不以空字符('\0')结尾,因此不能直接用于需要C风格字符串的API:

cpp
#include <iostream>
#include <string_view>
#include <cstring>

int main() {
std::string_view sv = "Hello, World!";

// 错误:strlen需要以'\0'结尾的字符串
// size_t length = strlen(sv.data()); // 可能导致缓冲区溢出

// 正确:使用string_view自己的length()方法
size_t length = sv.length();
std::cout << "长度: " << length << std::endl;

// 如果必须使用C API,需要先转换为std::string
std::string str(sv);
size_t c_length = strlen(str.c_str());
std::cout << "C风格长度: " << c_length << std::endl;

return 0;
}

实际案例:配置解析器

以下是一个使用std::string_view解析简单配置文件的例子,展示了在实际应用中的优势:

cpp
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <fstream>
#include <sstream>

class ConfigParser {
private:
std::unordered_map<std::string, std::string> config;

public:
// 解析配置文件
bool parse(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) return false;

std::string line;
while (std::getline(file, line)) {
processLine(line);
}
return true;
}

// 使用string_view高效处理每一行
void processLine(std::string_view line) {
// 跳过空行和注释
if (line.empty() || line[0] == '#') return;

// 查找等号
auto pos = line.find('=');
if (pos == std::string_view::npos) return;

// 提取键和值
auto key = line.substr(0, pos);
auto value = line.substr(pos + 1);

// 修剪空白字符
key = trimView(key);
value = trimView(value);

// 存储键值对(这里需要转为std::string)
if (!key.empty()) {
config[std::string(key)] = std::string(value);
}
}

// 使用string_view进行高效的空白字符修剪
std::string_view trimView(std::string_view sv) {
// 跳过前导空白
size_t start = 0;
while (start < sv.size() && std::isspace(sv[start])) {
++start;
}

// 跳过后缀空白
size_t end = sv.size();
while (end > start && std::isspace(sv[end - 1])) {
--end;
}

return sv.substr(start, end - start);
}

// 获取配置值
std::string_view getValue(const std::string& key) const {
auto it = config.find(key);
if (it != config.end()) {
return it->second;
}
return {};
}

// 打印所有配置
void printConfig() const {
for (const auto& [key, value] : config) {
std::cout << key << " = " << value << std::endl;
}
}
};

int main() {
// 创建一个示例配置文件
{
std::ofstream file("test_config.txt");
file << "# 这是一个配置文件\n";
file << "server_name = MyServer\n";
file << "port = 8080\n";
file << " path = /var/www/html \n"; // 包含空白
file << "debug = true\n";
}

// 解析配置
ConfigParser parser;
if (parser.parse("test_config.txt")) {
std::cout << "配置文件已成功解析:\n";
parser.printConfig();

// 使用配置值
std::cout << "\n服务器将在端口 " << parser.getValue("port")
<< " 上启动 '" << parser.getValue("server_name") << "'\n";
} else {
std::cout << "无法打开配置文件!\n";
}

return 0;
}

输出:

配置文件已成功解析:
path = /var/www/html
port = 8080
debug = true
server_name = MyServer

服务器将在端口 8080 上启动 'MyServer'

在这个示例中,std::string_view高效地处理了字符串解析操作,避免了多余的内存分配,特别是在行处理和修剪函数中。

总结

std::string_view是C++17引入的强大工具,为字符串处理提供了更高效的解决方案。它的主要优点包括:

  1. 性能优势:避免不必要的内存分配和复制
  2. 接口灵活性:可以接受多种形式的字符串输入
  3. 内存效率:只存储指针和长度,不拥有底层数据

但使用时需要注意几个关键点:

  1. 生命周期管理:确保原始字符串在string_view使用期间保持有效
  2. 不会以空字符结尾:不能直接用于期望C风格字符串的API
  3. 不可修改性:只能查看,不能修改字符串内容

当你需要临时访问字符串数据而不需要修改它时,std::string_view通常是最佳选择。

练习

  1. 编写一个使用std::string_view的函数,统计字符串中特定字符的出现次数。
  2. 实现一个简单的字符串分割函数,使用std::string_view按指定分隔符分割字符串。
  3. 创建一个程序,使用std::string_view从文件中读取每一行并处理,比较与使用std::string的性能差异。
  4. 挑战题:实现一个简单的JSON解析器,使用std::string_view提高解析效率。

参考资源

  • C++ 参考文档: std::string_view
  • C++17标准: ISO/IEC 14882:2017
  • 《Effective Modern C++》 by Scott Meyers
  • 《C++ Core Guidelines》关于string_view的建议