10. 错误处理与调试:常见编译错误、运行时匹配错误、使用 regerror 获取错误信息、调试技巧
正则表达式这东西,写起来爽,调试起来有时候真想砸键盘。我刚开始用 C 语言做正则匹配那会儿,踩过的坑能绕办公室三圈。编译报错、运行时崩溃、匹配结果莫名其妙……说白了,这些都不是 bug,是你还没学会跟正则引擎好好沟通。
这一章,我就把那些年我摔过的跟头、总结的经验,一股脑倒给你。你跟着走一遍,以后遇到正则报错,心里就有底了。
10.1 常见编译错误:regcomp() 返回的坑
先说说编译阶段。你写好了正则模式串,调用 regcomp() 去编译,结果它返回一个非零值。嗯,这时候别慌,先看看是啥错误。
我遇到过最典型的,就是括号不匹配。比如你写了个 "(abc",少了个右括号。或者转义字符写错了,像 "\d" 在 POSIX 标准里是不认的,得用 "[[:digit:]]"。
还有一类坑:模式串太长,或者包含了非法字符。我记得有一次,我从配置文件里读了一个正则,结果文件编码有问题,里面藏了个不可见字符。编译直接炸了。
核心要点:regcomp() 返回 0 表示成功,非零表示失败。别忽略这个返回值!
下面这个表格,是我整理的最常见的编译错误码,你保存一下,以后查起来方便:
| 错误码 | 宏定义 | 含义 | 常见原因 |
|---|---|---|---|
| 1 | REG_BADBR | 无效的边界表达式 | \{ 或 \} 使用错误 |
| 2 | REG_BADPAT | 无效的正则表达式 | 语法错误,如括号不匹配 |
| 3 | REG_BADRPT | 无效的重复操作符 | *、+ 前面没有内容 |
| 4 | REG_EBRACE | 大括号不匹配 | \{ 缺少 \} |
| 5 | REG_EBRACK | 方括号不匹配 | [ 缺少 ] |
| 6 | REG_EPAREN | 圆括号不匹配 | ( 缺少 ) |
| 7 | REG_ERANGE | 无效的范围 | [z-a] 这种倒序范围 |
| 8 | REG_ESPACE | 内存不足 | 模式太复杂,或系统资源不够 |
10.2 运行时匹配错误:regexec() 的陷阱
编译通过了,别高兴太早。运行时匹配错误更隐蔽。我见过最离谱的,是有人把 regexec() 的第三个参数传了个 0,结果匹配到了也不报错,就是拿不到结果。
运行时错误通常分两类:一类是参数传错了,比如 preg 指针是野指针,或者 pmatch 数组没分配够空间。另一类是逻辑错误,比如你用了 REG_EXTENDED 标志,但模式串里还混着基本正则的语法。
举个例子:
regex_t regex;
int reti;
char msgbuf[100];
// 编译一个简单的模式
reti = regcomp(®ex, "^hello.*world$", 0);
if (reti) {
fprintf(stderr, "编译失败\n");
return 1;
}
// 执行匹配
reti = regexec(®ex, "hello beautiful world", 0, NULL, 0);
if (!reti) {
printf("匹配成功\n");
} else if (reti == REG_NOMATCH) {
printf("没有匹配\n");
} else {
// 这里就是运行时错误
regerror(reti, ®ex, msgbuf, sizeof(msgbuf));
fprintf(stderr, "匹配出错: %s\n", msgbuf);
}
regfree(®ex);
你看,上面这段代码里,regexec() 返回 REG_NOMATCH 是正常情况,但返回其他非零值,就说明出错了。我建议你每次调用 regexec() 之后,都检查一下返回值,别偷懒。
警告:千万不要在 regexec() 返回非零时直接认为就是“不匹配”。它可能是内存错误、参数错误,甚至是引擎内部异常。一定要用 regerror() 去查具体原因。
10.3 使用 regerror() 获取错误信息
刚才提到了 regerror(),这个函数就是你的救命稻草。它的作用是把错误码转换成人类能读懂的字符串。
函数原型长这样:
size_t regerror(int errcode, const regex_t *preg, char *errbuf, size_t errbuf_size);
参数说明:
errcode:错误码,就是regcomp()或regexec()返回的那个值preg:指向编译后的正则表达式结构体errbuf:存放错误信息的缓冲区errbuf_size:缓冲区大小
返回值是实际需要的缓冲区长度。如果 errbuf_size 太小,错误信息会被截断,但返回值会告诉你需要多大。
我个人习惯这样用:
char errbuf[256];
regerror(reti, ®ex, errbuf, sizeof(errbuf));
fprintf(stderr, "正则错误: %s\n", errbuf);
嗯,就这么简单。但有个细节要注意:regerror() 返回的信息是 locale 相关的。在中文系统下,它可能会返回中文描述。不过为了保险,我建议你始终用英文环境,或者自己维护一套错误码映射表。
小技巧:如果你想知道 regerror() 到底需要多大的缓冲区,可以先传 NULL 和 0 调用一次,根据返回值动态分配内存。不过一般情况下,256 字节绰绰有余。
10.4 调试技巧:从入门到放弃?不,是到精通
调试正则表达式,说白了就是两件事:看模式对不对,看匹配结果对不对。我总结了一套流程,你照着做,能省不少时间。
10.4.1 分段测试法
别一上来就写一个几十个字符的超长正则。拆开测。比如你要匹配一个邮箱地址,先测 "@" 能不能匹配到,再测用户名部分,再测域名部分。每一段都确认无误了,再拼起来。
我曾经接手过一个项目,里面有个正则写了 200 多字符,出问题了没人敢动。我花了一下午,拆成 5 段,一段一段测,最后发现是中间一个转义符写错了。你说冤不冤?
10.4.2 打印中间结果
在 regexec() 之后,把 pmatch 数组里的 rm_so 和 rm_eo 打印出来。看看匹配到的子串起始和结束位置对不对。很多时候,你以为匹配到了,其实匹配的是别的东西。
regmatch_t pmatch[10];
reti = regexec(®ex, str, 10, pmatch, 0);
if (!reti) {
for (int i = 0; i < 10; i++) {
if (pmatch[i].rm_so != -1) {
printf("子串 %d: 从 %d 到 %d\n", i, pmatch[i].rm_so, pmatch[i].rm_eo);
}
}
}
10.4.3 使用测试工具辅助
虽然咱们讲的是 C 语言,但调试正则的时候,我建议你开个浏览器,用在线正则测试工具先验证一下模式串。等模式串确认无误了,再搬到 C 代码里。这样能排除掉模式串本身的问题,剩下的就是 C 代码的锅了。
10.4.4 内存泄漏检查
每次 regcomp() 之后,一定要记得 regfree()。这个不用我多说了吧?我见过有人在一个循环里反复 regcomp() 但不释放,最后内存爆了。用 valgrind 跑一下,一目了然。
调试口诀:先测模式串,再测匹配逻辑;先看返回值,再看匹配位置;先保证编译成功,再处理运行时错误。
10.5 知识体系:错误处理与调试全景图
下面这张图,是我画的错误处理与调试的完整流程。你跟着走一遍,基本不会漏掉什么。
你看,整个流程其实不复杂。编译阶段和匹配阶段都可能出错,但处理方式是一样的:用 regerror() 拿错误信息,然后根据信息去定位问题。调试技巧贯穿始终,帮你更快地找到根因。
好了,这一章的内容就到这儿。错误处理这东西,说白了就是养成好习惯。每次调用 regcomp() 和 regexec() 都检查返回值,出错了就用 regerror() 查原因,配合分段测试和打印中间结果,大部分问题都能快速解决。
记住,正则引擎不会骗你,它只会忠实地执行你写的模式。如果结果不对,那一定是你写错了——或者,你忘了检查返回值。