第3章:字符串长度与比较——strlen、strcmp、strncmp 的实现原理与手写代码

大家好,我是你们的老朋友。

今天咱们聊聊字符串处理里最基础、也最容易被忽视的三个函数:strlenstrcmpstrncmp

你可能会说:“这有什么好讲的?直接用不就行了?”

嗯,我以前也这么想。直到有一次,我在一个嵌入式项目里,因为 strlen 的返回值类型踩了坑,导致程序在特定条件下死循环。从那以后,我养成了一个习惯:凡是标准库函数,一定要亲手实现一遍,搞清楚它的脾气

今天,我就带你把这些函数的“底裤”扒干净。

3.1 strlen:别被它的简单骗了

strlen 的作用很简单:返回字符串的长度,不包括结尾的 '\0'

但你真的了解它吗?

3.1.1 标准库的声明

#include <string.h>
size_t strlen(const char *s);

注意返回值类型:size_t。这是个无符号整数,在 32 位系统上是 unsigned int,在 64 位系统上是 unsigned long long

⚠️ 我曾经踩过的坑:
strlen 的返回值直接赋给 int 变量,然后和另一个 int 比较。当字符串长度超过 INT_MAX 时,size_tint 会溢出,结果变成负数。比较逻辑直接崩了。

3.1.2 手写一个 strlen

最直观的实现,就是遍历直到遇到 '\0'

size_t my_strlen(const char *s) {
    size_t len = 0;
    while (*s != '\0') {
        len++;
        s++;
    }
    return len;
}

这个版本没问题,但效率一般。每次循环只处理一个字节。

我个人的习惯是,在性能敏感的场景下,用“按字对齐”的方式优化。原理很简单:一次读 4 个或 8 个字节,然后检查其中是否有 '\0'

这里有个经典技巧:

#include <stddef.h>   // for size_t
#include <stdint.h>   // for uintptr_t

size_t fast_strlen(const char *str) {
    const char *s = str;

    // 1. 对齐到 4 字节边界
    while ((uintptr_t)s & 3) {
        if (*s == '\0') return (size_t)(s - str);
        s++;
    }

    // 2. 按 4 字节块读取
    const uint32_t *p = (const uint32_t *)s;
    while (1) {
        uint32_t v = *p;
        // 检查 v 中是否有字节为 0
        // 经典技巧:((v - 0x01010101) & ~v & 0x80808080)
        if (((v - 0x01010101) & ~v & 0x80808080) != 0) {
            // 有 '\0',逐字节定位
            const char *cp = (const char *)p;
            if (cp[0] == '\0') return (size_t)(cp - str);
            if (cp[1] == '\0') return (size_t)(cp - str + 1);
            if (cp[2] == '\0') return (size_t)(cp - str + 2);
            if (cp[3] == '\0') return (size_t)(cp - str + 3);
        }
        p++;
    }
}

这段代码看起来有点复杂,但核心思想就一句话:用位运算一次性检测 4 个字节里有没有 '\0'

💡 小提示:
((v - 0x01010101) & ~v & 0x80808080) 这个表达式,是检测 32 位整数中是否有字节为 0 的经典方法。原理是:如果某个字节为 0,减 0x01 后会借位,导致高位变化。配合掩码就能揪出来。

不过说实话,日常开发中直接用标准库的 strlen 就够了。GCC 和 MSVC 的运行时库早就用汇编优化过了,比我们自己手写的快得多。我写这个版本,主要是为了让你理解底层原理。

3.2 strcmp:字符串比较的“字典序”

strcmp 比较两个字符串,按字典序返回:

  • 返回值 < 0:第一个字符串小于第二个
  • 返回值 = 0:两个字符串相等
  • 返回值 > 0:第一个字符串大于第二个

3.2.1 标准库声明

int strcmp(const char *s1, const char *s2);

比较规则:逐字节比较,直到遇到不同字符或 '\0'。比较的是字符的 ASCII 码值。

