string容器:从C风格字符串到C++的优雅进化
说实话,我刚开始学C++那会儿,对string是有点不屑的。觉得char*用得好好的,干嘛要学个新东西?直到我在一个项目里因为内存泄漏熬了三个通宵……嗯,从那以后,我再也没手写过char*的业务逻辑。
string的本质,是C++标准库为我们封装好的一个类。它管理着动态字符数组,自动处理内存的申请和释放。你想想看,这省了多少事?
string与char*的区别
先说说最核心的区别。char*是个指针,它指向一块字符内存。而string是个对象,它自己管自己。
| 对比项 | char* | std::string |
|---|---|---|
| 内存管理 | 手动new/delete | 自动管理 |
| 长度获取 | strlen()遍历 | .size()或.length() |
| 拼接操作 | strcat容易溢出 | operator+安全拼接 |
| 拷贝赋值 | strcpy深拷贝 | 直接赋值即可 |
| 空值安全 | 需要判null | 永远不为null |
核心结论:只要不是写底层库或嵌入式裸机代码,优先用string。我个人的原则是——除非必须和C接口交互,否则绝不碰char*。
string构造函数
string的构造方式挺多的,我挑几个最常用的说说。
#include <iostream>
#include <string>
using namespace std;
int main() {
// 默认构造
string s1;
// 用C风格字符串构造
string s2("hello world");
// 拷贝构造
string s3(s2);
// 用n个字符c填充
string s4(5, 'A'); // "AAAAA"
// 用迭代器范围构造
string s5(s2.begin(), s2.begin() + 5); // "hello"
// 子串构造
string s6(s2, 6, 5); // 从索引6取5个字符 -> "world"
return 0;
}
我在项目中遇到过一种情况:从网络缓冲区里读数据,需要构造一个固定长度的string。这时候用string(buf, len)就特别顺手,比先new char[]再strcpy优雅太多了。
string赋值操作
赋值这块,string提供了多种方式。我个人习惯用operator=,简单直接。
string s1, s2, s3;
s1 = "hello"; // 直接赋值C风格字符串
s2 = s1; // 对象赋值
s3.assign("world", 3); // assign成员函数,取前3个字符 -> "wor"
// assign也支持迭代器
s3.assign(s1.begin() + 1, s1.end()); // "ello"
小技巧:assign函数其实比operator=更灵活。比如你想从字符串中间取一段赋值,用assign配合迭代器就很方便。operator=只能整体赋值。
string字符串拼接
拼接是日常开发里最频繁的操作之一。string的拼接,说白了就是+和+=。
string s1 = "hello";
string s2 = "world";
string s3 = s1 + " " + s2; // "hello world"
s1 += " C++"; // s1变成"hello C++"
// append函数更灵活
s1.append(" standard");
s1.append(s2, 0, 3); // 追加s2的前3个字符 -> "wor"
我记得有一次写日志模块,需要拼接大量字符串。一开始用+拼接,性能惨不忍睹。后来改成+=和append,速度快了好几倍。为什么会这样?因为+会创建临时对象,频繁分配内存。而+=直接在原对象上扩容。
避坑指南:我曾经在循环里用s = s + "xxx"拼接字符串,结果处理10万条数据跑了3秒多。改成s += "xxx"后,降到0.2秒。如果你要大量拼接,建议先reserve()预留空间。
string查找与替换
查找和替换,是文本处理的核心。string提供了find、rfind、replace等函数。
string s = "hello world, hello C++";
// 查找子串第一次出现的位置
size_t pos = s.find("hello"); // 返回0
// 从指定位置开始查找
pos = s.find("hello", 5); // 从索引5开始找,返回13
// 反向查找
pos = s.rfind("hello"); // 返回13
// 查找失败返回string::npos
if (s.find("java") == string::npos) {
cout << "没找到" << endl;
}
// 替换
s.replace(6, 5, "C++"); // 从索引6开始,替换5个字符为"C++"
// 结果: "hello C++, hello C++"
注意:find返回的是size_t类型,判断是否找到时一定要用string::npos,不要用-1或0。我见过有人写成if (pos >= 0),结果size_t永远不小于0,bug藏得很深。
string字符串比较
比较两个字符串,直接用==、!=、<、>就行。string重载了这些运算符,按字典序比较。
string s1 = "apple";
string s2 = "banana";
if (s1 == s2) { /* ... */ }
if (s1 < s2) { /* apple排在banana前面 */ }
// compare函数返回-1、0、1
int ret = s1.compare(s2); // 返回负数
ret = s1.compare(0, 3, s2, 0, 3); // 比较前3个字符
我个人几乎不用compare函数,除非需要比较子串。日常开发里,==和<完全够用,代码也更清晰。
string字符存取
存取单个字符,有两种方式:[]运算符和at()函数。
string s = "hello";
// 用[]存取
char c = s[0]; // 'h'
s[0] = 'H'; // 变成"Hello"
// 用at()存取
c = s.at(1); // 'e'
s.at(1) = 'E'; // 变成"HEllo"
避坑指南:我曾经用s[100]访问了一个长度为10的字符串,程序没崩溃,但读到了一个随机字符。这个bug在测试环境没复现,上线后偶尔出现乱码。后来排查了两天才找到原因。[]不检查越界,而at()会抛出out_of_range异常。调试阶段建议用at(),上线后如果性能敏感可以换成[]。
string插入与删除
插入和删除,用insert和erase。
string s = "hello";
// 在索引1处插入"xxx"
s.insert(1, "xxx"); // "hxxxello"
// 删除从索引1开始的3个字符
s.erase(1, 3); // 恢复成"hello"
// 清空字符串
s.clear(); // s变成空串
s.erase(); // 效果同上
插入和删除操作会移动元素,时间复杂度是O(n)。如果你需要频繁在中间插入删除,建议考虑deque或list。
string子串获取
获取子串用substr,这个函数我几乎每天都会用到。
string s = "hello world";
// 从索引6开始取5个字符
string sub = s.substr(6, 5); // "world"
// 只给起始位置,取到末尾
sub = s.substr(6); // "world"
// 取前5个字符
sub = s.substr(0, 5); // "hello"
小技巧:解析配置文件或网络协议时,我经常用substr配合find来提取字段。比如先找到分隔符的位置,然后用substr截取。这种组合拳非常实用。
知识体系总览
下面这张图,是我梳理的string核心操作关系。你可以把它当作一个快速索引。
string容器,说白了就是C++给开发者的一份礼物。它把最繁琐的内存管理、边界检查、拷贝赋值都封装好了。你只需要关注业务逻辑,剩下的交给标准库。
我个人建议,新手入门先把string的常用接口练熟。等用顺手了,你会发现再也回不去char*了。