字符串处理:C风格字符串、string类、常用成员函数
字符串处理,说白了就是编程里的基本功。我做了这么多年C++,几乎每个项目都离不开它。无论是解析配置文件、处理用户输入,还是拼接网络协议包,字符串操作都是家常便饭。
C++里有两套字符串体系:一套是老祖宗C语言留下的C风格字符串,另一套是C++标准库提供的string类。我个人习惯,能用string类就尽量用,但C风格字符串的知识你也得懂——毕竟很多底层接口、遗留代码还在用。
C风格字符串:老派但必须懂
C风格字符串本质上就是个字符数组,以空字符'\0'结尾。你想想看,没有这个结束符,程序根本不知道字符串到哪结束。
// C风格字符串的几种定义方式
char str1[] = "Hello"; // 自动添加'\0',实际占6字节
char str2[6] = "Hello"; // 显式指定大小,必须留一个给'\0'
char* str3 = "Hello"; // 字符串常量,不可修改
// 常见操作
#include <cstring>
char src[] = "World";
char dst[20];
strcpy(dst, src); // 复制字符串
strcat(dst, "!"); // 拼接字符串
int len = strlen(dst); // 获取长度(不含'\0')
int cmp = strcmp("abc", "abd"); // 比较,返回负值/0/正值
⚠️ 避坑指南
我曾经在一个网络模块里用strcpy复制用户输入的数据,结果对方发了个超长字符串,直接缓冲区溢出,程序崩溃了整整一晚上。后来我养成了习惯:能用strncpy就别用strcpy,能指定最大长度就别偷懒。
C风格字符串最大的问题就是不安全。你手动管理内存,手动保证缓冲区够大,稍不留神就出bug。而且操作起来也麻烦——复制、拼接、比较都要调函数,不像面向对象那样直观。
string类:现代C++的标配
std::string是C++标准库提供的字符串类,封装了动态内存管理。说白了,你只管用,它自己会扩容、会释放,省心多了。
#include <string>
std::string s1 = "Hello";
std::string s2("World");
std::string s3 = s1 + " " + s2; // 拼接,s3 = "Hello World"
// 直接比较,不用调函数
if (s1 == "Hello") {
// 相等
}
// 获取长度
size_t len = s3.length(); // 或者 s3.size()
我在项目中遇到过好几次这样的场景:用C风格字符串时,拼接三个字符串得先算总长度、分配缓冲区、再一个个strcat。换成string类,一行加号搞定。代码干净,人也舒服。
常用成员函数:你一定会用到的
string类的成员函数很多,但常用的就那么十几个。我按功能分了几类,你重点掌握这些就够了。
| 分类 | 函数 | 说明 |
|---|---|---|
| 容量相关 | size() / length() | 返回字符个数 |
| empty() | 判断是否为空字符串 | |
| capacity() | 返回当前分配的内存容量 | |
| 访问与修改 | at(pos) / operator[] | 访问指定位置的字符 |
| front() / back() | 访问首/尾字符 | |
| push_back(c) / pop_back() | 在末尾添加/删除字符 | |
| 查找与子串 | find(str, pos) | 从pos开始查找子串,返回位置 |
| rfind(str) | 从末尾向前查找 | |
| substr(pos, len) | 提取子串 | |
| 插入与删除 | insert(pos, str) | 在pos位置插入字符串 |
| erase(pos, len) | 删除从pos开始的len个字符 | |
| 转换 | c_str() | 返回const char*,用于C风格接口 |
| data() | 同c_str(),C++11起返回字符数组 |
💡 重点提示
at()和operator[]的区别:at()会做边界检查,越界时抛出out_of_range异常;operator[]不检查,越界是未定义行为。调试阶段我建议用at(),上线后如果性能敏感可以换成operator[]。
实战:一个完整的字符串处理示例
光说不练假把式。我写个小例子,把上面这些函数串起来用一遍。
#include <iostream>
#include <string>
int main() {
std::string text = "C++ programming is fun!";
// 1. 获取长度
std::cout << "长度: " << text.length() << std::endl;
// 2. 查找子串
size_t pos = text.find("programming");
if (pos != std::string::npos) {
std::cout << "'programming' 的位置: " << pos << std::endl;
}
// 3. 提取子串
std::string sub = text.substr(4, 11); // "programming"
std::cout << "子串: " << sub << std::endl;
// 4. 插入
text.insert(0, "Hello, ");
std::cout << "插入后: " << text << std::endl;
// 5. 替换
size_t start = text.find("fun");
text.replace(start, 3, "awesome");
std::cout << "替换后: " << text << std::endl;
// 6. 转换为C风格字符串,传给旧接口
const char* cstr = text.c_str();
std::cout << "C风格: " << cstr << std::endl;
return 0;
}
📌 小技巧
find()找不到子串时返回std::string::npos,这个值通常是-1(转换成size_t后是最大值)。所以判断时一定要用 != npos,别写成 >= 0,因为size_t是无符号的。
string vs C风格字符串:怎么选?
我个人的经验法则很简单:
- 新代码:无脑用std::string。安全、方便、可读性好。
- 调用C库函数:用c_str()转一下。比如fopen、printf这些。
- 性能极致场景:比如高频日志、网络包序列化,可以考虑C风格字符串或自定义缓冲区。但先profile,别瞎优化。
- 嵌入式/内存受限:C风格字符串更可控,没有动态分配的开销。
你想想看,大部分业务代码里,string类的性能完全够用。我见过有人为了省那几微秒,手写一堆C风格字符串操作,结果bug不断,维护成本翻倍。得不偿失。
知识体系总览
下面这张图把本章的核心知识点串起来了,方便你整体把握。
嗯,到这里字符串处理的核心内容就差不多了。C风格字符串是基础,你得理解它的内存模型和风险。string类是日常开发的主力,常用成员函数要烂熟于心。两者之间的转换(c_str())也是高频操作。
我个人建议你写代码时多练练string类的各种操作,尤其是find和substr的组合使用,解析文本时特别管用。遇到性能瓶颈再回头优化,别一开始就想着手写C风格字符串——那是给自己挖坑。