3.2.2 手写 strcmp

int my_strcmp(const char *s1, const char *s2) {
    while (*s1 && (*s1 == *s2)) {
        s1++;
        s2++;
    }
    return (unsigned char)(*s1) - (unsigned char)(*s2);
}

注意最后一行:一定要转成 unsigned char

为什么?

因为 char 在有些编译器上是 signed 的。如果某个字符的 ASCII 码大于 127,直接相减可能得到负数,导致比较结果错误。

🔍 我遇到过的一个真实 bug:
一个同事用 strcmp 比较包含中文字符的字符串(GBK 编码,高位字节 > 127)。结果明明相同的字符串,返回值却不等于 0。查了半天,发现是 char 被当作有符号数处理了。从那以后,我写字符串比较函数,一律用 unsigned char

3.3 strncmp:带长度限制的比较

strncmpstrcmp 多一个参数:最多比较 n 个字符。

int strncmp(const char *s1, const char *s2, size_t n);

这个函数最大的价值是:防止越界

你想想看,如果两个字符串都不是以 '\0' 结尾的(比如从固定大小的缓冲区读来的数据),用 strcmp 会一直读下去,直到遇到内存中的某个 '\0'。这可能导致段错误,或者更糟——悄无声息地读到别人的数据。

3.3.1 手写 strncmp

int my_strncmp(const char *s1, const char *s2, size_t n) {
    if (n == 0) return 0;

    while (n-- > 0) {
        if (*s1 != *s2) {
            return (unsigned char)(*s1) - (unsigned char)(*s2);
        }
        if (*s1 == '\0') {
            break;  // 两个字符串都结束了
        }
        s1++;
        s2++;
    }
    return 0;
}

这里有个细节:先比较字符,再检查 '\0'。顺序不能反。

如果先检查 '\0',当两个字符串都结束时,会直接返回 0,但此时可能还没比较完 n 个字符。逻辑上没问题,但效率略低。我个人习惯先比较,因为大多数情况下字符串在前几个字符就分出胜负了。

3.4 知识体系总览

下面这张图,帮你把这三个函数的关系理清楚:

字符串长度与比较函数体系 strlen 返回字符串长度 strcmp 字典序比较 strncmp 带长度限制比较 增加 n 参数 实现要点: ① 返回值类型 size_t 无符号 ② 字符比较用 unsigned char ⚠️ 常见坑: ① strlen 返回值赋给 int 可能溢出 ② 非 '\0' 结尾字符串用 strcmp 会越界 🚀 优化方向: ① 按字对齐 + 位运算检测 '\0' ② 使用 SIMD 指令集加速

3.5 避坑指南与最佳实践

讲了这么多,最后总结几条我这些年积累的经验:

  1. 能用 strncmp 就别用 strcmp。除非你 100% 确定两个字符串都以 '\0' 结尾。我见过太多因为 strcmp 越界导致的崩溃。
  2. 注意 strncmp 的 n 参数。它比较的是字符数,不是字节数。对于多字节编码(如 UTF-8),一个字符可能占多个字节。这时候 strncmp 可能不是你想要的。
  3. 返回值不要直接当布尔值用。有些新手写 if (strcmp(a, b)),以为相等时返回 true。实际上相等时返回 0,在 C 语言里 0 表示 false。正确写法是 if (strcmp(a, b) == 0)
  4. 性能敏感场景考虑自定义实现。标准库的 strlen 虽然快,但如果你知道字符串的大致长度范围,可以自己写一个带上限的版本,避免扫描整个字符串。
💡 我的个人习惯:
在嵌入式项目中,我通常会写一个 safe_strlen 函数,接受一个最大长度参数。这样即使遇到非 '\0' 结尾的字符串,也不会死循环。

好了,这一章的内容就到这里。字符串长度和比较,看似简单,但细节里藏着魔鬼。希望你能亲手把这三个函数写一遍,相信我,写完之后你对字符串的理解会上一个台阶。

下次见!


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