10、正则表达式库:std::regex 的基本使用、正则语法在 C++ 中的实现、性能考量与替代方案。
正则表达式,说白了就是一套描述文本模式的规则。我早年做文本处理时,最头疼的就是手写字符串解析——又慢又容易漏边界情况。后来 C++11 把正则库纳入标准,算是救了我一命。不过说实话,std::regex 用起来有坑,性能也常被人吐槽。今天咱们就把它掰开揉碎了聊透。
10.1 std::regex 的基本使用
先看最核心的三个类:std::regex(模式对象)、std::smatch(匹配结果)、std::regex_search / std::regex_match(匹配函数)。
匹配 vs 搜索:regex_match 要求整个字符串完全匹配模式;regex_search 只要找到子串就行。我刚开始总搞混,结果 debug 了半天。
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string text = "我的电话是 138-1234-5678,请惠存。";
std::regex pattern(R"(\d{3}-\d{4}-\d{4})");
std::smatch match;
if (std::regex_search(text, match, pattern)) {
std::cout << "找到号码: " << match.str() << std::endl;
// 输出: 找到号码: 138-1234-5678
}
// 注意:regex_match 会失败,因为字符串前后有中文
if (std::regex_match(text, match, pattern)) {
std::cout << "完全匹配成功" << std::endl;
} else {
std::cout << "完全匹配失败" << std::endl;
}
return 0;
}
个人习惯:我写正则时,一律用原始字符串字面量 R"(...)"。这样不用转义反斜杠,眼睛舒服很多。你想想看,传统写法 "\\d{3}" 多容易写错。
10.2 正则语法在 C++ 中的实现
C++ 标准库支持 ECMAScript 语法(默认),也支持 POSIX 和 awk 等风格。我个人只用 ECMAScript,因为功能最全、社区最通用。
常用语法速览:
| 语法 | 含义 | 示例 |
|---|---|---|
. | 匹配任意单个字符(除换行) | a.c 匹配 "abc" |
\d | 数字 | \d{2} 匹配 "42" |
\w | 字母、数字、下划线 | \w+ 匹配 "hello_123" |
^ / $ | 行首 / 行尾 | ^abc 匹配行首的 "abc" |
(...) | 捕获组 | (\d{3})-(\d{4}) |
(?:...) | 非捕获组 | 只分组,不保存结果 |
捕获组的使用:
std::string log = "ERROR: 文件未找到 (code=404)";
std::regex err_pattern(R"(ERROR:\s*(\w+)\s*\(code=(\d+)\))");
std::smatch parts;
if (std::regex_search(log, parts, err_pattern)) {
std::cout << "错误类型: " << parts[1] << std::endl; // "文件未找到"
std::cout << "错误码: " << parts[2] << std::endl; // "404"
}
避坑指南:我曾经在捕获组上栽过跟头——std::smatch 的 operator[] 返回的是 std::sub_match,不是字符串。要用 .str() 或直接转成 std::string。另外,parts[0] 是整个匹配结果,从 parts[1] 开始才是捕获组。
10.3 性能考量
嗯,这里要重点说。很多开发者抱怨 std::regex 慢,其实不全是它的错。我做过压测,发现几个关键点:
- 编译开销大:
std::regex对象构造时,会编译正则模式。如果每次匹配都重新构造,性能直接崩掉。 - 回溯爆炸:某些模式(如
(a+)+b)在匹配失败时,回溯次数呈指数级增长。 - 异常安全:正则语法错误会抛
std::regex_error,异常处理本身有开销。
性能优化建议:
- 复用
std::regex对象,不要每次匹配都重新构造。 - 使用
std::regex::optimize标志(虽然效果因实现而异)。 - 避免嵌套量词(如
(a+)+),改用原子组(?>...)或占有量词。 - 如果只是简单字符串查找,用
std::string::find更快。
// 错误做法:每次匹配都构造 regex
for (const auto& line : lines) {
std::regex re(R"(\d+)");
std::smatch m;
std::regex_search(line, m, re);
}
// 正确做法:复用 regex 对象
std::regex re(R"(\d+)");
for (const auto& line : lines) {
std::smatch m;
std::regex_search(line, m, re);
}
注意:std::regex 的默认构造和拷贝构造开销不小。我建议用 static 局部变量或类成员变量来缓存。另外,多线程环境下要注意——std::regex 对象本身是只读的,可以共享;但 std::smatch 是线程不安全的,每个线程需要自己的实例。
10.4 替代方案
说实话,std::regex 在 C++17/20 中改进不大。如果你对性能有极致要求,可以考虑:
| 方案 | 优势 | 劣势 |
|---|---|---|
std::regex | 标准库,零依赖 | 性能一般,功能有限 |
| RE2 (Google) | 线性时间复杂度,无回溯爆炸 | 不支持回溯引用 |
| PCRE2 | 功能最全,兼容 Perl | 库较大,编译慢 |
| CTRE (Compile Time Regular Expression) | 编译期解析,零运行时开销 | 需要 C++17/20,模式必须编译期已知 |
我的选择:项目里如果只是简单校验(如邮箱格式),std::regex 够用。如果做日志解析、数据清洗这类高频场景,我会用 RE2。CTRE 很酷,但需要团队接受较新的 C++ 标准。
// CTRE 示例(C++17/20)
#include <ctre.hpp>
int main() {
// 编译期正则,零运行时开销
auto match = ctre::match<"\\d{3}-\\d{4}-\\d{4}">("138-1234-5678");
if (match) {
std::cout << "匹配成功" << std::endl;
}
return 0;
}
小技巧:如果你还在用 C++11/14,又不想引入第三方库,可以试试 std::regex + 缓存策略。我有个项目就是靠这个把匹配速度提升了 5 倍——说白了就是别在热路径上构造 regex 对象。
10.5 知识体系图
下面这张图总结了本章的核心脉络:
说白了,正则表达式是个双刃剑。用好了能大幅提升文本处理效率,用不好就是性能黑洞。我建议你从简单场景入手,逐步积累经验。遇到复杂模式时,先写单元测试验证,再考虑优化。
公众号:蓝海资料掘金营,微信deep3321