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::regex C++ 标准库(C++11 引入) 零依赖、随手可用 不用装任何东西,跨平台 各编译器实现差异大,性能普遍较慢,部分语法支持不全
RE2 Google 开源 安全优先的高性能引擎 线性时间匹配,永不回溯爆炸,适合不可信输入 不支持反向引用、环视等"高级回溯特性"
PCRE2 Perl 兼容正则社区 功能最全、兼容 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> 头文件里。在代码开头加上:

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

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

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

cpp 复制代码
#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++ 编译器):

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

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

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

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

cpp 复制代码
#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 是整件包裹,后面的 12...是里面分装的小盒子(捕获组,第 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 个数字。
cpp 复制代码
#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 转义:特殊字符要"加反斜杠"

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

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

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

cpp 复制代码
// 普通字符串写法(容易看花眼)
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 正则最接近)。构造时可用第二个参数切换:

cpp 复制代码
// 默认 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 / extended POSIX 风格,\d 这类缩写不支持,要用 \[:digit:]
std::regex::awk / grep / egrep 兼容经典 Unix 工具

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


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

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

cpp 复制代码
#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:局部搜索(找片段)

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

cpp 复制代码
#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:批量替换

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

cpp 复制代码
#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 保留捕获组做"部分保留"的替换(只打码中间四位):

cpp 复制代码
#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(推荐,直观)

cpp 复制代码
#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(想只取"捕获组内容"时很方便)

cpp 复制代码
#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_match bool(或通过 smatch 拿分组)
是否包含 / 第一处匹配在哪 regex_search bool + smatch(位置、内容)
把所有命中替换掉 regex_replace 新字符串
把每处命中都取出来 regex_iterator 逐段遍历
只要每处命中的某个分组 regex_token_iterator 逐段遍历(可指定分组)

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

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

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

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

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

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

cpp 复制代码
#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 个捕获组刚才匹配到的内容一模一样"。

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

cpp 复制代码
#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 不支持(只能用编号 m1、m2...) ---

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

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

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

cpp 复制代码
// 需求:匹配 "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 个字符,但运行时间可能超过几秒甚至永远跑不完

cpp 复制代码
#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 分成若干段",比如 aaa...、aaaa...、aaaa...------有 2^30 种分法
  2. 每种分法都要试到末尾,发现没有 b 才失败,然后退回换下一种分法。
  3. 试完所有分法 ≈ 10 亿种组合 → 程序卡死。

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

7.3 经典危险模式清单

危险模式 危险原因 攻击输入示例
^(a+)+$ 外层 + 套内层 + 一串 a 后跟别的字符
`^(a a)+$` 多分支 + 量词
^(\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 首尾相接
并集 `a b`
星号 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 许可)。安装方式:

bash 复制代码
# 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(部分匹配)。

cpp 复制代码
#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 批量优化:

cpp 复制代码
#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 默认就带保护,还可以显式设置上限:

cpp 复制代码
#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::regex RE2
构造 std::regex p(s) re2::RE2 p(s)
全文匹配 regex_match RE2::FullMatch
部分匹配 regex_search RE2::PartialMatch
遍历所有命中 sregex_iterator re2::StringPiece + 循环 PartialMatch(用 Rewrite 或手写偏移推进)
批量替换 regex_replace RE2::GlobalReplace / RE2::Replace
批量多模式 手动循环 RE2::Set

批量替换示例:

cpp 复制代码
#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),匹配速度比解释执行快数倍。启用方式:

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

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


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

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

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

cpp 复制代码
#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 / 手机号

cpp 复制代码
#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 案例三:数据脱敏------身份证、手机号、卡号打码

cpp 复制代码
#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 案例四:批量提取------从文本中取出所有数字、所有日期

cpp 复制代码
#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 推进偏移:

cpp 复制代码
#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)" 编译警告/未定义行为
2 regex_match 和 regex_search 混用 用 match 找子串 全文校验用 match,找片段用 search 永远 false,莫名失败
3 贪婪匹配吃到不该吃的 ".*" 匹配多个引号段 ".*?" 懒惰匹配 替换/提取结果错误
4 量词套量词 (a+)+、(\w+\s?)* 化简模式;或换 RE2 灾难性回溯卡死
5 用 POSIX 语法却期望 \d std::regex("\\d+", std::regex::extended) 默认 ECMAScript,或用 \[:digit:] 匹配失败
6 以为 std::regex 支持命名分组 (?<name>...) 用编号 m1;或换 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 的演示)

cpp 复制代码
#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,m0 整个匹配,m1 起是捕获组
反向引用能用吗? 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 附:全部示例代码统一编译命令

bash 复制代码
# 纯 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
相关推荐
circuitsosk3 小时前
AI输出的“质检员”:构建智能体质量评估、异常检测与人工兜底的三层防线
人工智能·python·microsoft·正则表达式·langchain
码匠许师傅4 小时前
【C++ 面试真题】聊聊 C++ 的序列容器
java·c++·面试
C++ 老炮儿的技术栈4 小时前
Qt5.9.1 Windows 完整开发环境搭建流程
开发语言·c++·windows·qt·编辑器·代码化
欧特克_Glodon4 小时前
OpenCV计算机视觉开发入门与实践<十一>:矩阵的复制和矩形类Rect
c++·opencv·计算机视觉·矩阵
键盘会跳舞5 小时前
C++:std::tuple 源码级深度拆解——变参模板、SFINAE与模板元编程核心技巧
开发语言·c++·sfinae·变参模板
bkspiderx5 小时前
从零开始:VS Code搭建C/C++开发环境全指南(Windows/macOS/Linux)
c语言·c++·windows·vs code·c/c++开发环境
东华万里5 小时前
第40篇C++核心基础与工程实践:从底层逻辑到避坑指南
开发语言·c++·面试·大学生专区
luj_17685 小时前
元设计的诱惑与现实
c语言·开发语言·c++·经验分享·算法
小星星闪亮登场5 小时前
ST表--倍增思想
开发语言·数据结构·c++·算法·思维