第5章:字符串查找——strchr、strrchr、strstr 的用法与手写实现
大家好,欢迎来到《C语言字符串处理算法精讲》的第五讲。
今天咱们聊聊字符串查找。说白了,就是在一个字符串里找另一个字符或子串。你想想看,日常开发中这种需求多不多?解析配置文件、处理网络协议、过滤日志……几乎无处不在。
C标准库给了我们三个好用的工具:strchr、strrchr 和 strstr。但光会用还不够,你得知道它们怎么工作的,边界情况怎么处理,甚至能自己手写一个。嗯,这才是真本事。
5.1 strchr:正向查找字符
strchr 的作用很简单:在字符串中从左到右查找第一个匹配的字符,返回指向该字符的指针。如果没找到,返回 NULL。
函数原型长这样:
char *strchr(const char *str, int c);
注意第二个参数是 int 类型,但实际传字符就行。C语言里字符本质就是整数,这个设计是为了兼容 EOF 之类的特殊值。
5.1.1 标准用法示例
#include <stdio.h>
#include <string.h>
int main() {
const char *url = "https://example.com/api/v1";
char *colon = strchr(url, ':');
if (colon) {
printf("找到冒号位置: %s\n", colon);
// 输出: 找到冒号位置: ://example.com/api/v1
}
return 0;
}
这个例子很典型。解析 URL 时,我们经常用 strchr 找冒号,然后判断协议类型。我在做嵌入式 HTTP 客户端时,就靠这个区分 http 和 https。
5.1.2 手写实现 strchr
标准库的实现通常很高效,但咱们得理解原理。我手写一个最朴素的版本:
char *my_strchr(const char *str, int c) {
if (str == NULL) return NULL;
while (*str != '\0') {
if (*str == (char)c) {
return (char *)str;
}
str++;
}
// 特殊处理:如果要找的是 '\0',返回字符串结尾
if (c == '\0') {
return (char *)str;
}
return NULL;
}
这里有个坑,我当年踩过。标准规定 strchr 可以查找 '\0' 字符,返回指向字符串结尾的指针。很多新手会忽略这个细节。你想想看,如果传入 strchr("hello", '\0'),应该返回 "hello" 末尾的 '\0' 位置,而不是 NULL。
5.2 strrchr:反向查找字符
strrchr 和 strchr 功能类似,但方向相反——从右往左找。函数原型:
char *strrchr(const char *str, int c);
这个函数在文件路径处理中特别有用。比如你要从 "/home/user/docs/report.pdf" 中提取文件名,用 strrchr 找最后一个 '/' 就对了。
5.2.1 典型应用:提取文件名
#include <stdio.h>
#include <string.h>
int main() {
const char *path = "/var/log/syslog";
char *last_slash = strrchr(path, '/');
if (last_slash) {
printf("文件名: %s\n", last_slash + 1);
// 输出: 文件名: syslog
}
return 0;
}
你看,加1跳过斜杠,直接拿到文件名。简洁高效。
5.2.2 手写实现 strrchr
实现思路:先找到字符串末尾,然后从后往前遍历。我习惯这样写:
char *my_strrchr(const char *str, int c) {
if (str == NULL) return NULL;
const char *p = str;
// 先走到字符串末尾
while (*p != '\0') {
p++;
}
// 从后往前找
while (p >= str) {
if (*p == (char)c) {
return (char *)p;
}
p--;
}
return NULL;
}
注意这里 p >= str 的判断。当 c 是 '\0' 时,第一次循环就会匹配到字符串结尾的 '\0',直接返回。这个逻辑天然支持查找结束符。
5.3 strstr:查找子串
strstr 是这三个里最强大的——它查找的是子串,不是单个字符。函数原型:
char *strstr(const char *haystack, const char *needle);
参数名很形象:haystack(干草堆)里找 needle(针)。找到返回 needle 首次出现的位置,没找到返回 NULL。
5.3.1 实际应用:关键词过滤
#include <stdio.h>
#include <string.h>
int main() {
const char *log = "ERROR: disk full at /dev/sda1";
const char *keyword = "ERROR";
if (strstr(log, keyword)) {
printf("发现错误日志,需要告警!\n");
}
return 0;
}
我在做日志监控系统时,就用 strstr 做关键词匹配。虽然简单,但够用。当然,生产环境可能用更高效的算法,但那是后话了。
5.3.2 手写实现 strstr(朴素匹配)
最直观的实现就是暴力匹配。外层循环遍历 haystack,内层循环逐字符比较:
char *my_strstr(const char *haystack, const char *needle) {
if (haystack == NULL || needle == NULL) return NULL;
// 空串匹配任何位置
if (*needle == '\0') return (char *)haystack;
const char *h, *n;
while (*haystack != '\0') {
h = haystack;
n = needle;
// 逐字符比较
while (*n != '\0' && *h == *n) {
h++;
n++;
}
// 如果 needle 全部匹配,返回当前位置
if (*n == '\0') {
return (char *)haystack;
}
haystack++;
}
return NULL;
}
这个算法的时间复杂度是 O(n*m),n 和 m 分别是两个字符串的长度。对于短字符串够用,但长文本就不太行了。我记得有一次处理几十万行的日志,用这个暴力匹配跑了快一分钟,后来换成 KMP 算法才降到秒级。
5.4 三个函数的对比与选择
我整理了一个表格,方便你快速对比:
| 函数 | 查找目标 | 查找方向 | 返回值 | 时间复杂度 |
|---|---|---|---|---|
| strchr | 单个字符 | 从左到右 | 首次出现位置 / NULL | O(n) |
| strrchr | 单个字符 | 从右到左 | 最后一次出现位置 / NULL | O(n) |
| strstr | 子串 | 从左到右 | 首次出现位置 / NULL | O(n*m) 朴素 |
怎么选?简单说:
- 找单个字符用
strchr或strrchr - 找子串用
strstr - 需要最后一个匹配位置用
strrchr
别用 strstr 去找单个字符,虽然能工作,但效率不如 strchr。我见过有人这么干,纯粹是偷懒。
5.5 知识体系图
下面这张 SVG 图总结了本章的核心逻辑,帮你理清思路:
5.6 避坑指南与个人经验
最后,我分享几个实战中遇到的坑,你记一下:
- 返回值判空:这是最基本的。我见过太多人直接拿返回值当字符串用,结果 NULL 指针解引用,程序崩溃。
- const 限定符:标准库函数参数是
const char *,但返回值是char *。如果你传入的是 const 字符串,不要试图修改返回的指针内容。 - 字符类型转换:
strchr和strrchr的第二个参数是int,但内部会转成char比较。如果你传负数,行为可能不符合预期。 - 性能问题:在循环中反复调用
strstr做大量文本匹配时,考虑用更高效的算法(如 KMP、Boyer-Moore)。我当年优化日志解析,把strstr换成 KMP,速度提升了 10 倍。
好了,字符串查找就讲到这里。这三个函数虽然简单,但用好了能解决很多实际问题。记住:理解原理比记住用法更重要。自己手写一遍,比看十遍文档都管用。
公众号:蓝海资料掘金营,微信deep3321