43、正则表达式:C11标准中的regex.h。正则匹配与替换。
说实话,C语言的标准库在文本处理这块一直有点「寒酸」。早期版本里,你要做字符串匹配,基本就是自己手写状态机,或者去用POSIX的regcomp()/regexec()——但那不是标准C。直到C11标准,终于把regex.h纳入了标准库。嗯,虽然这个实现争议不小,但总算是有了官方说法。
我个人习惯是:能用标准库就用标准库,除非性能实在扛不住,或者平台不支持。C11的regex.h虽然功能不算特别强大,但应付日常的文本校验、日志提取、配置解析,完全够用。
43.1 regex.h 的核心函数
说白了,C11的正则接口就四个核心函数:
| 函数 | 作用 |
|---|---|
regcomp() |
编译正则表达式(把字符串模式转成内部结构) |
regexec() |
执行匹配 |
regerror() |
把错误码转成可读的字符串 |
regfree() |
释放编译后的正则资源 |
你想想看,这跟很多脚本语言的正则API思路差不多——先编译,再执行,最后释放。只不过C里你得手动管理这些步骤。
重要提醒:C11标准中的regex.h是可选特性。也就是说,有些编译器(比如某些版本的MSVC)可能不支持。我在跨平台项目里吃过这个亏——代码在Linux上跑得好好的,移植到Windows上就报regex.h not found。所以,如果你要写跨平台代码,建议先检查__STDC_VERSION__和平台宏。
43.2 基本匹配示例
来看一个最简单的例子:检查一个字符串是不是合法的邮箱格式(简化版)。
#include <stdio.h>
#include <regex.h>
int main() {
regex_t regex;
int reti;
const char *pattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
const char *test_str = "user@example.com";
// 编译正则
reti = regcomp(®ex, pattern, REG_EXTENDED);
if (reti) {
printf("正则编译失败\n");
return 1;
}
// 执行匹配
reti = regexec(®ex, test_str, 0, NULL, 0);
if (!reti) {
printf("匹配成功: %s\n", test_str);
} else if (reti == REG_NOMATCH) {
printf("不匹配\n");
} else {
printf("匹配出错\n");
}
// 释放资源
regfree(®ex);
return 0;
}
这里要注意几个细节:
REG_EXTENDED表示使用扩展正则语法(ERE),支持+、?、{}等元字符。如果不加这个标志,默认是基本正则(BRE),很多特性用不了。- 正则字符串里的反斜杠要转义。比如
\\.在C字符串里要写成"\\."。 - 匹配完一定要调用
regfree(),否则内存泄漏。嗯,这个我刚开始学的时候忘过好几次。
43.3 捕获子串与分组
光匹配还不够,很多时候我们需要提取匹配到的内容。比如从日志里抓出IP地址和端口号。
#include <stdio.h>
#include <regex.h>
int main() {
regex_t regex;
regmatch_t pmatch[3]; // 最多捕获3个分组
const char *pattern = "([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}):([0-9]+)";
const char *test_str = "连接来自 192.168.1.1:8080";
regcomp(®ex, pattern, REG_EXTENDED);
int reti = regexec(®ex, test_str, 3, pmatch, 0);
if (!reti) {
printf("完整匹配: ");
for (int i = pmatch[0].rm_so; i < pmatch[0].rm_eo; i++)
putchar(test_str[i]);
printf("\n");
printf("IP地址: ");
for (int i = pmatch[1].rm_so; i < pmatch[1].rm_eo; i++)
putchar(test_str[i]);
printf("\n");
printf("端口: ");
for (int i = pmatch[2].rm_so; i < pmatch[2].rm_eo; i++)
putchar(test_str[i]);
printf("\n");
}
regfree(®ex);
return 0;
}
regmatch_t结构体里只有两个字段:rm_so(起始偏移)和rm_eo(结束偏移)。注意,pmatch[0]永远对应整个匹配,pmatch[1]对应第一个括号分组,以此类推。
我的经验:用regmatch_t提取子串时,千万别忘了检查rm_so是否为-1。如果某个分组没匹配到,rm_so会被设为-1。我曾经在解析可选分组时没做这个检查,直接拿rm_so当数组下标,结果程序崩了。
43.4 正则替换——自己动手
C11的regex.h没有提供现成的替换函数。不像Python的re.sub()那么方便。你需要自己实现替换逻辑。
基本思路是:循环匹配,每次找到匹配后,把匹配之前的部分和替换字符串拼接到结果中,然后继续从匹配结束位置往后找。
#include <stdio.h>
#include <string.h>
#include <regex.h>
// 简单的正则替换函数
char* regex_replace(const char *str, const char *pattern, const char *replacement) {
regex_t regex;
regmatch_t pmatch[1];
char *result = NULL;
size_t result_len = 0;
const char *cursor = str;
regcomp(®ex, pattern, REG_EXTENDED);
while (regexec(®ex, cursor, 1, pmatch, 0) == 0) {
// 把匹配之前的部分复制到结果
size_t prefix_len = pmatch[0].rm_so;
result = realloc(result, result_len + prefix_len + strlen(replacement) + 1);
strncpy(result + result_len, cursor, prefix_len);
result_len += prefix_len;
strcpy(result + result_len, replacement);
result_len += strlen(replacement);
// 移动游标到匹配结束位置
cursor += pmatch[0].rm_eo;
}
// 复制剩余部分
size_t tail_len = strlen(cursor);
result = realloc(result, result_len + tail_len + 1);
strcpy(result + result_len, cursor);
result_len += tail_len;
result[result_len] = '\0';
regfree(®ex);
return result;
}
int main() {
char *result = regex_replace("hello 123 world 456", "[0-9]+", "NUM");
printf("%s\n", result); // 输出: hello NUM world NUM
free(result);
return 0;
}
注意:上面的代码为了简洁,省略了错误处理和边界检查。实际项目中,你还要考虑:
- 如果替换字符串里包含
$1、$&等反向引用,需要额外解析。 - 如果匹配次数非常多,
realloc频繁调用会影响性能,建议先估算结果大小。 - 如果正则匹配到空字符串(比如
.*),会陷入死循环。需要加一个判断:如果rm_so == rm_eo,就跳出。
43.5 知识体系总览
下面这张图把C11正则的核心流程和关键点串起来了。我建议你把它存下来,写代码时对照着看。
43.6 避坑指南与个人经验
最后,分享几个我踩过的坑:
- 关于REG_EXTENDED:我刚开始用的时候,没加这个标志,结果
+和?都不生效,还以为是库的bug。后来查了文档才发现,默认是基本正则,很多元字符需要反斜杠转义才能用。比如\+才表示「一次或多次」。所以,除非你有特殊需求,否则一律用REG_EXTENDED。 - 关于线程安全:
regex_t结构体不是线程安全的。如果你在多线程环境里用同一个编译好的正则,记得加锁,或者每个线程各自编译一份。我曾经在并发解析日志时,两个线程同时调用regexec(),结果匹配结果全乱了。 - 关于性能:如果你要匹配大量短字符串(比如每秒几万次),建议把
regcomp()放在循环外面。编译正则的开销其实不小,尤其是模式复杂的时候。我见过有人把regcomp()写在循环里,结果性能直接掉了两个数量级。 - 关于替换中的反向引用:标准
regex.h不支持替换时的$1、$&语法。如果你需要这个功能,要么自己解析替换字符串,要么换用PCRE库。我个人在项目里写过一个简单的替换引擎,支持$1到$9,代码量大概两百行,不算太复杂。
一句话总结:C11的regex.h能解决80%的文本匹配需求,但替换功能需要自己动手。如果你需要更强大的正则支持(比如零宽断言、递归匹配),建议直接上PCRE或PCRE2。不过话说回来,对于嵌入式或资源受限的环境,标准库的正则已经够用了——毕竟它不需要额外链接第三方库。