一、POSIX 正则库入门:从 说起

正则表达式,说白了就是一种模式匹配的语言。你给它一个字符串,它告诉你「这个字符串长啥样」。我刚开始接触 C 语言的正则时,第一反应是:标准库居然不直接支持?后来才发现,POSIX 标准早就把这事儿安排好了。

嗯,今天我们就来聊聊 POSIX 正则库。这套接口定义在 头文件里,是 Linux 和 Unix 系统下的标配。Windows 下用 MinGW 或 Cygwin 也能用,但说实话,我在 Windows 上踩过坑,后面会提到。

1.1 头文件:你需要的都在这里

打开 ,你会发现里面主要定义了三个东西:

  • regex_t:编译后的正则表达式结构体。你可以把它理解成一个「编译好的模式」。
  • regmatch_t:匹配结果结构体。它记录了匹配到的子串在源字符串中的起始和结束位置。
  • 四个核心函数:regcomp、regexec、regfree、regerror。

我个人习惯在写代码前先看一眼这些结构体的定义。比如 regex_t 里有个 re_nsub 字段,记录着正则表达式里有多少个捕获组。这个字段我在调试时经常用到。

核心要点:POSIX 正则库分为两套接口——基本正则表达式(BRE)和扩展正则表达式(ERE)。默认是 BRE,如果你想要更现代的语法(比如 +、?、|),记得在编译时加上 REG_EXTENDED 标志。

1.2 regcomp:编译正则表达式

写正则的第一步,是把字符串形式的模式「编译」成 regex_t 结构体。regcomp 就是干这个的。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    const char *pattern = "^[a-zA-Z0-9]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,}$";
    int ret;

    ret = regcomp(&regex, pattern, REG_EXTENDED | REG_ICASE);
    if (ret != 0) {
        printf("编译失败\n");
        return 1;
    }

    printf("编译成功!\n");
    regfree(&regex);
    return 0;
}

这里要注意几个点:

  • 第三个参数是标志位:REG_EXTENDED 表示使用扩展正则语法,REG_ICASE 表示忽略大小写。你可以用 | 组合多个标志。
  • 返回值:成功返回 0,失败返回非 0。别指望它返回正数表示成功——我见过有人写反了,调试了半天。
  • 转义问题:C 字符串里,反斜杠本身需要转义。比如 \d 要写成 \\d。这个坑我踩过不止一次。

我的经验:如果你不确定正则语法对不对,先用 Python 或在线工具验证一下。C 语言调试正则太痛苦了,没有实时反馈。

1.3 regexec:匹配正则表达式

编译好了,就该上场了。regexec 负责在目标字符串里查找匹配。

#include <regex.h>
#include <stdio.h>

int main() {
    regex_t regex;
    const char *pattern = "hello";
    const char *text = "hello world";
    int ret;

    regcomp(&regex, pattern, REG_EXTENDED);

    ret = regexec(&regex, text, 0, NULL, 0);
    if (ret == 0) {
        printf("匹配成功!\n");
    } else if (ret == REG_NOMATCH) {
        printf("没有匹配\n");
    } else {
        printf("匹配出错\n");
    }

    regfree(&regex);
    return 0;
}

参数说明:

  • 第二个参数:目标字符串,必须是 null 结尾的 C 字符串。
  • 第三、四个参数:regmatch_t 数组和它的长度。如果你只关心是否匹配,传 0 和 NULL 就行。
  • 第五个参数:标志位,一般传 0。REG_NOTBOL 和 REG_NOTEOL 可以改变行首行尾的行为。

为什么会返回 REG_NOMATCH?说白了就是没找到。但如果你传了错误的 regex_t(比如没编译就拿来用),行为是未定义的。嗯,这里要注意:永远先编译,再匹配

1.4 regfree:释放正则表达式

有借有还,再借不难。regcomp 内部会动态分配内存,用完必须释放。

regfree(&regex);

就这么一行。但很多人会忘记。我曾经在一个循环里反复编译正则而不释放,结果内存泄漏到程序崩溃。从那以后,我养成了「编译后立即配对 regfree」的习惯。

警告:regfree 之后,regex_t 结构体就失效了。不要再对它调用 regexec 或 regfree。如果你需要再次使用,必须重新 regcomp。

1.5 regerror:错误处理

regcomp 和 regexec 返回错误码时,你肯定想知道具体出了什么问题。regerror 就是干这个的。

#include <regex.h>
#include <stdio.h>
#include <string.h>

int main() {
    regex_t regex;
    const char *pattern = "[a-z";  // 故意写错
    int ret;
    char errbuf[256];

    ret = regcomp(&regex, pattern, REG_EXTENDED);
    if (ret != 0) {
        regerror(ret, &regex, errbuf, sizeof(errbuf));
        printf("错误信息:%s\n", errbuf);
        return 1;
    }

    regfree(&regex);
    return 0;
}

参数说明:

  • 第一个参数:错误码,就是 regcomp 或 regexec 的返回值。
  • 第二个参数:对应的 regex_t 指针。如果错误发生在编译前,可以传 NULL。
  • 第三、四个参数:输出缓冲区和大小。

我个人习惯把错误信息打印到 stderr,方便和正常输出分开。另外,errbuf 的大小建议至少 256 字节,有些错误信息挺长的。

1.6 知识体系总览

下面这张图帮你理清 POSIX 正则库的核心流程:

POSIX 正则库核心流程 模式字符串 regcomp regex_t 编译结果 regexec 匹配结果 regfree 释放 regerror 错误处理 虚线表示错误路径,实线表示正常流程

1.7 避坑指南

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

  • 忘记释放:regcomp 分配的内存不会自动回收。程序退出时系统会回收,但长期运行的服务会泄漏。我曾经有一个后台服务跑了三天,内存从 10MB 涨到 2GB,最后发现是循环里忘了 regfree。
  • 线程安全:regex_t 不是线程安全的。如果你在多线程环境里用同一个 regex_t 匹配,记得加锁,或者每个线程独立编译一份。
  • 正则语法差异:POSIX 正则和 Perl 正则不一样。比如 \d 在 POSIX 里不支持,你得用 [[:digit:]]。这个坑我帮同事排查过,他死活想不通为什么 \d 匹配不到数字。
  • 缓冲区溢出:regmatch_t 里的 rm_so 和 rm_eo 是下标,不是长度。计算子串长度时用 rm_eo - rm_so,别直接用 rm_eo。

我的习惯:写正则相关代码时,我会先写一个简单的测试函数,把各种边界情况都跑一遍。比如空字符串、超长字符串、特殊字符等。正则这东西,看着简单,用起来全是细节。

好了,POSIX 正则库的入门就聊到这里。四个函数,一个结构体,记住流程:编译 → 匹配 → 释放。出错时用 regerror 查原因。就这么简单。


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