64. STL与正则表达式:regex使用、模式匹配、替换操作

正则表达式这东西,说实话,我早年写C++时一直觉得它是个「黑魔法」。直到有一次接手一个日志分析模块,需要从几十万行服务器日志里提取IP、时间戳和错误码——用字符串手工解析?那代码写出来我自己都不想看第二遍。后来老老实实学了<regex>,嗯,真香。

今天我们就来聊聊C++ STL里的正则表达式库。它不像Python或JavaScript里那么「开箱即用」,但一旦掌握了套路,处理文本的效率能提升一个档次。

正则表达式的基本概念

正则表达式,说白了就是一套「模式匹配」的规则语言。你给它一个字符串,它告诉你:这个字符串里有没有符合规则的部分?如果有,在哪里?是什么?

C++11开始,STL正式引入了<regex>头文件。它支持多种语法风格:ECMAScript(默认)、basic、extended、awk、grep、egrep。我个人习惯用ECMAScript,因为它最接近我们平时在JavaScript或Python里用的写法。

语法类型说明使用场景
ECMAScript默认语法,功能最全日常开发首选
basicPOSIX基本正则兼容老旧系统
extendedPOSIX扩展正则部分Unix工具
awk/grep/egrep对应工具语法移植已有规则
我的建议:除非有特殊兼容需求,否则就用默认的ECMAScript。它支持\d、\w、\s这些简写,写起来顺手很多。

核心类与使用流程

C++正则库有三个核心类,你得记住它们:

  • std::regex —— 代表一个正则表达式对象
  • std::smatch —— 存储匹配结果(针对std::string)
  • std::regex_iterator —— 遍历所有匹配项

使用流程其实就三步:构造正则对象 → 执行匹配 → 处理结果。听起来简单,但坑不少。我们一个个来看。

模式匹配:regex_match vs regex_search

这两个函数经常有人搞混。我刚开始学的时候也踩过坑。

  • std::regex_match:要求整个字符串完全匹配正则模式
  • std::regex_search:只要字符串中有一部分匹配即可

举个例子你就明白了:

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

int main() {
    std::string text = "我的手机号是13812345678,请保存。";
    std::regex pattern(R"(\d{11})");  // 11位数字

    // regex_match:整个字符串必须是11位数字 → 不匹配
    if (std::regex_match(text, pattern)) {
        std::cout << "match成功\n";
    } else {
        std::cout << "match失败\n";  // 会走这里
    }

    // regex_search:只要包含11位数字即可 → 匹配
    std::smatch result;
    if (std::regex_search(text, result, pattern)) {
        std::cout << "找到手机号: " << result.str() << "\n";
    }

    return 0;
}

输出结果:

match失败
找到手机号: 13812345678
我曾经踩过的坑:用regex_match去验证用户输入的邮箱格式,结果用户输入了「abc@def.com 请尽快回复」,匹配失败。后来才意识到,应该先用regex_search提取,或者用^和$锚定整个字符串。

捕获组与子匹配

正则表达式里用括号()可以定义捕获组。这在提取结构化数据时特别有用。比如从日志里提取日期和时间:

std::string log = "2025-03-15 14:30:22 [ERROR] 连接超时";
std::regex pattern(R"((\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2}))");
std::smatch match;

if (std::regex_search(log, match, pattern)) {
    std::cout << "完整时间戳: " << match[0] << "\n";
    std::cout << "年: " << match[1] << "\n";
    std::cout << "月: " << match[2] << "\n";
    std::cout << "日: " << match[3] << "\n";
    std::cout << "时: " << match[4] << "\n";
    std::cout << "分: " << match[5] << "\n";
    std::cout << "秒: " << match[6] << "\n";
}

这里match[0]是整个匹配的字符串,match[1]match[6]分别对应每个括号捕获的内容。你想想看,如果用手工去截取这些字段,得写多少行代码?

迭代匹配:处理多个结果

如果文本里有多个匹配项怎么办?比如从一份配置文件中提取所有IP地址。这时候就该std::regex_iterator出场了。

