第5章:字符串查找——strchr、strrchr、strstr 的用法与手写实现

大家好,欢迎来到《C语言字符串处理算法精讲》的第五讲。

今天咱们聊聊字符串查找。说白了,就是在一个字符串里找另一个字符或子串。你想想看,日常开发中这种需求多不多?解析配置文件、处理网络协议、过滤日志……几乎无处不在。

C标准库给了我们三个好用的工具:strchrstrrchrstrstr。但光会用还不够,你得知道它们怎么工作的,边界情况怎么处理,甚至能自己手写一个。嗯,这才是真本事。

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。

避坑指南:我曾经在解析二进制协议时,用 strchr 查找分隔符,结果没处理 NULL 返回就继续解引用指针,直接段错误。记住:使用前必须判空

5.2 strrchr:反向查找字符

strrchrstrchr 功能类似,但方向相反——从右往左找。函数原型:

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',直接返回。这个逻辑天然支持查找结束符。

小技巧:如果你要查找的字符在字符串中只出现一次,strchr 和 strrchr 结果一样。但如果有多个,strrchr 返回最后一个。我建议根据场景选,别搞混。

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 算法才降到秒级。

重要提醒:strstr 的 needle 参数不能为空指针,但可以是空字符串。标准规定:如果 needle 长度为 0,返回 haystack。这个细节在写代码时要注意。

5.4 三个函数的对比与选择

我整理了一个表格,方便你快速对比:

函数 查找目标 查找方向 返回值 时间复杂度
strchr 单个字符 从左到右 首次出现位置 / NULL O(n)
strrchr 单个字符 从右到左 最后一次出现位置 / NULL O(n)
strstr 子串 从左到右 首次出现位置 / NULL O(n*m) 朴素

怎么选?简单说:

  • 找单个字符用 strchrstrrchr
  • 找子串用 strstr
  • 需要最后一个匹配位置用 strrchr

别用 strstr 去找单个字符,虽然能工作,但效率不如 strchr。我见过有人这么干,纯粹是偷懒。

5.5 知识体系图

下面这张 SVG 图总结了本章的核心逻辑,帮你理清思路:

字符串查找函数体系 strchr 正向查找字符 strrchr 反向查找字符 strstr 查找子串 返回首次出现位置 支持查找 '\0' 返回最后出现位置 常用于路径解析 返回子串首次位置 空串匹配任意位置 手写实现要点:判空 → 遍历 → 比较 → 返回 核心原则:先判空,再遍历,边界情况('\0'、空串)要覆盖

5.6 避坑指南与个人经验

最后,我分享几个实战中遇到的坑,你记一下:

  1. 返回值判空:这是最基本的。我见过太多人直接拿返回值当字符串用,结果 NULL 指针解引用,程序崩溃。
  2. const 限定符:标准库函数参数是 const char *,但返回值是 char *。如果你传入的是 const 字符串,不要试图修改返回的指针内容。
  3. 字符类型转换strchrstrrchr 的第二个参数是 int,但内部会转成 char 比较。如果你传负数,行为可能不符合预期。
  4. 性能问题:在循环中反复调用 strstr 做大量文本匹配时,考虑用更高效的算法(如 KMP、Boyer-Moore)。我当年优化日志解析,把 strstr 换成 KMP,速度提升了 10 倍。
我的习惯:写代码时,我会把 strchr/strrchr/strstr 的返回值直接赋给一个临时指针,然后立即判空。这样既清晰又安全。别偷懒,一行代码搞定所有事往往容易出问题。

好了,字符串查找就讲到这里。这三个函数虽然简单,但用好了能解决很多实际问题。记住:理解原理比记住用法更重要。自己手写一遍,比看十遍文档都管用。


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