PAYATHON 2026

如何在 C 字符串库中实现多个搜索函数?

技术老齐

建议采用第一种方案:保留六个含义明确的搜索接口,将可复用的实现放在 .c 文件的私有辅助函数中。_Generic 和可变参数宏只负责选择入口。

这六种搜索操作已经固定,引入函数指针策略会增加调用约定和调试成本。通过合理组织代码就能控制头文件的规模,无须为了少写几个函数声明而引入回调机制。

先区分两类搜索

这六个函数分为子串搜索和字符集合搜索两类:

操作搜索内容搜索方向
find完整子串,或单个字符向前
rfind完整子串,或单个字符向后
find_first_of字符集合中的任意字符向前
find_first_not_of不属于字符集合的字符向前
find_last_of字符集合中的任意字符向后
find_last_not_of不属于字符集合的字符向后

例如,find(str, "abc") 寻找连续的 "abc",find_first_of(str, "abc") 则寻找 'a'、'b'、'c' 中的任意一个字符。两者的匹配条件不同,不适合共用同一个子串搜索算法。

如果沿用 C++ 的位置语义,向前搜索的 pos 是起点;反向搜索的 pos 则是候选匹配位置的上限。对于 rfind,这个上限限制的是子串的起始位置。C++ 搜索语义

还要区分两种 count:C++ 的 find(s, pos, count) 用它表示从参数 s 中取多少个字符作为搜索目标,问题中的 count 则表示被搜索字符串的窗口长度。后者是自定义接口,建议保留 _range 名称,并写清约定。C++ 搜索重载定义

独立接口可以共享实现

代码可以这样组织:

  • cstring.h:公开函数声明和少量便利包装。
  • cstring.c:搜索算法、边界检查和 static 辅助函数。
  • 可选的 cstring_generic.h:_Generic 分派和参数个数选择宏。

字符集合搜索的四个入口可以共用一个私有扫描函数。用两个枚举参数分别指定搜索方向和匹配模式,也就是向前或向后、属于或不属于集合。公开函数传入对应模式,调用者仍然使用含义清楚的函数名。

子串搜索可以分别保留正向和反向循环,共享长度检查、字节比较等必要逻辑。如果抽出辅助函数后反而需要大量特殊分支,保留两个简短循环通常更容易维护。

头文件中的宏如果直接调用某个 intl_* 函数,该函数仍须有可见声明和可链接的定义。intl_ 前缀本身不能让函数成为私有实现。只供 .c 文件内部调用的函数,才适合声明为 static。

函数指针适合什么情况

当库允许调用者提供匹配规则,例如自定义字符比较或分类谓词时,函数指针就有明确用途。当前这六种固定操作,用普通函数和私有枚举参数已经足够。

直接把 strstr、memchr 等标准库函数作为可替换策略也不方便。它们的参数类型、长度要求和返回值语义不同,通常还需要适配函数,不能靠强制转换函数指针来绕过签名差异。

性能要结合实际情况判断。逐字符执行的间接调用可能增加开销,编译器能否消除这些调用取决于优化条件。可以先写出清楚、直接的实现,拿到实际性能数据后再调整。

现有 find 应先修正范围判断

intl_find_str_range 中有几处范围判断会影响正确性。

strstr 找不到目标时返回 NULL,此时执行 found - str 属于未定义行为,因此必须先检查返回值。C11 草案 §6.5.6、§7.24.5.7

即使找到了目标,found - str 也是相对于整个字符串的下标,不能直接与窗口长度 count 比较:

(found - str) < count

例如,在 "abcdef" 的 pos = 3, count = 2 窗口中搜索 "de",正确结果是 3,上述判断却会拒绝它。

匹配还必须完整落在窗口内,只检查起点不够。例如,在 [0, 4) 中搜索 "de",起点 3 虽然位于窗口内,最后一个字符却已经越过窗口。

此外,strstr(str + pos, other) 不受 count 限制,可能继续扫描窗口之后的数据。对于有明确长度的字符串库,使用长度受控的比较更容易保证边界正确。

一个长度受控的实现示例

这个私有辅助函数按字节搜索,不依赖 cstring 的具体表示。调用者必须提供有效的数据指针及对应长度,长度不包括字符串末尾的终止符。

搜索规则如下:

  • pos > size 时返回 CSTR_NPOS。
  • count 超过剩余长度时,将窗口截断到字符串末尾。
  • 空目标在合法位置返回 pos,包括 pos == size。
  • 非空目标必须完整落在搜索窗口中。
#include <stddef.h>
#include <string.h>

/* CSTR_NPOS 使用库中已有的定义。 */
static size_t
cstr_find_range_impl(const char *str, size_t size,
                     const char *other, size_t other_size,
                     size_t pos, size_t count)
{
    if (pos > size) {
        return CSTR_NPOS;
    }

    size_t remaining = size - pos;

    if (count > remaining) {
        count = remaining;
    }

    if (other_size == 0) {
        return pos;
    }

    if (other_size > count) {
        return CSTR_NPOS;
    }

    size_t last = count - other_size;

    for (size_t offset = 0; offset <= last; ++offset) {
        if (memcmp(str + pos + offset, other, other_size) == 0) {
            return pos + offset;
        }
    }

    return CSTR_NPOS;
}

先检查再做减法,可以避免用 pos + count 判断边界时发生无符号溢出。循环只比较能够完整容纳目标子串的位置。

如果 const_cstring 可以直接作为 const char * 使用,且 cstr_size 返回逻辑字节长度,现有入口可以这样包装:

size_t
intl_find_str_range(const_cstring str, const_cstring other,
                    size_t pos, size_t count)
{
    return cstr_find_range_impl(
        str, cstr_size(str),
        other, cstr_size(other),
        pos, count
    );
}

如果接口也接受普通 C 字符串,应由相应的适配函数取得长度。确认表示方式之前,不能对普通 char * 调用 cstr_size。

这个朴素实现便于检查正确性,性能尚无测试结论。如果希望越界窗口直接返回失败,可以修改 count > remaining 分支,同时调整默认参数包装。此时不能继续传入整个 cstr_size(str) 来表示剩余窗口。

宏和反向搜索的注意事项

可以保留现有的 _Generic 思路,让宏只负责选择入口:

  • 先选择函数名,再统一传入实参。_Generic 的控制表达式不会求值,实参在实际函数调用中求值一次。
  • C 中的普通字符常量,例如 'a',类型是 int,字符分派需要覆盖这种调用。
  • 对于不支持的类型,可以省略 default,让编译器诊断。用 dummy_func 兜底容易产生难以理解的错误。
  • __cstr_find_* 以双下划线开头,属于保留标识符,应改为带库前缀的名称,例如 CSTR_PRIV_FIND_2。C11 草案 §6.4.4.4、§6.5.1.1、§7.1.3

反向循环不要使用 size_t i >= 0 这样的条件,因为 size_t 是无符号类型。应先处理空字符串,再从合法位置开始扫描;每次递减前先判断 i == 0,若成立就退出。

实现后,测试应覆盖空字符串、空目标、空字符集合、pos == size、越界位置、零长度窗口、跨窗口匹配,以及反向搜索命中下标 0 等情况。字符集合扫描如果使用查找表,应先将字节转换为 unsigned char,再作为下标。这类接口处理的是字节,不会自动获得 Unicode 字符语义。

备注:内容仅供参考。