那些年:C++ 还没有“官方正则”
很久以前。
C++ 里想“用正则”,并不是一句 #include 就完事。
你要么去翻 POSIX 的 regex.h。
要么拉个 PCRE。
要么用 Boost。
然后开始跟编译选项、链接、版本兼容吵架。
最尴尬的是。
正则往往不是“锦上添花”。
它通常发生在你最不想动代码的时候。
比如线上出事。
你手上只有一堆日志。
你只想把里面那几个字段抠出来。
更早一点:正则不是从 C++ 长出来的
正则这门手艺,最早是在 Unix 工具链里长出来的。 你可以把它想成:一群人天天在文本里找模式,于是把“找的规则”总结成了一套写法。
后来它慢慢分出了几条血脉。
有偏“工具”的,比如 grep/sed/awk 那一套。
也有偏“语言”的,比如 Perl 那一套。
POSIX:给 C 世界一个最朴素的版本
POSIX 这条线的风格很像 C。 能用就行。 功能不追求花哨,但接口稳定,很多系统都自带。
所以你会看到 regex.h。
它不是 C++ 的东西。
它是“操作系统给你的正则”。
Perl:把正则从工具变成了“语法糖大礼包”
Perl 年代,正则直接写进语言里。 写起来爽。 功能也更像“瑞士军刀”。
很多你今天觉得理所当然的写法,比如分组、重复、各种转义,都是这条线把它们流行起来的。
PCRE:把 Perl 的味道搬回 C/C++ 工程里
PCRE 的意思基本就是:Perl-Compatible Regular Expressions。 你可以把它理解成“Perl 那套正则语法的库版本”。 很多工程喜欢它,就是因为它好用,而且跨平台。
Boost.Regex:C++ 社区的过渡期
在标准库还没动手之前,Boost.Regex 先把路趟了一遍。 它让 C++ 工程里用正则这件事,变得更像“一个正常的库”。 也让标准委员会看到了:这玩意儿确实有人用。
C++11 的选择:接口标准化,但语法得先选一套
到了 C++11,std::regex 终于进了标准库。
但标准库不能说“支持所有流派”。
它得先有一个默认语法。
所以你会看到一个很现实的折中。
默认是 ECMAScript(你可以把它理解成:跟 JavaScript 那套比较接近的一种语法风格)。
同时也提供 POSIX 风格的选项。
这件事对新手的影响很直接。
你在网上抄一段“看起来像 Perl/PCRE 的正则”,不一定在 std::regex 里就完全一样。
ECMAScript 到底是什么,你不用背历史但要知道它像谁
这里的 ECMAScript,说白了就是“JavaScript 那条线的正则味道”。
你不用去查 ECMAScript 标准文档。
你只要记住:std::regex 默认跟它比较接近。
而你在命令行里常见的 grep,更像是 POSIX 那条线。
同样是正则,有些细节会不一样。
所以你会遇到一种很典型的新手困惑:我在网上抄的正则,为啥在 C++ 里不对。
语法流派怎么选:你可以显式告诉 std::regex
using namespace std::regex_constants;
std::regex re1(R"(\d+)");
std::regex re2(R"(\d+)", ECMAScript);
std::regex re3(R"([0-9]+)", extended);
第一行是默认语法。 第二行把默认说得更明确。 第三行是 POSIX ERE(extended regular expression)那条路。
你不需要一口气学完这些。
但你至少要知道:std::regex 不是“只有一种正则”。
它只是默认选了其中一种。
小对比:同一个意思,不同流派写法不一样
比如“匹配一串数字”。
在默认的 ECMAScript 里,你很可能会写 \d+。
using namespace std::regex_constants;
std::regex re_js(R"(\d+)", ECMAScript);
但如果你切到 POSIX 风格,\d 这种写法可能就不认了。
你得写得更“字符集”一点。
using namespace std::regex_constants;
std::regex re_posix(R"([[:digit:]]+)", extended);
再比如“匹配一个单词”。
std::regex re_word(R"(\w+)");
\w 在很多流派里都挺常见。
但你要换到偏 POSIX 的语法时,通常就得写成 [A-Za-z0-9_] 这种样子。
你不用现在就把它背下来。 你只要形成一个条件反射: 正则写法不统一。 遇到“不生效”,先想是不是语法流派不一样。
我们当时都怎么活下来的
最常见的办法,是手写解析。
一开始很朴素:strstr 找分隔符,sscanf 抠数字。
能跑就行。
但需求会长,日志格式会变。 你会慢慢写出一坨“半正则”的代码。 它不是不能用,就是每次改完都心虚。
另一条路,是用第三方正则库。 它们往往更强,也更像你在别的语言里见过的那套。 代价也很现实:你得把它带进工程,部署时得有人负责。
线上啪一下:我只是想从日志里抠个日期
假设你写了个小服务,每天晚上跑个任务读日志。 你只想统计一下“某天发生了多少次错误”。
上线第一天还好。 第二天 CPU 直接顶满。 你回头看提交记录,心里一凉:最近只改了一行,用了正则。
#include <regex>
#include <string>
bool has_date(const std::string& s) {
std::regex re(R"(\d{4}-\d{2}-\d{2})");
return std::regex_search(s, re);
}
这段代码的问题,不在“能不能匹配”。
而在于你每调用一次,都在重新构建 std::regex。
正则对象不是字符串。
它通常会被编译成内部状态机。
编译这一步,可能很贵。
把它放进循环里,你就等着挨打。
std::regex re(R"(\d{4}-\d{2}-\d{2})");
bool has_date_fast(const std::string& s) {
return std::regex_search(s, re);
}
先把 std::regex 提到外面。
很多“线上啪一下”,到这里就先止血了。
C++11 说:行,给你一个标准接口
std::regex 的价值,首先是“它是标准的”。
你不再需要在项目里到处讲:这台机器装没装某个库。
你也不必为了一段匹配逻辑,跟链接器对线半小时。
最常用的三个动作是:构建、search、match。
std::regex re(R"(\d{4}-\d{2}-\d{2})");
这行是“编译正则”。 你给它一个模式串,它内部会做准备工作。 你可以把它理解成:先把这段规则“整理好”,后面反复用。
它内部怎么整理的,你不需要背。 大概感觉是:把一堆字符规则,变成一套“读到这里该怎么走”的流程。 你也可以把它粗暴地理解成一种“状态机”(根据当前读到的字符,决定下一步去哪)。
std::smatch m;
if (std::regex_search(s, m, re)) {
auto date = m[0].str();
}
std::regex_search 是“在字符串里找一段能匹配的子串”。
std::smatch 里会存匹配结果,m[0] 是整个命中的那段。
如果你写过一点点 scanf,可以把它想成“更灵活的匹配 + 把命中的片段拿回来”。
if (std::regex_match(s, re)) {
}
std::regex_match 更严格,它要求“整串都符合”。
search 是“找一段”,match 是“整段”。
别搞反。
先把最容易皱眉的点讲明白:转义和 R"(...)"
正则里有“转义”。 C++ 字符串里也有“转义”。 两个叠在一起的时候,你就会看到双倍的反斜杠。
std::regex re1("\\d+");
这里的 \\d 不是你打多了。
它的意思是:C++ 先把 \\ 变成一个 \,然后正则再把 \d 当成“数字”。
std::regex re2(R"(\d+)");
这就是原始字符串字面量(raw string literal)。 你可以简单理解成:里面基本不认 C++ 的转义,所以写正则舒服很多。
例子 1:search vs match,别靠感觉
std::regex re(R"(\d{4}-\d{2}-\d{2})");
std::string a = "2026-01-18";
std::string b = "date=2026-01-18";
bool x = std::regex_match(a, re);
bool y = std::regex_match(b, re);
bool u = std::regex_search(a, re);
bool v = std::regex_search(b, re);
match 是“整串必须长得像正则”。
search 是“在里面找一段符合的”。
日志这种东西,大多是 search。
例子 2:捕获组怎么取,m[1] 是谁
std::regex re(R"((\d{4}-\d{2}-\d{2}).*level=(\w+))");
std::string s = "2026-01-18 level=ERROR msg=boom";
std::smatch m;
if (std::regex_search(s, m, re)) {
auto date = m[1].str();
auto level = m[2].str();
}
这里的 () 叫“捕获组”。
你可以把它理解成:我不仅要匹配,我还想把其中几段顺手拿出来。
m[0] 是整段命中的子串。
m[1] 开始才是你括号里抓到的内容。
例子 3:一行里不止一个命中,怎么全找出来
std::regex re(R"(\d+)");
std::string s = "id=12 cost=34";
for (std::sregex_iterator it(s.begin(), s.end(), re), end; it != end; ++it) {
auto num = it->str();
}
这就是“找多个匹配”的常用写法。 你不用自己移动下标。 迭代器会帮你从前往后找。
例子 4:替换比你想的更常见
std::regex re(R"(password=[^\s]+)");
std::string s = "user=tom password=123456";
auto masked = std::regex_replace(s, re, "password=***");
这类场景很实际。 脱敏。 打日志。 别把密码直接印出去。
例子 5:正则写错了怎么办
try {
std::regex re("(");
} catch (const std::regex_error&) {
}
std::regex 构造时可能抛异常。
如果你的正则来自配置文件或用户输入,这个点一定要记住。
例子 6:大小写不敏感,别自己手动 tolower
using namespace std::regex_constants;
std::regex re(R"(error)", icase);
std::string s = "ERROR: boom";
bool ok = std::regex_search(s, re);
icase 的意思是忽略大小写。
你不用先把字符串全转小写。
也不用写两个分支去匹配 ERROR/error。
例子 7:按正则切分字符串
std::regex sep(R"([,;\s]+)");
std::string s = "a, b; c";
for (std::sregex_token_iterator it(s.begin(), s.end(), sep, -1), end; it != end; ++it) {
auto token = it->str();
}
这就是“split”。
最后那个 -1 的意思是:我要的是分隔符之间的内容。
你可以把它当成“正则版的分割”。
让你吓一跳的性能坑:回溯和最坏情况
有些正则会在某些输入上突然慢得离谱。 不是因为你的机器差。 是因为它在内部做了太多次“试错”。
std::regex re(R"((a+)+$)");
std::string s(28, 'a');
s.push_back('b');
bool ok = std::regex_match(s, re);
这段正则看着很无辜。 但它包含了“重复里面套重复”。
当匹配失败时,一些实现会不断回头换一种切法再试。 这就叫回溯。 输入稍微长一点,尝试次数可能爆炸。
你不需要背这个例子。 你只要记住一个工程直觉: 正则不是免费的。 尤其是看起来“很聪明”的那种。
坑 1:你以为在用语言特性,其实在用某个实现
std::regex 是标准接口,但底层怎么实现,取决于你的标准库。
也就是:你用的编译器/平台带的那套实现。
同一段正则,在不同实现里,性能可能差一个数量级。
这不是“你写得不够聪明”。 这是工程事实:接口统一了,性能没法统一。 历史上也确实有人踩过这种坑。
正则可以用。
但别把它当热路径上的“常规武器”。
顺手提一句。
如果你知道这个正则会反复使用,可以试试 std::regex_constants::optimize。
它不保证起飞。
但至少告诉读代码的人:你是有意识地在乎性能的。
std::regex re(R"(\d{4}-\d{2}-\d{2})", std::regex_constants::optimize);
这行的意思是:让实现尽量为“多次匹配”做优化。 它不保证一定变快,但至少让你在代码里把意图说清楚。
如果你要在百万行文本上跑它。 先测。 别凭感觉。
坑 2:regex 不是 parser
你会很自然地想:既然能匹配,那能不能“解析”更复杂的结构。 比如带嵌套的括号、JSON、HTML。 然后你会遇到一种痛:它不是“写不出来”,而是“写出来也不敢改”。
有人吐槽过一句话,很刻薄,但很真实:
“Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.”
std::regex re(R"(\{.*\})");
std::string s = "{ a: { b: 1 } }";
std::smatch m;
std::regex_search(s, m, re);
这段看起来能跑。
但一旦输入里出现换行、转义、字符串里的 }。
你就开始补丁套补丁。
最后你会发现:bug 不是出在业务逻辑。 而是出在“正则读不懂”。 你甚至很难给它写单元测试,因为你自己也不确定“应该怎么匹配才算对”。
这不是你的问题。 是正则这门工具的边界:它擅长的是模式匹配,不擅长层级结构。
这里的 parser(解析器)你可以简单理解成:按字符读输入,然后自己一步步决定“这是对象开始了/结束了/这是一个字段”。 它听起来更麻烦,但逻辑往往更直白,也更可维护。
一句话成段的结论
能用正则解决的问题,很多时候也能用一段更直白的解析代码解决。
横向对比:什么时候该用 std::regex
如果你只是写个小工具,输入规模不大,偶尔跑一次。
std::regex 很合适。
它省事,也省沟通成本。
如果你在热路径上跑,每秒要处理很多字符串。 或者你写的正则看起来已经“像一门语言”。 那就先测,再决定。
再往后一步,有些团队会选择第三方库。 原因通常不玄学:要么更快,要么更可控。 比如有的库会刻意限制某些特性,来换取“不会突然卡死”的时间上限。
而手写解析也不是丢人的旧办法。 当格式稳定、规则简单、性能敏感时。 一段清清楚楚的扫描代码,可能比一段正则更好维护。
小洞见:正则像是在写“微型程序”
你以为你写的是一个字符串。 其实你写的是一段小程序。 它可能有分支、有回溯(走错路就倒回去重试)、也有最坏情况。
所以我对 std::regex 的态度一直很朴素:小规模、偶尔用,很舒服。
真要上热路径、上大规模输入,先测一次,再决定要不要让这段“微型程序”上生产。
别让一段正则,悄悄变成你的线上事故生成器。