资讯动态

C++使用正则表达式的详细教程

发布时间:2026/10/20 16:43:26 来源:尧图企业网站定制
前言正则表达式适合处理「结构相对固定、但字段长度和内容可变」的文本日志行解析、配置项提取、输入格式校验、批量替换。C 在 C11 之前没有标准正则库只能依赖 POSIXregcomp/regexec、Boost.Regex 或各平台私有的实现C11 引入regex之后std::regex、std::smatch、std::regex_replace成为可移植的标准写法。初学者最常见的三个误解是把std::regex_match当成「查找子串」其实它要求整个输入都匹配把正则对象在循环里反复构造忽略编译开销以及以为 C 的正则语法和 JavaScript、Python 完全一致——std::regex默认使用 ECMAScript 文法但它基于较早的 ECMA-262 版本命名捕获组、后行断言等现代特性并不在其支持范围内。本文从三个入口函数讲起说明match_results的分组访问方式再讲迭代器遍历与切分最后讨论语法子集、性能与灾难性回溯。示例以 C17 为准全部使用std::string与std::regex不涉及第三方库。一、三个入口match、search、replace使用正则的第一步是构造std::regex。构造时会对模式串做解析语法错误抛出std::regex_error可以用what()或code()查看原因。构造开销不小凡是循环内重复使用的模式都应该在循环外构造一次。std::regex的构造函数可以接受一个文法标志默认是std::regex::ECMAScript还可以按位或上std::regex::icase忽略大小写、std::regex::optimize提示实现做优化具体效果由实现决定、std::regex::multilineC17 起让^/$匹配每行行首行尾。旧的 POSIX 文法basic、extended、awk、grep、egrep仍可用但捕获与转义规则不同新代码没有必要选它们。三个入口的区别必须记牢函数匹配范围典型用途std::regex_match整个输入序列必须完全匹配格式校验、整行解析std::regex_search输入中任意位置找到一处匹配即可查找、提取字段std::regex_replace替换所有匹配默认批量改写文本// 适用于 C17#include iostream#include regex#include stringint main() {const std::string date 2026-10-19;const std::regex re(R((\d{4})-(\d{2})-(\d{2})));std::smatch m;if (std::regex_match(date, m, re)) {std::cout year m[1].str() month m[2].str() day m[3].str() \n;}const std::string line build at 2026-10-19 done;if (std::regex_search(line, m, re)) {std::cout found: m.str(0) at offset m.position(0) \n;}return 0;}m[0]是整个匹配m[1]、m[2]依次是第 1、2 个捕获组。m.str(n)与m[n].str()等价返回std::string的副本m.position(n)返回该组在输入中的起始偏移m.length(n)返回长度。若某个可选分组没有参与匹配m[n].matched为false此时str()返回空串position()的值没有意义。regex_replace默认替换所有不重叠的匹配替换串里$1、$2表示捕获组$表示整个匹配$$表示字面量美元符。加上std::regex_constants::format_first_only就只替换第一处。// 适用于 C17#include regex#include stringstd::string normalize_date(const std::string in) {const std::regex re(R((\d{4})-(\d{2})-(\d{2})));// 2026-10-19 - 19/10/2026return std::regex_replace(in, re, $3/$2/$1);}注意替换串里的$是有特殊含义的如果要把用户输入原样作为替换内容必须先转义$和\否则会被解释成反向引用。这一点在拼接模板时尤其容易出事。二、match_results、分组与标志std::smatch是std::match_resultsstd::string::const_iterator的别名对应宽字符串是std::wsmatchC 字符串是std::cmatch。它有几个容易忽略的成员size()返回「分组数 1」即使某些组没参与匹配empty()表示匹配是否成功没有匹配时为空ready()表示结果是否已经由某个匹配函数填充prefix()/suffix()返回匹配之前的子序列与之后的子序列类型是sub_match可转成std::stringbegin()/end()可以遍历所有分组。分组编号按左括号出现的顺序从左到右、从外到内。(?:...)是非捕获组不占用编号这在只需要分组语义、不需要取内容时能减少编号混乱。默认的 ECMAScript 文法不支持命名捕获组(?name...)这类写法如果代码里需要按名字取字段只能自己维护「编号到名字」的映射或者换用 Boost.Regex 等支持命名组的库以该库文档为准。一个常见需求是「可选的多个分支」比如匹配keyvalue或key: value。可以写成(\w)\s*[:]\s*(\w)其中[:]是字符类\s*允许空白。要注意\w在 ECMAScript 文法下等价于[A-Za-z0-9_]不含中文处理 UTF-8 中文时不要指望\w应显式列出字符范围或用字节级模式。另外std::regex匹配的是字节序列char它不理解 UTF-8 编码。对 UTF-8 文本使用.匹配的是单个字节而不是单个字符量词{2}数的是字节。需要按字符处理非 ASCII 文本时应改用std::wregex并把文本转成wchar_t转换规则依赖 locale 与平台或者使用专门的 Unicode 正则库。这是标准库正则的能力边界不是配置问题。三、遍历所有匹配与切分字符串只取第一处匹配用regex_search就够要遍历全部匹配用std::sregex_iterator。它的构造函数接受迭代器区间和正则对象解引用得到smatch。// 适用于 C17#include iostream#include regex#include stringint main() {const std::string log useralice ip127.0.0.1 code200; userbob ip127.0.0.2 code404;const std::regex item(R((\w)([^\s;])));for (std::sregex_iterator it(log.begin(), log.end(), item), end;it ! end; it) {const std::smatch m *it;std::cout m[1].str() - m[2].str() \n;}return 0;}std::sregex_iterator的默认构造版本表示「尾后迭代器」循环条件写成it ! end即可。每次it会从上一处匹配的末尾继续搜索因此不会重复匹配同一段文本零长度匹配也能推进标准要求迭代器在这种情况下前进不会死循环。切分字符串可以用std::regex_token_iterator把最后一个参数设为-1表示「取分隔符之间的片段」// 适用于 C17#include regex#include string#include vectorstd::vectorstd::string split(const std::string s, const std::string sep) {const std::regex re(sep);std::vectorstd::string out;for (std::sregex_token_iterator it(s.begin(), s.end(), re, -1), end;it ! end; it) {out.push_back(it-str());}return out;}如果分隔符本身就是正则元字符如.、|、必须先转义否则语义完全变了。把用户提供的分隔符直接拼进模式串是典型的注入问题攻击者可以构造一个导致灾难性回溯的模式。稳妥做法是只接受固定枚举的分隔符或者写一个escape_regex函数把元字符逐个加反斜杠。四、ECMAScript 文法常用子集标准库默认的 ECMAScript 文法覆盖了日常所需的大部分语法但有几处与流行语言不同值得单独列出语法含义备注.任意字符默认不含换行需要跨行时用[\s\S]\d\w\s数字、单词字符、空白均为 ASCII 语义[abc][^abc]字符类、否定字符类类内-表示范围*?{n,m}量词默认贪婪加?变懒惰如.*?(...)(?:...)捕获组、非捕获组编号按左括号顺序(?...)(?!...)先行断言、否定先行断言只断言位置不消耗字符^$行首、行尾multiline下按行\b单词边界依赖 ASCII 单词定义\1反向引用引用前面的捕获组需要明确的是默认文法不支持后行断言(?...)、(?!...)也不支持命名捕获组。写了这些语法构造std::regex时会抛std::regex_error而不是静默忽略。遇到「必须用后行断言」的需求只能改写模式比如把前文也捕获进来再截取或换库。另一个差异是转义。在普通字符串字面量里写\d反斜杠会被 C 编译器先处理\d不是标准的转义序列编译器通常给出警告并按字面保留字符d这个行为不可依赖。正确写法是\\d或原始字符串R(\d)。建议一律用原始字符串可读性最好。五、性能、回溯与输入校验std::regex的匹配引擎在不同标准库实现中差异很大。libstdc 的实现长期被认为偏慢libc 与 MSVC 的实现也不相同具体快慢必须以自己工具链上的基准为准本文不给任何倍数。能确定的优化原则有三条第一正则对象只构造一次。构造包含解析与状态机构建成本远高于一次普通匹配把它放进循环是纯粹浪费。第二避免灾难性回溯。嵌套量词如(a)$、(.*)*在长输入上可能产生指数级回溯。这类模式在输入不匹配时最危险。防御手段包括把嵌套量词改写为等价的单层量词、用占有量词思路C 标准正则不支持占有量词只能改写、限制输入长度、对用户输入设置超时或长度上限。第三不要用正则解析嵌套结构。HTML、JSON、C 源码都不是正则语言试图用一个模式匹配任意嵌套标签结果往往是错漏百出且性能不可控。这类任务应使用专门的解析器正则只适合处理其中的词法片段。还有一个安全侧的习惯对来自外部的输入先做长度上限检查再交给正则。正则匹配的耗时与输入长度和模式结构都相关不设上限等于把 CPU 时间交给对方控制。常见坑点❌ 用std::regex_match在长文本里查找子串结果总是失败。✅ 整体匹配用regex_match查找用regex_search。❌ 在循环体内构造std::regex。✅ 在循环外构造一次循环内复用。❌ 把\d当模式串反斜杠被 C 转义吃掉。✅ 用R(\d)或\\d。❌ 用(?...)后行断言运行期抛std::regex_error。✅ 默认 ECMAScript 文法不支持后行断言改写模式或换库。❌ 认为m[1].str()在组未参与匹配时是安全的「上一次的值」。✅ 先检查m[1].matched未匹配时内容为空。❌ 把用户输入直接拼进模式串或替换串。✅ 转义元字符替换串中的$与\也要处理。❌ 用.匹配 UTF-8 中文按字符数量词。✅std::regex是字节级非 ASCII 用wregex或 Unicode 库。❌ 写(a)$这类嵌套量词去校验长输入。✅ 改写模式、限制输入长度防止灾难性回溯。总结主题结论标准版本regex自 C11 起multiline需 C17入口regex_match全匹配regex_search查找regex_replace替换分组m[0]整体m[n]第 n 组先查matched遍历sregex_iterator切分用regex_token_iterator的-1文法默认 ECMAScript无命名组、无后行断言编码字节级匹配不识别 UTF-8 字符边界性能预构造、防回溯、限长度具体基准自行测量std::regex足以覆盖大多数「提取与校验」场景前提是记住它是字节级的、默认文法有边界、匹配失败与匹配成功要用对入口。把正则对象移出循环、给外部输入加长度上限这两条就能挡掉大部分性能与安全问题。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