C++ 正则表达式完全指南:从 std::regex 实战到 RE2 引擎原理(NFA/DFA/回溯陷阱)

第 1 章 正则表达式到底是什么?——先建立直觉

1.1 一个生活化的类比

想象你在一个巨大的图书馆里找书。如果只知道书名里的几个字,你不会逐本翻完所有书,而是会用"检索规则":

  • "书名以《C++ 开头"
  • "书名包含 网络 两个字"
  • "书名以 入门 结尾"

正则表达式(Regular Expression,简称 regex / regexp)就是这种"检索规则"的标准化写法。它用一串特殊字符组成的"模式(pattern)",描述"什么样的文本算命中",然后让引擎去文本里找。

它就像文本世界的"模糊匹配搜索模板":

生活中的说法正则写法含义
"以 C++ 开头"^C\+\+从行首开始匹配 C++
"包含任意数字"\d匹配 0-9 中任意一个数字
"连续 3 个字母"[a-z]{3}匹配小写字母连续出现 3 次
"以 入门 结尾"入门$匹配到行尾的"入门"

1.2 正则能做什么(能力地图)

  1. 匹配(Match):判断一段文本是否符合某个格式(如"这是不是一个合法邮箱")。
  2. 查找(Search):在一大段文本里找出所有满足条件的位置(如"找出日志里所有 IP 地址")。
  3. 提取(Extract):把命中部分的子片段抓出来(如"从 URL 里提取域名")。
  4. 替换(Replace):把命中部分替换成别的内容(如"把手机号中间四位打码")。
  5. 切分(Split):按模式把文本切成多段。

1.3 正则的两个本质概念(务必先记住)

  • 模式(pattern):你写的正则字符串,如 \d{4}-\d{2}-\d{2}(匹配日期)。
  • 引擎(engine):真正拿着模式去扫描文本的程序。C++ 里最常见引擎有三个:std::regex(标准库自带)、RE2(Google 出品)、PCRE2(Perl 兼容)。第 2 章对比。

类比:模式是"菜谱",引擎是"厨师"。同样的菜谱,不同的厨师做出来速度、能力都不一样——这正是后面性能差异的根源。


第 2 章 C++ 正则库全景:std::regex / RE2 / PCRE2 怎么选?

2.1 三大引擎一句话介绍

引擎出处一句话定位最突出的优点最突出的缺点
std::regexC++ 标准库(C++11 引入)零依赖、随手可用不用装任何东西,跨平台各编译器实现差异大,性能普遍较慢,部分语法支持不全
RE2Google 开源安全优先的高性能引擎线性时间匹配,永不回溯爆炸,适合不可信输入不支持反向引用、环视等"高级回溯特性"
PCRE2Perl 兼容正则社区功能最全、兼容 Perl支持几乎全部语法(回溯、环视、递归),带 JIT 加速存在灾难性回溯风险,需小心写模式

2.2 怎么选(决策树)

你的正则需要反向引用 / 环视 / 递归匹配 吗?
├─ 需要 → PCRE2(或 Boost.Regex)
├─ 不需要,但输入来自用户 / 网络 / 日志等不可信数据?
│   ├─ 是 → 优先 RE2(防 ReDoS 攻击)
│   └─ 否 → 看性能要求:
│       ├─ 高吞吐、频繁匹配 → RE2
│       └─ 低频、图省事、不想引第三方库 → std::regex

2.3 std::regex 的"三套实现"问题(重要!)

std::regex 是标准规定接口、编译器各自实现的。同一个正则,在三个主流编译器下行为、性能差异很大:

编译器/标准库实现来源已知特点
GCC(libstdc++)自研速度慢(历史包袱),部分语法支持不全(如 \d 在 ECMAScript 模式下早期版本不支持,新版已修复)
Clang(libc++)自研支持较好,性能一般
MSVC(Windows)自研支持较好,性能一般

⚠️ 易错点:同一份代码在 Linux 上编译运行正常,换到 Windows 可能行为不同——因为底层的"厨师"换了。写跨平台代码时,正则功能请按"三者的公共子集"来写,或者直接用 RE2/PCRE2 这类库消除差异。

2.4 本篇的路线图

  • 第 3~6 章:用 std::regex 学会正则的核心用法(零依赖、立刻能跑)。
  • 第 7~8 章:深入引擎原理,理解"为什么有的正则会卡死"。
  • 第 9~10 章:接入 RE2 和 PCRE2,在生产环境选对工具。

第 3 章 快速上手:你的第一个 std::regex 程序

3.1 第 1 步:包含头文件

正则功能在 <regex> 头文件里。在代码开头加上:

#include <regex>   // 正则表达式的全部功能都在这
#include <string>  // std::string 字符串
#include <iostream>// 控制台输出

3.2 第 2 步:写一个最简单的匹配程序

目标:判断字符串 "hello123" 里是否包含数字。

#include <regex>
#include <string>
#include <iostream>

int main() {
    // 1) 待搜索的文本
    std::string text = "hello123";

    // 2) 构造正则对象:\d 表示"任意一个数字"
    //    注意:C++ 字符串里反斜杠要写成 \\,所以正则里的 \d 要写成 "\\d"
    std::regex pattern("\\d");

    // 3) regex_search:在 text 中搜索是否有子串能匹配 pattern
    bool found = std::regex_search(text, pattern);

    // 4) 输出结果
    if (found) {
        std::cout << "找到了数字!" << std::endl;
    } else {
        std::cout << "没有数字" << std::endl;
    }
    return 0;
}

编译运行(Linux/macOS,g++ 编译器):

g++ -std=c++17 demo.cpp -o demo
./demo
# 输出:找到了数字!

Windows 用 MSVC:直接新建控制台程序粘贴代码,F5 运行即可。

3.3 第 3 步:把匹配到的内容"抓出来"

光知道"有没有"不够,我们还想知道匹配到了什么、在哪个位置

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "订单号是 AB12345,请核对。";

    // 模式:AB 后面跟 5 个数字
    std::regex pattern("AB\\d{5}");

    // std::smatch:存放匹配结果的"容器",s 表示 string 版本
    std::smatch match_result;

    if (std::regex_search(text, match_result, pattern)) {
        // match_result[0] 就是整个匹配到的字符串
        std::cout << "匹配到: " << match_result[0] << std::endl;
        // position() 返回匹配开始的位置(下标从 0 开始)
        std::cout << "起始位置: " << match_result.position() << std::endl;
    }
    return 0;
}

输出:

匹配到: AB12345
起始位置: 5

类比:std::smatch 像一个"快递包裹",[0] 是整件包裹,后面的 [1]、[2]…是里面分装的小盒子(捕获组,第 6 章细讲)。

