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(&regex, "^hello.*world$", 0);
if (reti) {
    fprintf(stderr, "编译失败\n");
    return 1;
}

// 执行匹配
reti = regexec(&regex, "hello beautiful world", 0, NULL, 0);
if (!reti) {
    printf("匹配成功\n");
} else if (reti == REG_NOMATCH) {
    printf("没有匹配\n");
} else {
    // 这里就是运行时错误
    regerror(reti, &regex, msgbuf, sizeof(msgbuf));
    fprintf(stderr, "匹配出错: %s\n", msgbuf);
}

regfree(&regex);

你看,上面这段代码里,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, &regex, 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_sorm_eo 打印出来。看看匹配到的子串起始和结束位置对不对。很多时候,你以为匹配到了,其实匹配的是别的东西。

regmatch_t pmatch[10];
reti = regexec(&regex, 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 知识体系:错误处理与调试全景图

下面这张图,是我画的错误处理与调试的完整流程。你跟着走一遍,基本不会漏掉什么。

正则表达式错误处理与调试流程 regcomp() 编译 返回 0 → 成功 返回非0 → 失败 regerror() 获取信息 regexec() 匹配 返回 0 → 匹配成功 REG_NOMATCH → 无匹配 其他非0 → 运行时错误 regerror() 获取信息 调试技巧 • 分段测试法 • 打印 pmatch • 在线工具验证 • valgrind 检查 • 检查返回值 • 注意 regfree() • 缓冲区大小 • locale 影响 核心:编译检查 → 匹配检查 → 错误信息获取 → 调试定位

你看,整个流程其实不复杂。编译阶段和匹配阶段都可能出错,但处理方式是一样的:用 regerror() 拿错误信息,然后根据信息去定位问题。调试技巧贯穿始终,帮你更快地找到根因。

好了,这一章的内容就到这儿。错误处理这东西,说白了就是养成好习惯。每次调用 regcomp()regexec() 都检查返回值,出错了就用 regerror() 查原因,配合分段测试和打印中间结果,大部分问题都能快速解决。

记住,正则引擎不会骗你,它只会忠实地执行你写的模式。如果结果不对,那一定是你写错了——或者,你忘了检查返回值。


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