如何在 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 字符语义。
备注:内容仅供参考。