3.4 第 4 步:掌握最常用的 4 个函数(先混个脸熟)

函数干什么的类比
std::regex_match整个字符串完全符合模式才算成功"身份证号是不是 18 位"
std::regex_search字符串里任意位置有符合模式的片段就算成功"这段话里有没有数字"
std::regex_replace把所有匹配到的片段替换成新内容"把手机号打码"
std::regex_iterator遍历所有匹配片段"把日志里每个 IP 都找出来"

⚠️ 易错点(新手 90% 会踩):regex_match 是"全文必须完全匹配",regex_search 是"包含即可"。很多人想用 match 找子串,结果永远返回 false,还以为正则写错了。第 5 章详细演示。


第 4 章 正则语法速览:字符、量词、分组、锚点

4.1 字符匹配:匹配"单个字符"的基本单元

写法含义示例能匹配
a普通字符,匹配它自己a"a"
.任意一个字符(换行除外)c.t"cat"、"cut"、"c+t"
\d任意一个数字(0-9)\d\d"42"、"07"
\D任意一个非数字\D"a"、"%"
\w任意一个"单词字符"(字母、数字、下划线)\w\w"ab"、"_9"
\W任意一个非单词字符\W" "、"-"、"。"
\s任意一个空白字符(空格、Tab、换行)a\sb"a b"
\S任意一个非空白字符\S"ab"
[abc]字符集合:a、b、c 中任意一个[aeiou]"a"、"e"
[a-z]字符范围:a 到 z 任意一个[0-9]"5"
[^abc]取反:不是 a、b、c 的任意字符[^0-9]"x"

类比:\d 相当于"数字通配符",[0-9] 是"明确列出 0 到 9"。\d 是 [0-9] 的便捷缩写。

4.2 量词:控制"重复几次"

写法含义示例能匹配
*0 次或多次ab*c"ac"、"abc"、"abbc"
+1 次或多次ab+c"abc"、"abbc"(不能匹配 "ac")
?0 次或 1 次ab?c"ac"、"abc"
{n}恰好 n 次\d{4}"2026"
{n,}至少 n 次\d{2,}"12"、"12345"
{n,m}n 到 m 次\d{2,4}"12"、"1234"

贪婪 vs 懒惰(重点)

  • 默认是贪婪(greedy):尽量多匹配。\d+ 匹配 "12345" 时会吃掉全部 5 个数字。
  • 在量词后加 ? 变成懒惰(lazy):尽量少匹配。\d+? 匹配 "12345" 时只吃 1 个数字。
#include <regex>
#include <iostream>

int main() {
    std::string text = "12345";
    std::smatch m;

    // 贪婪:\d+ 会尽可能多吃,结果整串 "12345"
    std::regex greedy("\\d+");
    std::regex_search(text, m, greedy);
    std::cout << "贪婪: [" << m[0] << "]" << std::endl;

    // 懒惰:\d+? 只吃最少,结果只有 "1"
    std::regex lazy("\\d+?");
    std::regex_search(text, m, lazy);
    std::cout << "懒惰: [" << m[0] << "]" << std::endl;

    return 0;
}

输出:

贪婪: [12345]
懒惰: [1]

⚠️ 易错点:想匹配"引号里的内容"时,".*" 贪婪会一路吃到最后一个引号;要用 ".*?" 才能吃到第一个引号就停。例:对 "a" and "b",".*" 匹配整个 "a" and "b",".*?" 只匹配 "a"。

4.3 分组与捕获:用小括号"打包"

写法含义示例
(abc)捕获组:把 abc 打包,可整体用量词,也可提取(ab)+ 匹配 "abab"
(?:abc)非捕获组:只打包不提取(省内存)同上,但不占捕获编号
\1反向引用:引用第 1 个捕获组匹配的内容(\w)\1 匹配 "aa"、"zz"

4.4 锚点与边界:锁定位置

写法含义示例
^字符串/行开头^hello 只匹配开头的 hello
$字符串/行结尾world$ 只匹配结尾的 world
\b单词边界(单词字符与非单词字符的分界)\bcat\b 匹配 "a cat" 里的 cat,不匹配 "category" 里的 cat
\B非单词边界\Bcat\B 匹配 "concatenate" 里的 cat

4.5 转义:特殊字符要"加反斜杠"

