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提供了findrfindreplace等函数。

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,不要用-10。我见过有人写成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插入与删除

插入和删除,用inserterase

string s = "hello";

// 在索引1处插入"xxx"
s.insert(1, "xxx");  // "hxxxello"

// 删除从索引1开始的3个字符
s.erase(1, 3);       // 恢复成"hello"

// 清空字符串
s.clear();           // s变成空串
s.erase();           // 效果同上

插入和删除操作会移动元素,时间复杂度是O(n)。如果你需要频繁在中间插入删除,建议考虑dequelist

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核心操作关系。你可以把它当作一个快速索引。

std::string 构造与赋值 默认构造 / 拷贝构造 C风格字符串构造 operator= / assign 操作与访问 拼接: + / += / append 查找: find / rfind 替换: replace 插入/删除: insert / erase 比较与子串 比较: == / != / compare 字符存取: [] / at() 子串: substr 核心原则:优先用string,避免手动管理内存 大量拼接时用 += 或 append,并提前 reserve

string容器,说白了就是C++给开发者的一份礼物。它把最繁琐的内存管理、边界检查、拷贝赋值都封装好了。你只需要关注业务逻辑,剩下的交给标准库。

我个人建议,新手入门先把string的常用接口练熟。等用顺手了,你会发现再也回不去char*了。

公众号:蓝海资料掘金营,微信deep3321