第3章:字符串长度与比较——strlen、strcmp、strncmp 的实现原理与手写代码
大家好,我是你们的老朋友。
今天咱们聊聊字符串处理里最基础、也最容易被忽视的三个函数:strlen、strcmp 和 strncmp。
你可能会说:“这有什么好讲的?直接用不就行了?”
嗯,我以前也这么想。直到有一次,我在一个嵌入式项目里,因为 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_t 转 int 会溢出,结果变成负数。比较逻辑直接崩了。
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,直接相减可能得到负数,导致比较结果错误。
一个同事用
strcmp 比较包含中文字符的字符串(GBK 编码,高位字节 > 127)。结果明明相同的字符串,返回值却不等于 0。查了半天,发现是 char 被当作有符号数处理了。从那以后,我写字符串比较函数,一律用 unsigned char。
3.3 strncmp:带长度限制的比较
strncmp 比 strcmp 多一个参数:最多比较 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 知识体系总览
下面这张图,帮你把这三个函数的关系理清楚:
3.5 避坑指南与最佳实践
讲了这么多,最后总结几条我这些年积累的经验:
- 能用
strncmp就别用strcmp。除非你 100% 确定两个字符串都以'\0'结尾。我见过太多因为strcmp越界导致的崩溃。 - 注意
strncmp的 n 参数。它比较的是字符数,不是字节数。对于多字节编码(如 UTF-8),一个字符可能占多个字节。这时候strncmp可能不是你想要的。 - 返回值不要直接当布尔值用。有些新手写
if (strcmp(a, b)),以为相等时返回 true。实际上相等时返回 0,在 C 语言里 0 表示 false。正确写法是if (strcmp(a, b) == 0)。 - 性能敏感场景考虑自定义实现。标准库的
strlen虽然快,但如果你知道字符串的大致长度范围,可以自己写一个带上限的版本,避免扫描整个字符串。
在嵌入式项目中,我通常会写一个
safe_strlen 函数,接受一个最大长度参数。这样即使遇到非 '\0' 结尾的字符串,也不会死循环。
好了,这一章的内容就到这里。字符串长度和比较,看似简单,但细节里藏着魔鬼。希望你能亲手把这三个函数写一遍,相信我,写完之后你对字符串的理解会上一个台阶。
下次见!