想匹配字面上的 .、*、(、\ 等特殊字符时,前面加 \:

// 想匹配 "3.14" 里的点号:点号是特殊字符,必须写成 \.
std::regex pattern("3\\.14");   // C++ 字符串里 \\ 表示一个反斜杠
想匹配的字面字符正则写法C++ 字符串写法
.\."\\."
*\*"\\*"
(\("\\("
\\\"\\\\"

⚠️ 易错点(C++ 特有,必考):C++ 字符串里反斜杠本身需要转义,所以正则里的每个 \,在 C++ 源码里都要写两个。正则 \d → C++ 写 "\\d";正则 \. → C++ 写 "\\."。忘了双写是新手最常见的编译/运行错误。C++11 起可以用原始字符串字面量 R"(...)" 少写一半反斜杠:

// 普通字符串写法(容易看花眼)
std::regex p1("\\d{4}-\\d{2}-\\d{2}");
// 原始字符串写法(推荐!反斜杠不用双写)
std::regex p2(R"(\d{4}-\d{2}-\d{2})");   // 和 p1 完全等价

4.6 语法风格:std::regex 默认用 ECMAScript

std::regex 支持多种语法风格,默认是 ECMAScript(和 JavaScript 正则最接近)。构造时可用第二个参数切换:

// 默认 ECMAScript
std::regex r1("\\d+");
// 显式指定 ECMAScript
std::regex r2("\\d+", std::regex::ECMAScript);
// POSIX 扩展语法(不同语法的 \d 等写法不同,一般用不到)
std::regex r3("[[:digit:]]+", std::regex::extended);
语法风格常量说明
std::regex::ECMAScript(默认)与 JavaScript 风格一致,功能最全
std::regex::basic / extendedPOSIX 风格,\d 这类缩写不支持,要用 [[:digit:]]
std::regex::awk / grep / egrep兼容经典 Unix 工具

⚠️ 易错点:POSIX 风格不支持 \d、\w、\s 缩写,要用 [[:digit:]]、[[:alpha:]] 等"字符类"写法。如果非要用 \d 却选了 extended,会编译出错或匹配失败。


第 5 章 四大核心 API 实战:match / search / replace / iterator

5.1 std::regex_match:全文严格匹配(格式校验)

使用场景:校验"整个输入是否符合格式"——身份证、手机号、版本号、日期。

#include <regex>
#include <string>
#include <iostream>

int main() {
    // 要校验的字符串
    std::string id = "110101200001011234";
    // 18 位身份证:17 位数字 + 最后一位数字或 X
    std::regex id_pattern(R"(\d{17}[\dXx])");

    // regex_match:整个字符串必须完全匹配
    if (std::regex_match(id, id_pattern)) {
        std::cout << "身份证格式合法" << std::endl;
    } else {
        std::cout << "身份证格式不合法" << std::endl;
    }

    // 反例:字符串里混了别的字符,match 会失败
    std::string bad = "身份证号是110101200001011234";  // 前面多了汉字
    if (std::regex_match(bad, id_pattern)) {
        std::cout << "(意外)匹配成功" << std::endl;
    } else {
        std::cout << "正确:多字导致全文匹配失败" << std::endl;
    }
    return 0;
}

5.2 std::regex_search:局部搜索(找片段)

使用场景:在文本中找"是否包含"、找"第一处匹配"。

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string log = "2026-08-17 10:23:45 [ERROR] connection timeout to 192.168.1.10:8080";
    // 找 IPv4 地址:4 组 1~3 位数字,用点分隔
    std::regex ip_pattern(R"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})");
    std::smatch m;

    if (std::regex_search(log, m, ip_pattern)) {
        std::cout << "找到 IP: " << m[0] << std::endl;                    // 192.168.1.10
        std::cout << "起始位置: " << m.position() << std::endl;           // 输出下标
    }
    return 0;
}

💡 提示:std::smatch 是 std::match_results<std::string::const_iterator> 的类型别名;如果操作的是 std::wstring(宽字符串),用 std::wsmatch。

5.3 std::regex_replace:批量替换

使用场景:数据脱敏、格式统一、清洗脏数据。

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "联系 13812345678 或 13987654321,我们会尽快回复。";

    // 手机号:1 开头 + 10 位数字
    // 替换成 "****",实现脱敏
    std::regex phone(R"(1\d{10})");
    std::string result = std::regex_replace(text, phone, "****");

    std::cout << result << std::endl;
    // 输出:联系 **** 或 ****,我们会尽快回复。
    return 0;
}

用 $1 保留捕获组做"部分保留"的替换(只打码中间四位):

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "我的手机是 13812345678";
    // 分组:(\d{3}) 前 3 位,(\d{4}) 中间 4 位,(\d{4}) 后 4 位
    // 替换串里 $1、$2、$3 分别引用第 1、2、3 个捕获组
    std::regex phone(R"((\d{3})(\d{4})(\d{4}))");
    std::string result = std::regex_replace(text, phone, "$1****$3");

    std::cout << result << std::endl;
    // 输出:我的手机是 138****5678
    return 0;
}

⚠️ 易错点:替换字符串里的 $ 是特殊符号,$& 表示"整个匹配到的内容",$1~$9 表示捕获组。如果想输出字面 $,要写成 $$。

5.4 std::regex_iterator 与 std::regex_token_iterator:遍历所有匹配

使用场景:一次性取出所有 IP、所有数字、所有日期。

写法 A:std::sregex_iterator(推荐,直观)

#include <regex>
#include <string>
#include <iostream>
#include <vector>

int main() {
    std::string log = "尝试连接 10.0.0.1,失败后连接 10.0.0.2,最后 172.16.5.9 成功。";
    std::regex ip_pattern(R"(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})");

    // 构造迭代器:begin 指向第一处匹配,end 是"结束哨兵"
    auto begin = std::sregex_iterator(log.begin(), log.end(), ip_pattern);
    auto end = std::sregex_iterator();   // 默认构造 = 结束位置

    std::vector<std::string> ips;
    for (auto it = begin; it != end; ++it) {
        // *it 是 smatch,[0] 是完整匹配串
        ips.push_back((*it)[0].str());
    }

    std::cout << "共找到 " << ips.size() << " 个 IP:" << std::endl;
    for (const auto& ip : ips) {
        std::cout << "  " << ip << std::endl;
    }
    return 0;
}

写法 B:std::sregex_token_iterator(想只取"捕获组内容"时很方便)

#include <regex>
#include <string>
#include <iostream>

int main() {
    // 场景:从 "name=alice&age=25&city=beijing" 中提取所有 value
    std::string query = "name=alice&age=25&city=beijing";
    // 只提取捕获组 1(等号后面的部分)
    std::regex kv(R"(=(\w+))");
    // 第二个参数 -1 表示"返回不匹配的部分",1 表示"返回第 1 个捕获组"
    auto begin = std::sregex_token_iterator(query.begin(), query.end(), kv, 1);
    auto end = std::sregex_token_iterator();

    std::cout << "提取到的 value:";
    for (auto it = begin; it != end; ++it) {
        std::cout << " " << *it;
    }
    std::cout << std::endl;
    // 输出:提取到的 value: alice 25 beijing
    return 0;
}

5.5 四种 API 选择速查

需求用哪个返回什么
整个字符串是否符合格式regex_matchbool(或通过 smatch 拿分组)
是否包含 / 第一处匹配在哪regex_searchbool + smatch(位置、内容)
把所有命中替换掉regex_replace新字符串
把每处命中都取出来regex_iterator逐段遍历
只要每处命中的某个分组regex_token_iterator逐段遍历(可指定分组)

第 6 章 捕获组进阶:反向引用与命名分组

6.1 捕获组是什么?为什么要用?

正则里用小括号 () 包起来的部分叫捕获组(capturing group)。它有两个作用:

  1. 整体打包:让量词作用于"一组字符"而不是单个字符。例如 (ab)+ 匹配 "abab"。
  2. 单独提取:引擎会把每个括号里匹配到的内容单独存一份,供后续使用。

类比:捕获组像"快递包裹里的分装袋"。m[0] 是整个包裹,m[1] 是第 1 个分装袋,m[2] 是第 2 个……编号从 1 开始,按左括号出现的顺序从左到右编号。

6.2 提取多组内容:解析日期

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "会议定于 2026-08-17 下午 3 点召开。";
    // 三个捕获组:年、月、日
    std::regex date_pattern(R"((\d{4})-(\d{2})-(\d{2}))");
    std::smatch m;

    if (std::regex_search(text, m, date_pattern)) {
        std::cout << "完整匹配: " << m[0] << std::endl;   // 2026-08-17
        std::cout << "年: " << m[1] << std::endl;         // 2026
        std::cout << "月: " << m[2] << std::endl;         // 08
        std::cout << "日: " << m[3] << std::endl;         // 17
    }
    return 0;
}

6.3 反向引用:匹配"重复出现的内容"

反向引用(backreference) 用 \1、\2 表示"这里必须和第 1、2 个捕获组刚才匹配到的内容一模一样"。

