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(&regex, pattern, REG_EXTENDED);
    if (reti) {
        printf("正则编译失败\n");
        return 1;
    }

    // 执行匹配
    reti = regexec(&regex, test_str, 0, NULL, 0);
    if (!reti) {
        printf("匹配成功: %s\n", test_str);
    } else if (reti == REG_NOMATCH) {
        printf("不匹配\n");
    } else {
        printf("匹配出错\n");
    }

    // 释放资源
    regfree(&regex);
    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(&regex, pattern, REG_EXTENDED);
    int reti = regexec(&regex, 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(&regex);
    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(&regex, pattern, REG_EXTENDED);

    while (regexec(&regex, 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(&regex);
    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正则的核心流程和关键点串起来了。我建议你把它存下来,写代码时对照着看。

C11 regex.h 正则处理流程 1. 编译正则 regcomp(&regex, pattern, flags) flags: REG_EXTENDED / REG_ICASE 2. 执行匹配 regexec(&regex, str, nmatch, pmatch, eflags) 返回 0=成功, REG_NOMATCH=不匹配 3. 处理结果 pmatch[0] → 完整匹配 pmatch[1..n] → 分组捕获 4. 释放资源 regfree(&regex) 替换操作(需手动实现) 循环 regexec → 拼接前缀+替换文本 移动游标 → 处理剩余部分 常见坑点 • 跨平台:MSVC 不支持 regex.h,需用 PCRE 或自己封装 • 空匹配:.* 可能导致死循环,需检查 rm_so == rm_eo • 内存泄漏:regfree() 别忘了调用

43.6 避坑指南与个人经验

最后,分享几个我踩过的坑:

  • 关于REG_EXTENDED:我刚开始用的时候,没加这个标志,结果+?都不生效,还以为是库的bug。后来查了文档才发现,默认是基本正则,很多元字符需要反斜杠转义才能用。比如\+才表示「一次或多次」。所以,除非你有特殊需求,否则一律用REG_EXTENDED
  • 关于线程安全:regex_t结构体不是线程安全的。如果你在多线程环境里用同一个编译好的正则,记得加锁,或者每个线程各自编译一份。我曾经在并发解析日志时,两个线程同时调用regexec(),结果匹配结果全乱了。
  • 关于性能:如果你要匹配大量短字符串(比如每秒几万次),建议把regcomp()放在循环外面。编译正则的开销其实不小,尤其是模式复杂的时候。我见过有人把regcomp()写在循环里,结果性能直接掉了两个数量级。
  • 关于替换中的反向引用:标准regex.h不支持替换时的$1$&语法。如果你需要这个功能,要么自己解析替换字符串,要么换用PCRE库。我个人在项目里写过一个简单的替换引擎,支持$1$9,代码量大概两百行,不算太复杂。

一句话总结:C11的regex.h能解决80%的文本匹配需求,但替换功能需要自己动手。如果你需要更强大的正则支持(比如零宽断言、递归匹配),建议直接上PCRE或PCRE2。不过话说回来,对于嵌入式或资源受限的环境,标准库的正则已经够用了——毕竟它不需要额外链接第三方库。


专注资料整理