第22章 字符串类string
第22章 字符串类string
- string类的核心操作与内存管理
- 高效字符串查找、替换与子串操作
- string与数值、C风格字符串的转换
如果你还在用 char 数组和 strcpy() 处理文本,现在是时候升级了。C++标准库的 std::string 不是一个简单的字符容器,它是一个自带内存管理、异常安全和丰富接口的智能字符串工具。我在一个日志分析项目中,将核心处理模块从C字符串改为string后,代码行数减少了30%,内存错误归零。
22.1 string的本质:不只是字符数组
可以把string想象成一个“智能文本收纳盒”。普通的字符数组(C风格字符串)就像一个固定大小的纸箱,你需要自己计算大小、小心搬运以防溢出。而string是一个自动伸缩的魔术箱,你只管往里放文本,它会自己调整大小,还提供查找、裁剪、拼接等各种工具。
技术上,std::string 是 basic_string<char> 的别名,一个模板特化的类。它管理着一个动态分配的字符数组,并记录了长度和容量。
22.2 核心操作全景图
与vector类似,string支持下标访问、迭代器、push_back()、clear()等操作。但它有一系列专为文本处理设计的独有成员函数。
| 操作类别 | 关键成员函数 | 功能说明 | 时间复杂度 |
|---|---|---|---|
| 构造与赋值 | string(const char*), operator=, assign() | 从C字符串、字符、另一个string构造或赋值 | O(n) |
| 容量操作 | size()/length(), capacity(), reserve(100) | 获取长度/容量,预分配内存避免多次重分配 | O(1) |
| 元素访问 | operator[], at(), front(), back() | 访问字符,at()会进行边界检查并可能抛出std::out_of_range | O(1) |
| 修改操作 | append(), operator+=, insert(), erase() | 拼接、插入、删除字符或子串 | 通常O(n) |
| 字符串操作 | c_str(), data(), substr(), copy() | 获取C风格指针、提取子串、复制到字符数组 | O(n) |
| 查找与替换 | find(), rfind(), replace() | 正向/反向查找子串,替换指定部分 | 平均O(n*m) |
| 比较 | compare(), operator==, operator< | 按字典序比较字符串 | O(n) |
关键点:短字符串优化(SSO)
这是string性能的关键。对于较短的字符串(例如在GCC/Clang上通常是15或23个字符以内),string对象会将其直接存储在自身的栈内存中,而不是去堆上动态分配。这彻底消除了短字符串创建和拷贝的动态内存开销。
std::string s1 = "hello"; // 很可能触发SSO,数据在栈上
std::string s2 = "this is a very long string that exceeds SSO buffer"; // 在堆上分配
// 验证:s1.capacity() 可能等于一个较小的固定值(如15)
22.3 查找与替换:文本处理的瑞士军刀
find() 系列函数是string最强大的功能之一。它们返回的是子串首次出现的位置(size_t类型),如果未找到则返回 std::string::npos。
std::string log = "Error: file not found. Error: permission denied.";
size_t pos = 0;
while ((pos = log.find("Error", pos)) != std::string::npos) {
std::cout << "Found 'Error' at index: " << pos << std::endl;
pos += 5; // 跳过本次找到的"Error",继续向后查找
}
// 将所有"Error"替换为"Warning"
pos = 0;
while ((pos = log.find("Error", pos)) != std::string::npos) {
log.replace(pos, 5, "Warning"); // 从pos开始,替换5个字符
pos += 7; // 跳过新插入的"Warning"
}
常见误区
- 混淆 find() 和 rfind():find() 从前往后找,rfind() 从后往前找。如果你需要找到最后一个匹配项,用 rfind(),而不是在循环里用 find() 一直找到头。
- 忘记检查 npos:直接使用 find() 的返回值作为下标是危险的,必须先判断是否等于 std::string::npos。
- replace() 导致迭代器失效:如果在循环中使用迭代器遍历string并调用 replace(),迭代器可能会失效。更安全的做法是使用索引(size_t)进行循环和修改。
22.4 数值与字符串的相互转换
这是实际开发中的高频操作。C++11提供了标准方法 std::to_string() 和 std::stoi() / stod() 等,彻底告别了 sprintf() 和 atoi()。
数字转字符串
- 使用 std::to_string(42)
- 支持整数、浮点数
- 返回 std::string
字符串转整数
- 使用 std::stoi("100")
- 可选参数指定进制
- 失败抛出 std::invalid_argument
字符串转浮点
- 使用 std::stod("3.14")
- 处理科学计数法
- 失败抛出异常
高级解析
- stoi(str, &idx, base)
- idx返回处理到的字符位置
- base指定进制(2-36)
// 将用户输入的字符串转换为整数,并处理可能的错误
std::string user_input = "123abc";
try {
size_t pos = 0;
int value = std::stoi(user_input, &pos); // pos 将是 3,因为"123"被成功转换
std::cout << "Parsed value: " << value << ", stopped at pos: " << pos << std::endl;
if (pos < user_input.length()) {
std::cout << "Extra characters: " << user_input.substr(pos) << std::endl;
}
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << std::endl;
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << std::endl;
}
编者提示:字符串转换中的异常处理(invalid_argument, out_of_range)是第20章《异常处理机制》知识点的典型应用场景。良好的错误处理能让你的程序更加健壮。
22.5 性能考量与最佳实践
string用起来方便,但如果不了解其内部机制,也可能写出低效代码。
- 预分配内存:如果你知道字符串最终的大致长度,使用 reserve() 一次性分配足够内存,可以避免多次扩容和数据拷贝。我在处理一个平均长度为1KB的XML文本时,预分配让拼接速度提升了近5倍。
- 慎用 operator+ 进行多次拼接:
s = a + b + c + d;会产生多个临时string对象。对于循环内的拼接,使用 += 或 append() 效率更高,或者使用 std::ostringstream。 - 获取C风格字符串:c_str() 返回的是一个指向内部数据的只读指针,该指针在string发生任何非const操作后都可能失效。如果需要长期持有,应该用 strcpy() 复制出来。
// 低效做法
std::string result;
for (int i = 0; i < 10000; ++i) {
result = result + "data"; // 每次循环都创建新临时对象!
}
// 高效做法
std::string result;
result.reserve(50000); // 预分配足够空间
for (int i = 0; i < 10000; ++i) {
result.append("data"); // 直接在预留空间后追加
}
动手试一试
1. 实现一个简单的配置文件解析器:编写一个函数,读取如下格式的字符串,解析出键值对,存入 std::map<std::string, std::string>。
"host=127.0.0.1;port=8080;user=admin;timeout=30"
提示:使用 find() 定位 `=` 和 `;`,使用 substr() 截取子串。
2. 性能对比实验:编写两个函数,都用于生成一个包含10000个“itemX”(X为0-9999)的字符串。一个使用循环和 operator+,另一个使用 reserve() 和 append()。使用 std::chrono 测量两者耗时,感受差异。
检验你的理解
- 下面的代码输出是什么?为什么?
std::string s = "Hello"; const char* p1 = s.c_str(); s.append(" World"); std::cout << p1; - 如何安全地将一个 std::string 的内容传递给一个只接受 const char* 的C语言API函数?
- 编写一行代码,使用 std::to_string() 和 std::stod(),将数字 3.14159 转换成字符串,再转换回 double,并保证精度。
本章小结
- std::string 是一个管理动态字符数组的类,提供了丰富的文本操作接口,并利用短字符串优化(SSO)提升短文本性能。
- 字符串查找(find/rfind)和替换(replace)是核心功能,使用时需注意返回值 npos 的检查和迭代器失效问题。
- 使用 std::to_string() 和 std::stoi()/stod() 等函数进行数值与字符串的安全转换,替代不安全的C函数。
- 通过 reserve() 预分配内存、在循环中使用 append() 或 += 而非 operator+,可以显著提升字符串拼接的性能。
- c_str() 返回的指针仅在string未修改时有效,需要长期持有时必须复制数据。
微信扫码关注
抖音扫码关注