典型场景:找出成对重复的单词(打字重复错误)。

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "this is is a test test case";
    // (\w+) 匹配一个单词;\s+ 匹配中间空白;\1 要求再出现同一个单词
    std::regex dup_pattern(R"((\w+)\s+\1)");

    auto begin = std::sregex_iterator(text.begin(), text.end(), dup_pattern);
    auto end = std::sregex_iterator();

    std::cout << "重复单词:" << std::endl;
    for (auto it = begin; it != end; ++it) {
        // 完整匹配是 "is is" / "test test",第 1 组是单词本身
        std::cout << "  " << (*it).str() << " -> 重复的是: " << (*it)[1] << std::endl;
    }
    return 0;
}

输出:

重复单词:
  is is -> 重复的是: is
  test test -> 重复的是: test

⚠️ 易错点(重要!):反向引用是"回溯型引擎"的特性,RE2 不支持。如果你把上面这段代码换成 RE2,会得到编译错误:"invalid escape sequence" 或 "backreferences not supported"。这是第 8 章引擎原理的核心区别,先记住结论:要反向引用 → PCRE2/std::regex;要防卡死 → RE2

6.4 命名分组:给捕获组起名字(C++ 标准库不支持!)

命名分组写法引用方式
PCRE2(?<year>\d{4}) 或 (?'year'\d{4})\k<year>
RE2(?P<year>\d{4})(?P=year)
std::regex不支持(只能用编号 m[1]、m[2]…)

⚠️ 易错点:std::regex 没有命名分组语法。网上很多用 (?<name>...) 的 JavaScript 教程,直接抄进 C++ 会编译失败。需要命名分组请用 PCRE2 或 RE2。

6.5 非捕获组:(?:...) 只打包不提取

当小括号只是为了"整体加量词",不需要提取内容时,用 (?:...) 能省掉编号、略微提升性能、避免编号混乱。

// 需求:匹配 "abc" 或 "xyz" 连续出现 2 次
// 写法 A:用捕获组 (abc|xyz) 也能跑,但白白占用了 m[1]
std::regex a(R"((abc|xyz){2})");
// 写法 B:用非捕获组,不占编号(推荐)
std::regex b(R"((?:abc|xyz){2})");

第 7 章 性能陷阱:灾难性回溯(ReDoS)是怎么把程序卡死的

7.1 一个"看起来没问题"的卡死现场

下面这段代码,输入只有 30 个字符,但运行时间可能超过几秒甚至永远跑不完

#include <regex>
#include <string>
#include <iostream>

int main() {
    // 模式:若干 a 后面跟若干 a,再跟一个 b
    // 匹配目标:"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"(30 个 a,后面没有 b)
    std::regex pattern(R"(^(a+)+$)");
    std::string evil(30, 'a');   // 30 个 a,故意不匹配(没有 b)

    // 这句话可能要跑几十秒甚至几分钟!
    bool ok = std::regex_match(evil, pattern);
    std::cout << std::boolalpha << ok << std::endl;
    return 0;
}

把 30 改成 40,时间会暴涨到指数级。这就是著名的灾难性回溯(Catastrophic Backtracking),也叫 ReDoS(Regular expression Denial of Service,正则拒绝服务)

7.2 为什么会卡死?——三分钟看懂回溯

回溯(backtracking) 是回溯型引擎的"试错机制":

类比:你在一个岔路很多的迷宫里找出口。走一条路发现不通,就退回上一个岔路口换一条路再试。如果岔路口套岔路口,试错的组合数会爆炸。

^(a+)+$ 匹配 30 个 a 且没有 b 时,引擎的行为:

  1. 外层 (a+)+ 可以理解为"把 a 分成若干段",比如 [a][a][a]...、[aa][a][a]...、[aaa][a]...——有 2^30 种分法
  2. 每种分法都要试到末尾,发现没有 b 才失败,然后退回换下一种分法。
  3. 试完所有分法 ≈ 10 亿种组合 → 程序卡死。

嵌套量词 + 匹配失败 = 灾难。凡是出现 (x+)+、(x*)*、(x+)* 这类"量词套量词"的结构,配合无法匹配的输入,就可能指数爆炸。

7.3 经典危险模式清单

危险模式危险原因攻击输入示例
^(a+)+$外层 + 套内层 +一串 a 后跟别的字符
`^(aa)+$`多分支 + 量词
^(\w+\s?)*$量词套量词长串无空格的字符
^(.*,)*\d+$点号星号嵌套大量逗号+结尾非数字

7.4 如何防御

  1. 模式层面:消除"量词套量词"。(a+)+ 改成 a+(大多数场景等价)。
  2. 输入层面:限制输入长度;对不可信输入设置超时。
  3. 引擎层面用 RE2(线性时间,天然免疫回溯爆炸),这是最彻底的方案。
  4. 工具层面:上线前用 ReDoS 检测工具(如 safe-regex、vuln-regex-detector)扫描模式。

⚠️ 易错点:std::regex 无法设置"匹配超时",一旦遇到灾难性回溯就是真·卡死。生产环境如果正则处理用户输入,强烈建议换成 RE2(第 9 章)。


第 8 章 引擎原理深度解析:NFA / DFA / 回溯 / RE2 的线性时间魔法

本章是本篇的"深度"核心。如果你只想用库,可以跳到第 9 章;如果想理解"为什么 RE2 安全、PCRE2 功能全、std::regex 慢",请务必读完。

8.1 正则引擎的两种基本流派

所有正则引擎本质上分两类:

流派代表匹配方式时间复杂度支持反向引用/环视
回溯型(Backtracking NFA)PCRE、Perl、Java、Python re、std::regex(多数实现)深度优先试错,一条路走到黑再回头最坏指数级✅ 支持
自动机型(DFA / NFA 模拟)RE2、grep、awk 部分实现、Rust regex同时跟踪所有可能状态,一次扫描推进线性 O(n)❌ 不支持

8.2 从正则到自动机:模式是如何"变成机器"的

要理解自动机型引擎,先认识两个概念:

NFA(非确定有限自动机,Nondeterministic Finite Automaton)

类比:一张"地铁换乘图"。从起点站出发,每到一站,如果当前字符满足条件就前进;NFA 的"非确定"体现在:同一时刻可能有多条路线同时可选。

把正则 a(b|c)d 翻译成 NFA:

start --a--> [1] --b--> [2] --d--> [accept]
                 \--c--> [3] --d--/

DFA(确定有限自动机,Deterministic Finite Automaton)

类比:把"地铁换乘图"提前改造成"每条路只有一个方向的单向高架",每站只有一条确定的路可走。DFA 每个状态读一个字符只产生一个确定的下一个状态,所以匹配速度极快,但状态可能很多(最坏指数级膨胀)。