std::string config = R"(
    server 192.168.1.1
    server 10.0.0.5
    server 172.16.0.10
)";
std::regex ip_pattern(R"((\d{1,3}\.){3}\d{1,3})");

auto begin = std::sregex_iterator(config.begin(), config.end(), ip_pattern);
auto end = std::sregex_iterator();

std::cout << "找到的IP地址:\n";
for (auto it = begin; it != end; ++it) {
    std::cout << "  - " << it->str() << "\n";
}

输出:

找到的IP地址:
  - 192.168.1.1
  - 10.0.0.5
  - 172.16.0.10

这里要注意一点:sregex_iterator的构造开销不小。如果文本很长,建议先考虑是否真的需要遍历所有匹配项。我在项目中处理过百万行级别的日志,每次构造迭代器都会卡一下,后来改用regex_token_iterator做流式处理,性能好了不少。

替换操作:regex_replace

替换是正则表达式的另一个高频场景。C++提供了std::regex_replace,支持用捕获组引用进行灵活替换。

std::string text = "联系人: 张三(13812345678), 李四(13987654321)";
std::regex phone_pattern(R"(\d{11})");

// 简单替换:把手机号替换为[已隐藏]
std::string result1 = std::regex_replace(text, phone_pattern, "[已隐藏]");
std::cout << result1 << "\n";
// 输出: 联系人: 张三([已隐藏]), 李四([已隐藏])

// 高级替换:用捕获组重新格式化
std::regex format_pattern(R"((\d{3})(\d{4})(\d{4}))");
std::string result2 = std::regex_replace(text, format_pattern, "$1****$3");
std::cout << result2 << "\n";
// 输出: 联系人: 张三(138****5678), 李四(139****4321)

看到$1$3了吗?它们分别引用第1个和第3个捕获组。这种写法在处理数据脱敏、格式转换时特别顺手。

一个小技巧:如果你需要做大量替换操作,可以提前把std::regex对象缓存起来。每次构造regex对象都会编译模式,这个开销不小。我习惯把常用的正则表达式定义为全局常量或静态变量。

性能注意事项

正则表达式虽好,但别滥用。C++的<regex>实现有一个特点:编译期不做任何优化,所有工作都留到运行时。这意味着:

  • 同一个正则表达式反复构造,性能会急剧下降
  • 复杂的回溯模式(如嵌套量词)可能导致灾难性回溯
  • 短字符串匹配用string::find可能更快

我在项目中遇到过一个问题:用正则表达式解析CSV文件,结果一个字段里包含了几百个字符,正则引擎回溯了几十万次才匹配完。后来改用状态机手工解析,速度提升了100倍。所以,正则表达式适合「模式匹配」,不适合「语法解析」。

知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

C++正则表达式核心知识体系 std::regex std::smatch std::regex_iterator regex_match(完全匹配) regex_search(部分匹配) regex_replace(替换) 数据验证 邮箱/手机号/身份证 文本提取 日志/配置/HTML 数据脱敏 手机号/银行卡/密码 格式转换 日期/时间/命名规范 ⚠ 注意:避免灾难性回溯 · 缓存regex对象 · 短文本用string::find 复杂解析场景考虑状态机或手写解析器

避坑指南

最后,分享几个我这些年积累的经验:

  • 转义问题:C++字符串里反斜杠本身需要转义,所以写\\d才能表示数字。我建议用原始字符串字面量R"(...)",省心很多。
  • 异常处理:正则表达式语法错误会抛出std::regex_error异常。线上代码一定要捕获它,否则一个错误的模式就能让程序崩溃。
  • 多字节字符:std::regex对UTF-8的支持有限。如果你要处理中文,建议先转成宽字符或使用第三方库。
  • 性能测试:别凭感觉判断正则表达式的快慢。用std::chrono实际测一下,有时候一个简单的string::find比正则快10倍。

正则表达式是个好工具,但它不是万能的。学会在合适的场景使用它,才是真正的工程智慧。


公众号:蓝海资料掘金营,微信deep3321