一、字符串处理——C++里最常用的基本功
说实话,字符串处理是每个C++程序员都绕不开的坎。我刚开始写C++那会儿,还在用C风格字符串,踩过的坑能写满一页A4纸。后来有了std::string,我才算真正解放了双手。
这一章,我会带你彻底搞懂C++里的字符串处理。从老派的C风格字符串,到现代的std::string,再到字符串流和正则表达式,咱们一步步来。
1.1 C风格字符串——老祖宗留下的遗产
C风格字符串,说白了就是一个以'\0'结尾的字符数组。嗯,就是这么简单粗暴。
// C风格字符串的定义
char str1[] = "Hello"; // 自动添加'\0'
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'}; // 手动加'\0'
const char* str3 = "World"; // 字符串常量
// 常用操作——得用C标准库函数
#include <cstring>
char dest[20];
strcpy(dest, "Hello"); // 复制
strcat(dest, " World"); // 拼接
int len = strlen(dest); // 长度(不含'\0')
int cmp = strcmp("abc", "abd"); // 比较,返回负值/0/正值
⚠️ 我曾经踩过的坑:用strcpy时目标数组太小,直接导致缓冲区溢出。程序跑着跑着就崩了,查了半天才发现是字符串拷贝越界。后来我改用strncpy,至少能指定最大长度。
C风格字符串的问题很明显:你得自己管理内存,自己保证缓冲区够大,自己记得加'\0'。说实话,这太容易出错了。
1.2 std::string——现代C++的救星
std::string是C++标准库提供的字符串类。它帮你管理内存,自动调整大小,再也不用担心缓冲区溢出了。
#include <string>
std::string s1 = "Hello";
std::string s2("World");
std::string s3 = s1 + " " + s2; // 拼接,s3 = "Hello World"
// 常用操作
s1.length(); // 长度
s1.size(); // 同length()
s1.empty(); // 是否为空
s1[0]; // 访问字符,像数组一样
s1.at(0); // 带边界检查的访问
s1.append("!!!"); // 追加
s1.insert(5, " "); // 在位置5插入
s1.erase(5, 3); // 从位置5删除3个字符
s1.replace(0, 5, "Hi"); // 替换子串
s1.find("World"); // 查找,返回位置或npos
s1.substr(0, 5); // 取子串
💡 我个人习惯:能用std::string就别用C风格字符串。除非你要跟C语言接口打交道,或者做极高性能要求的场景。std::string的便利性远大于那一点点性能开销。
1.3 string的常用操作——实战中高频使用
下面这些操作,我在项目中几乎天天用。你想想看,字符串处理无非就是拼接、查找、替换、分割这几件事。
#include <string>
#include <iostream>
std::string text = " Hello, C++ World! ";
// 1. 去除空白
text.erase(0, text.find_first_not_of(" \t\n")); // 去除前导空白
text.erase(text.find_last_not_of(" \t\n") + 1); // 去除尾部空白
// 2. 大小写转换
#include <algorithm>
#include <cctype>
std::transform(text.begin(), text.end(), text.begin(), ::toupper);
std::transform(text.begin(), text.end(), text.begin(), ::tolower);
// 3. 分割字符串
std::vector<std::string> split(const std::string& s, char delim) {
std::vector<std::string> tokens;
std::string token;
std::istringstream tokenStream(s);
while (std::getline(tokenStream, token, delim)) {
tokens.push_back(token);
}
return tokens;
}
// 4. 数字与字符串互转
int num = std::stoi("42"); // 字符串转int
double d = std::stod("3.14"); // 字符串转double
std::string s = std::to_string(42); // 数字转字符串
🔑 关键点:std::string的find()方法返回的是size_t类型,如果没找到会返回std::string::npos。判断时一定要用npos,不要用-1。
1.4 字符串流(stringstream)——灵活的数据转换器
字符串流是个好东西。它把字符串当成流来处理,可以像操作cin/cout一样读写字符串。我在解析配置文件时经常用它。
#include <sstream>
// 基本用法:拼接和格式化
std::ostringstream oss;
oss << "Name: " << "Alice" << ", Age: " << 25;
std::string result = oss.str(); // "Name: Alice, Age: 25"
// 解析:从字符串中提取数据
std::istringstream iss("42 3.14 hello");
int i;
double d;
std::string s;
iss >> i >> d >> s; // i=42, d=3.14, s="hello"
// 实战:解析CSV行
std::string line = "1001,Alice,85.5";
std::istringstream lineStream(line);
std::string id, name, score;
std::getline(lineStream, id, ',');
std::getline(lineStream, name, ',');
std::getline(lineStream, score, ',');
// id="1001", name="Alice", score="85.5"
⚠️ 注意:stringstream在反复使用时,记得调用clear()和str("")重置状态。我遇到过因为流状态错误导致后续解析全部失败的情况,排查了好久才发现是忘了重置。
1.5 正则表达式入门(regex)——模式匹配的利器
正则表达式,说白了就是一套描述字符串模式的规则。C++11开始标准库直接支持regex,不用再依赖第三方库了。
#include <regex>
std::string text = "我的邮箱是 alice@example.com,电话是 138-0000-0000";
// 1. 匹配邮箱
std::regex emailPattern(R"(\w+@\w+\.\w+)");
std::smatch match;
if (std::regex_search(text, match, emailPattern)) {
std::cout << "找到邮箱: " << match.str() << std::endl;
}
// 2. 替换
std::regex phonePattern(R"(\d{3}-\d{4}-\d{4})");
std::string result = std::regex_replace(text, phonePattern, "***-****-****");
// 结果: "我的邮箱是 alice@example.com,电话是 ***-****-****"
// 3. 迭代匹配所有
std::regex wordPattern(R"(\w+)");
auto words_begin = std::sregex_iterator(text.begin(), text.end(), wordPattern);
auto words_end = std::sregex_iterator();
for (auto it = words_begin; it != words_end; ++it) {
std::cout << it->str() << std::endl;
}
💡 我建议:正则表达式虽然强大,但别滥用。简单的字符串查找用find()就够了。正则表达式编译有开销,如果同一个模式要匹配多次,记得把regex对象缓存起来。
1.6 本章知识体系
下面这张图帮你理清本章的核心脉络:
从图中你能看到,字符串处理有四个核心方向。C风格字符串是基础,但日常开发我更推荐std::string。字符串流擅长数据转换,正则表达式则搞定复杂模式匹配。把这四块吃透,大部分字符串处理场景你都能应对自如。
好了,这一章的内容就到这里。字符串处理是C++编程的基石,多写多练自然就熟练了。下一章我们会深入探讨C++的输入输出流,到时候见。