8.3 Thompson 构造法:正则 → NFA 的机械翻译

1968 年 Ken Thompson(Unix 之父)提出:任何正则都能机械地翻译成 NFA,只需 6 种基本构件拼装:

正则片段NFA 构件
单个字符 a两个状态,中间一条标着 a 的边
连接 ab两个子 NFA 首尾相接
并集 `ab`
星号 a*加一条"跳过"边形成环
加号 a+a 的星号外面再强制走一次 a
问号 a?加一条"跳过"边

这套构造法的意义:任何正则都能在 O(长度) 时间内变成 NFA,之后在 NFA 上做匹配就有章可循了。

8.4 三种引擎的工作方式对比

① 回溯型引擎(PCRE/Java/Python re/std::regex 多数实现)

  • 深度优先方式在 NFA 上"探路":优先沿第一条边走,走到死胡同就回头试另一条。
  • 优点:天然支持反向引用(\1)、环视((?=...))、懒惰量词等"需要记录试错路径"的高级特性。
  • 缺点:路径组合指数增长 → 灾难性回溯。

② 传统 DFA 引擎(老式 grep 等)

  • 先用"子集构造法"把 NFA 变成 DFA(一次性预处理,状态爆炸是它的代价),然后每个字符只查一次表
  • 优点:匹配本身极快、稳定线性。
  • 缺点:预处理可能消耗大量内存;DFA 状态一多就爆。

③ NFA 模拟引擎(RE2 采用)——兼顾两者

RE2 的巧妙之处:不把 NFA 变成 DFA,而是"同时模拟 NFA 上的所有可能状态"

类比:回溯引擎是"一个人带一个笔记本,走一条路记一条笔记,走不通翻回去改笔记";RE2 是"带了一支军队,每个岔路口分一队人,各走各的,最后看有没有队伍到达终点"。军队人数 = NFA 状态数,是固定的,不会因为输入变长而爆炸。

实现手段叫 Pike VM(Rob Pike 发明的虚拟机):

  • 引擎维护一个状态列表,初始只含起始状态。
  • 每读一个字符,就计算"当前所有可达状态在吃下这个字符后,能到哪些新状态",得到新的状态列表。
  • 一个字符只做一次"全体状态转移",所以总代价 = 输入长度 × 状态数 = O(n × m),m 是正则长度,可视为常量,即 线性时间
输入:  a  b  c  d
状态集: {start} → {1} → {2} → {3} → {accept}
(每步并行推进所有可能状态,不会回头重试)

8.5 为什么 RE2 不支持反向引用和环视?

  • 反向引用要求"记住之前匹配的具体文本再回去比对",每个候选文本都产生新的路径,状态数随输入爆炸——破坏了"状态数固定"的线性保证
  • 环视((?=...)、(?<=...))要求"在不消耗字符的情况下向前/向后试探",同样需要回溯试错。
  • RE2 的设计哲学:宁可牺牲这些功能,也要保证最坏情况线性时间。因为它的目标场景是"处理不可信的、海量的输入"(Google 内部每天处理万亿级请求)。

8.6 性能对比实测思路

如果你想亲自验证三种引擎的差距,可以写一个基准程序:对同一段文本(如 1MB 日志)重复匹配 1000 次,分别统计 std::regex、RE2、PCRE2(可开 JIT)的耗时。经验结论大致如下(具体数值取决于编译器与机器):

引擎简单模式(\d+)复杂模式((a+)+ 类)危险输入下的表现
std::regex(libstdc++)慢(数倍于 RE2)可能卡死
PCRE2较快(JIT 后极快)可能卡死
RE2稳定线性,永不卡死

8.7 引擎原理总结表

维度回溯型(PCRE2/std::regex)NFA 模拟型(RE2)
匹配策略深度优先 + 回溯试错并行模拟所有状态
最坏时间复杂度指数级线性
反向引用
环视(lookaround)
懒惰量词✅(RE2 支持,语义等价实现)
内存占用取决于回溯深度与 NFA 状态数成正比(可控)
典型用途复杂文本处理、编辑器海量日志、不可信输入、安全过滤

第 9 章 RE2 实战:如何接入并写出高性能匹配代码

9.1 第 1 步:安装 RE2

RE2 是 Google 开源 C++ 库(GitHub: google/re2,BSD 许可)。安装方式:

# Ubuntu / Debian
sudo apt install libre2-dev

# macOS(Homebrew)
brew install re2

# Windows:用 vcpkg
vcpkg install re2
# 或 vcpkg install re2:x64-windows

CMake 接入:

find_package(re2 REQUIRED)
target_link_libraries(your_target PRIVATE re2::re2)

9.2 第 2 步:基本匹配(compile + match)

RE2 的用法核心:先 RE2::FullMatch(全文匹配)或 RE2::PartialMatch(部分匹配)。

#include <re2/re2.h>   // RE2 头文件
#include <string>
#include <iostream>

int main() {
    std::string text = "订单 AB12345 已发货";

    // RE2 模式:AB 后跟 5 位数字
    re2::RE2 pattern(R"(AB\d{5})");

    // PartialMatch:查找 text 中是否有匹配片段(类似 regex_search)
    if (RE2::PartialMatch(text, pattern)) {
        std::cout << "找到订单号模式" << std::endl;
    }

    // 提取捕获组:把匹配到的数字部分取出来
    std::string order_no;
    re2::RE2 extract(R"(AB(\d{5}))");
    if (RE2::PartialMatch(text, extract, &order_no)) {
        // 第 1 个捕获组内容会写入 order_no
        std::cout << "订单号 = " << order_no << std::endl;
    }
    return 0;
}

💡 说明:RE2 里 PartialMatch 对应 std::regex 的 regex_search,FullMatch 对应 regex_match。

9.3 第 3 步:批量匹配(RE2::Set)

一个常见需求:一次拿几百个模式,看文本命中哪些。RE2 提供 RE2::Set 批量优化:

#include <re2/re2.h>
#include <string>
#include <iostream>
#include <vector>

int main() {
    // 1) 创建 Set,指定匹配方式(RE2::UNANCHORED = 部分匹配)
    re2::RE2::Set set(re2::RE2::UNANCHORED);

    // 2) 注册多个模式,返回各自的索引
    int idx_ip   = set.Add(R"(\d+\.\d+\.\d+\.\d+)", nullptr);
    int idx_mail = set.Add(R"(\w+@\w+\.\w+)", nullptr);
    int idx_url  = set.Add(R"(https?://\S+)", nullptr);

    // 3) 编译(编译失败返回 false)
    if (!set.Compile()) {
        std::cerr << "编译失败" << std::endl;
        return 1;
    }

    // 4) 一次性匹配整段文本,命中的模式索引写入 matches
    std::string log = "访问 https://example.com 的用户 ip 是 10.0.0.8";
    std::vector<int> matches;
    if (set.Match(log, &matches)) {
        std::cout << "命中 " << matches.size() << " 条规则:" << std::endl;
        for (int idx : matches) {
            if (idx == idx_ip)   std::cout << "  - IP 规则" << std::endl;
            if (idx == idx_mail) std::cout << "  - 邮箱规则" << std::endl;
            if (idx == idx_url)  std::cout << "  - URL 规则" << std::endl;
        }
    }
    return 0;
}

类比:RE2::Set 像"一次刷一整张安检清单"——所有模式合并成一台机器,扫描一遍文本同时检查所有规则,比逐个模式匹配快得多。

9.4 第 4 步:常用安全参数

RE2 默认就带保护,还可以显式设置上限:

#include <re2/re2.h>
#include <iostream>

int main() {
    re2::RE2::Options opt;
    // 1) 限制内存使用(默认 8MB):防止恶意超长正则耗尽内存
    opt.set_max_mem(64 * 1024 * 1024);          // 64MB
    // 2) 限制"一个字符可匹配的字节数",防御 UTF-8 边界攻击
    opt.set_one_line(false);
    // 3) 设置日志级别,匹配错误时静默(不刷屏)
    re2::RE2::Options::Logging log_opt;
    opt.set_log_errors(false);

    re2::RE2 pattern(R"((a+)+b)", opt);
    if (!pattern.ok()) {
        std::cerr << "模式编译失败: " << pattern.error() << std::endl;
        return 1;
    }
    std::cout << "模式编译成功" << std::endl;
    return 0;
}

9.5 RE2 与 std::regex API 对照表

功能std::regexRE2
构造std::regex p(s)re2::RE2 p(s)
全文匹配regex_matchRE2::FullMatch
部分匹配regex_searchRE2::PartialMatch
遍历所有命中sregex_iteratorre2::StringPiece + 循环 PartialMatch(用 Rewrite 或手写偏移推进)
批量替换regex_replaceRE2::GlobalReplace / RE2::Replace
批量多模式手动循环RE2::Set

批量替换示例:

#include <re2/re2.h>
#include <string>
#include <iostream>

int main() {
    std::string text = "ip: 10.0.0.1, ip: 172.16.0.9";
    // GlobalReplace:替换所有命中;\\d 在 RE2 的替换串里也是 \\d 形式
    re2::RE2::GlobalReplace(&text, R"(\d+\.\d+\.\d+\.\d+)", "[IP]");
    std::cout << text << std::endl;
    // 输出:ip: [IP], ip: [IP]
    return 0;
}

第 10 章 PCRE2 与 JIT:需要完整回溯能力时的选择

10.1 PCRE2 是什么

PCRE2(Perl Compatible Regular Expressions, version 2) 是兼容 Perl 正则语法的库,功能最全:反向引用、环视、递归、条件分支、原子组、占有量词都有。很多著名软件(PHP、R、Apache、PostgreSQL)都在用它。

10.2 什么时候需要 PCRE2

  • 你需要反向引用(如"找重复单词")。
  • 你需要环视((?<=...) 后向断言、(?=...) 前向断言)。
  • 你需要递归匹配(如匹配成对括号 \((?:[^()]|(?R))*\))。
  • 你确认输入可信 / 已限制长度 / 模式经过审查。

10.3 PCRE2 的 JIT(Just-In-Time 编译)

PCRE2 能把正则编译成机器码(JIT),匹配速度比解释执行快数倍。启用方式:

#include <pcre2.h>
#include <string>
#include <iostream>

int main() {
    // 1) 编译模式(UTF 模式可加 PCRE2_UTF)
    int errcode = 0;
    PCRE2_SIZE erroffset = 0;
    std::string pattern_str = R"(\d{4}-\d{2}-\d{2})";
    pcre2_code* re = pcre2_compile(
        reinterpret_cast<PCRE2_SPTR>(pattern_str.c_str()),
        pattern_str.size(),
        0,                // 选项:0 表示默认
        &errcode, &erroffset, nullptr);

    if (re == nullptr) {
        std::cerr << "编译失败,错误码 " << errcode << std::endl;
        return 1;
    }

    // 2) 启用 JIT(可选,大幅提速)
    int jit_ret = pcre2_jit_compile(re, PCRE2_JIT_COMPLETE);
    if (jit_ret != 0) {
        std::cout << "JIT 不可用,退回解释执行" << std::endl;
    }

    // 3) 创建匹配上下文
    pcre2_match_data* md = pcre2_match_data_create_from_pattern(re, nullptr);

    // 4) 执行匹配
    std::string text = "今天日期 2026-08-17";
    int rc = pcre2_match(re,
        reinterpret_cast<PCRE2_SPTR>(text.c_str()),
        text.size(), 0, 0, md, nullptr);

    if (rc >= 0) {
        // 取第 0 组(完整匹配)的起止位置
        PCRE2_SIZE* ovector = pcre2_get_ovector_pointer(md);
        std::cout << "匹配成功,长度 " << (ovector[1] - ovector[0]) << std::endl;
    } else {
        std::cout << "未匹配(返回码 " << rc << ")" << std::endl;
    }

    pcre2_match_data_free(md);
    pcre2_code_free(re);
    return 0;
}

⚠️ 使用 PCRE2 一定要意识到:它和 std::regex 一样是回溯型引擎,同样存在灾难性回溯风险。JIT 只是加速,不改变算法复杂度。第 7 章的 ^(a+)+$ 在 PCRE2 下同样会卡死。

10.4 三个引擎终极对比表

特性std::regexRE2PCRE2
需要安装❌(标准库自带)
跨平台行为一致性❌(三套实现差异)
反向引用 \1
环视 (?=...)✅(部分实现)
命名分组✅ (?P<name>)✅ (?<name>)
最坏时间指数线性指数
抗 ReDoS❌(可配超时)
JIT 加速❌(本身已快)
典型场景快速原型、简单校验生产环境高吞吐 / 不可信输入复杂语法、文本编辑器

💡 如果既想要 RE2 的安全,又需要个别回溯特性,RE2 官方文档的建议是:先尽量用 RE2 能表达的模式;确需回溯特性时再引入 PCRE2,并对输入长度和匹配时间做双重限制。


第 11 章 实战案例:日志解析 / 输入校验 / 数据脱敏 / 批量提取

本章四个案例都基于 std::regex(零依赖可直接编译),第 11.5 节给出"生产环境替换成 RE2"的等价写法提示。

11.1 案例一:日志解析——提取访问日志里的 IP、时间、状态码

#include <regex>
#include <string>
#include <iostream>
#include <fstream>

int main() {
    // 模拟一行 Nginx/通用访问日志
    std::string line = "192.168.1.55 - - [17/Aug/2026:10:23:45 +0800] \"GET /api/user HTTP/1.1\" 200 1024";

    // 分组依次是:IP、日期时间、请求方法、路径、协议、状态码、字节数
    std::regex log_pattern(
        R"((\d+\.\d+\.\d+\.\d+).*?\[([^\]]+)\].*?\"(\w+)\s+(\S+)\s+([^\"]+)\"\s+(\d{3})\s+(\d+))");
    std::smatch m;

    if (std::regex_search(line, m, log_pattern)) {
        std::cout << "IP:     " << m[1] << std::endl;
        std::cout << "时间:   " << m[2] << std::endl;
        std::cout << "方法:   " << m[3] << std::endl;
        std::cout << "路径:   " << m[4] << std::endl;
        std::cout << "协议:   " << m[5] << std::endl;
        std::cout << "状态码: " << m[6] << std::endl;
        std::cout << "字节数: " << m[7] << std::endl;
    }
    return 0;
}

💡 技巧:日志行字段间用 .*?(懒惰匹配)跳过分隔内容,既直观又不易误伤;但注意大量 .*? 会稍慢,生产环境可换成更精确的字符类(如 [^\s]+)。

11.2 案例二:输入校验——邮箱 / URL / 手机号

#include <regex>
#include <string>
#include <iostream>

// 简单但实用的校验函数封装
bool is_valid_email(const std::string& s) {
    // 简化版邮箱:用户名@域名(不追求 100% 严谨,够日常校验用)
    static const std::regex email(R"(^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$)");
    return std::regex_match(s, email);
}

bool is_valid_url(const std::string& s) {
    // http/https + 域名 + 可选路径
    static const std::regex url(R"(^https?://[\w.-]+(:\d+)?(/\S*)?$)");
    return std::regex_match(s, url);
}

bool is_valid_phone(const std::string& s) {
    // 中国大陆手机号:1 开头,第二位 3-9,共 11 位
    static const std::regex phone(R"(^1[3-9]\d{9}$)");
    return std::regex_match(s, phone);
}

int main() {
    std::cout << std::boolalpha;
    std::cout << "a@b.com         -> " << is_valid_email("a@b.com") << std::endl;
    std::cout << "a@b             -> " << is_valid_email("a@b") << std::endl;
    std::cout << "https://x.com/a -> " << is_valid_url("https://x.com/a") << std::endl;
    std::cout << "13812345678     -> " << is_valid_phone("13812345678") << std::endl;
    std::cout << "23812345678     -> " << is_valid_phone("23812345678") << std::endl;
    return 0;
}

输出:

a@b.com         -> true
a@b             -> false
https://x.com/a -> true
13812345678     -> true
23812345678     -> false

⚠️ 说明:生产环境请把校验正则放在 static const 里复用(避免每次构造对象),如上面代码所示;并在高并发路径优先考虑 RE2。

11.3 案例三:数据脱敏——身份证、手机号、卡号打码

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text =
        "用户张三 身份证 110101199003071234,手机 13812345678,"
        "银行卡 6222021234567890123。";

    // 身份证:保留前 4 后 4
    text = std::regex_replace(text, std::regex(R"(\d{17}[\dXx])"), "$&");
    // 用更精确方式:分组打码
    text = std::regex_replace(text,
        std::regex(R"((\d{6})\d{8}(\d{4}))"),  // 身份证:前6 + 中间8 + 后4
        "$1********$2");

    // 手机号:保留前 3 后 4
    text = std::regex_replace(text,
        std::regex(R"((\d{3})\d{4}(\d{4}))"),
        "$1****$2");

    // 银行卡:保留前 4 后 4
    text = std::regex_replace(text,
        std::regex(R"((\d{4})\d{11}(\d{4}))"),
        "$1***********$2");

    std::cout << text << std::endl;
    // 输出:用户张三 身份证 110101********1234,手机 138****5678,银行卡 6222***********0123。
    return 0;
}

⚠️ 易错点:手机号正则 (\d{3})\d{4}(\d{4}) 会误伤身份证前 11 位(身份证也是数字串)。脱敏的稳妥顺序:先处理更长/更特殊的类型(身份证、银行卡),再处理手机号;更严谨的做法是先分词再逐词判断。

11.4 案例四:批量提取——从文本中取出所有数字、所有日期

#include <regex>
#include <string>
#include <iostream>
#include <vector>

int main() {
    std::string text = "价格 12.5 元,数量 3 件,日期 2026-08-17 发货,积分 8888。";

    // 1) 提取所有整数
    std::regex int_re(R"(\b\d+\b)");
    std::vector<std::string> ints;
    for (auto it = std::sregex_iterator(text.begin(), text.end(), int_re);
         it != std::sregex_iterator(); ++it) {
        ints.push_back((*it)[0].str());
    }
    std::cout << "整数:";
    for (auto& s : ints) std::cout << " " << s;
    std::cout << std::endl;

    // 2) 提取所有日期
    std::regex date_re(R"(\d{4}-\d{2}-\d{2})");
    std::vector<std::string> dates;
    for (auto it = std::sregex_iterator(text.begin(), text.end(), date_re);
         it != std::sregex_iterator(); ++it) {
        dates.push_back((*it)[0].str());
    }
    std::cout << "日期:";
    for (auto& s : dates) std::cout << " " << s;
    std::cout << std::endl;
    return 0;
}

输出:

整数: 12 5 3 2026 08 17 8888
日期: 2026-08-17

💡 注意:\b\d+\b 会把 2026-08-17 拆成 2026、08、17 三个整数——这正是"按整词提取"的正常语义。想跳过日期里的数字,需要更复杂的模式(如排除日期结构),实际业务按需调整。

11.5 生产环境等价写法(std::regex → RE2)

把案例四改成 RE2 只需改三处:头文件 #include <re2/re2.h>、用 RE2::PartialMatch 循环、用 StringPiece 推进偏移:

#include <re2/re2.h>
#include <string>
#include <iostream>
#include <vector>

int main() {
    std::string text = "价格 12.5 元,数量 3 件,日期 2026-08-17 发货,积分 8888。";
    re2::RE2 int_re(R"(\b\d+\b)");

    std::vector<std::string> ints;
    re2::StringPiece input(text);
    std::string matched;
    // 循环:每次 PartialMatch 找到一个,然后把输入前移
    while (RE2::FindAndConsume(&input, int_re, &matched)) {
        ints.push_back(matched);
    }
    std::cout << "整数:";
    for (auto& s : ints) std::cout << " " << s;
    std::cout << std::endl;
    return 0;
}

RE2::FindAndConsume 的语义:找到匹配后自动把输入指针推进到匹配末尾,天然适合"逐个取出所有命中"。RE2 还有 Consume(要求从当前位置开始匹配)和 FindAndConsume(任意位置找)两种。


第 12 章 易错点大全(踩坑合集)

易错点错误示例正确做法后果
1忘记反斜杠双写std::regex("\\d") 其实是对的,错的是写 std::regex("\d")用原始字符串 R"(\d)"编译警告/未定义行为
2regex_match 和 regex_search 混用用 match 找子串全文校验用 match,找片段用 search永远 false,莫名失败
3贪婪匹配吃到不该吃的".*" 匹配多个引号段".*?" 懒惰匹配替换/提取结果错误
4量词套量词(a+)+、(\w+\s?)*化简模式;或换 RE2灾难性回溯卡死
5用 POSIX 语法却期望 \dstd::regex("\\d+", std::regex::extended)默认 ECMAScript,或用 [[:digit:]]匹配失败
6以为 std::regex 支持命名分组(?<name>...)用编号 m[1];或换 RE2/PCRE2编译错误
7每次循环都重新构造 regex在 for 里 std::regex r(...)提到循环外 / static const性能浪费
8忽略空匹配死循环用 iterator 匹配可空模式 a* 遍历检查匹配长度,长度为 0 时手动推进迭代器无限循环/重复结果
9用 \d 匹配中文数字\d 只匹配 0-9按业务补充 [0-90-9一二三四五六七八九十]漏匹配
10替换串里 $ 理解错想输出 $ 却写 $写 $$ 表示字面 $;$& 是整个匹配输出内容错误
11跨平台只测一个编译器只测 Linux 就上线至少在 GCC/Clang/MSVC 各测一遍,或统一用 RE2行为不一致
12用正则解析 HTML/JSON用 .* 匹配嵌套标签换专门解析器(如 simdjson、html parser)规则永远写不对

12.1 空匹配死循环详解(易错点 8 的演示)

#include <regex>
#include <string>
#include <iostream>

int main() {
    std::string text = "abc";
    std::regex pattern(R"(a*)");   // 可匹配空串的模式

    auto begin = std::sregex_iterator(text.begin(), text.end(), pattern);
    auto end = std::sregex_iterator();

    int count = 0;
    for (auto it = begin; it != end; ++it) {
        std::cout << "命中: [" << (*it)[0] << "] 长度=" << (*it)[0].length() << std::endl;
        ++count;
        if (count > 10) {  // 防呆:避免真死循环
            std::cout << "(已截断输出)" << std::endl;
            break;
        }
    }
    return 0;
}

输出会包含大量长度为 0 的空命中(在位置 1、2、3 处各匹配一次空串)。实际业务里若用"迭代器 + 空模式",容易产出重复结果。处理办法:遍历时若 length() == 0,则手动把起始位置 +1 再继续。


第 13 章 FAQ 速查表

问题一句话答案
正则表达式是什么?用特殊字符描述"什么样的文本算命中"的检索模板
C++ 里怎么用正则?#include <regex>,构造 std::regex,调 regex_match / regex_search / regex_replace / regex_iterator
regex_match 和 regex_search 区别?match 要求全文完全匹配;search 只要求包含匹配片段
\d 在 C++ 里怎么写?"\\d"(双写反斜杠),推荐原始字符串 R"(\d)"
贪婪和懒惰量词怎么选?默认贪婪尽量多吃;量词后加 ? 变懒惰尽量少吃;取引号内容用懒惰
怎么提取匹配的部分?用 std::smatch,m[0] 整个匹配,m[1] 起是捕获组
反向引用能用吗?std::regex / PCRE2 可以,RE2 不行
std::regex 支持命名分组吗?不支持,用编号或换 RE2/PCRE2
为什么我的正则一匹配就卡死?大概率是灾难性回溯(如 (a+)+),换 RE2 或简化模式
RE2 和 PCRE2 选哪个?防卡死/高吞吐选 RE2;需要反向引用/环视选 PCRE2
RE2 怎么安装?apt libre2-dev / brew re2 / vcpkg re2
匹配性能太慢怎么办?复用 regex 对象、简化模式、换 RE2、必要时 PCRE2 JIT
能用正则解析 HTML 吗?不建议,嵌套结构请用专门解析器
跨平台正则行为不一样?std::regex 三套实现有差异,生产环境建议统一 RE2/PCRE2
正则校验邮箱要写多复杂?日常够用即可(见 11.2),过度追求 RFC 完整会让模式难维护

第 14 章 总结与延伸阅读

14.1 核心结论(三句话带走)

  1. 会用:std::regex 零依赖、开箱即用,掌握 match / search / replace / iterator 四件套 + 捕获组,就能覆盖 80% 日常需求。
  2. 会用对:全文校验用 regex_match,找片段用 regex_search,遍历用迭代器;反斜杠记得双写;避免量词套量词。
  3. 会选型:生产环境、高吞吐、不可信输入 → RE2(线性时间,永不回溯爆炸);需要反向引用/环视 → PCRE2(加 JIT,控制好输入与超时)。

14.2 与已写 C++ 主题的关系

  • 与《simdjson 高性能 JSON 解析库》互补:simdjson 解决"JSON 文本解析",正则解决"任意文本模式匹配",两者可组合使用(先用正则切出 JSON 片段,再交给 simdjson)。
  • 与《fmt 开源格式化库》互补:fmt 负责"格式化输出",正则负责"输入/中间文本的模式处理"。
  • 与《C++ 多线程并发编程实战》互补:正则匹配天然适合并行——大日志分片后多线程各跑 RE2,RE2::Set 还能一次匹配多规则。

14.3 延伸阅读

主题建议资料
RE2 官方GitHub google/re2 README 与 re2/syntax 文档
PCRE2 官方pcre2pattern(3) 手册页
正则性能分析Russ Cox 的三篇经典文章《Regular Expression Matching Can Be Simple And Fast》
ReDoS 研究OWASP ReDoS 条目;vuln-regex-detector 工具
C++ 标准库cppreference.com 的 <regex> 章节

14.4 附:全部示例代码统一编译命令

# 纯 std::regex 示例(第 3~7、11~12 章)
g++ -std=c++17 -O2 demo.cpp -o demo

# RE2 示例(第 9、11.5 章,需先安装)
g++ -std=c++17 -O2 re2_demo.cpp -o re2_demo -lre2

# PCRE2 示例(第 10 章,需先安装)
g++ -std=c++17 -O2 pcre2_demo.cpp -o pcre2_demo -lpcre2-8
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翎_鸢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值