字符串处理函数:strlen、strcpy、strcat、strcmp 的用法与手写实现

字符串处理,是 C 语言里绕不开的坎儿。说实话,我当年刚入行时,觉得这些函数太简单了,不就是数数长度、复制一下嘛。直到有一次线上服务因为 strcpy 缓冲区溢出直接崩溃,我才真正重视起来。嗯,今天咱们就把这四个函数彻底讲透——怎么用、怎么手写、怎么避坑。

1. strlen:别被长度骗了

strlen 返回的是字符串的长度,但不包括结尾的 '\0'。这个细节,我见过太多新手搞混。

#include <string.h>
char str[] = "Hello";
size_t len = strlen(str);  // len = 5,不是 6

为什么会这样?因为 strlen 从起始地址开始数,一直数到第一个 '\0' 为止。它不关心数组实际有多大,只关心 '\0' 在哪里。

⚠ 安全警告: 如果字符串没有以 '\0' 结尾,strlen 会一直往后数,直到遇到内存中的某个 '\0'。这可能导致返回一个巨大的值,甚至引发段错误。

手写一个 strlen 其实很简单:

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

我个人习惯用指针减法来算长度,这样代码更简洁。你想想看,如果传进来的是空指针怎么办?嗯,标准库没做这个检查,所以调用前你自己得保证 s 不为 NULL。

2. strcpy:复制有风险,拷贝需谨慎

strcpy 把源字符串复制到目标缓冲区,包括结尾的 '\0'。但它不做边界检查——这是最大的坑。

char dest[10];
char *src = "This is a very long string";
strcpy(dest, src);  // 缓冲区溢出!dest 只有 10 字节

我曾经在一个嵌入式项目里遇到过这个问题。当时有个日志模块用 strcpy 拼接路径,结果用户输入了一个超长文件名,直接把栈上的返回地址给覆盖了。嗯,从那以后,我团队里就立了个规矩:能用 strncpy 就别用 strcpy

手写实现:

char *my_strcpy(char *dest, const char *src) {
    char *ret = dest;
    while ((*dest++ = *src++) != '\0') {
        // 空循环体,赋值操作已经完成
    }
    return ret;
}
💡 小技巧: 注意看这个 while 条件——它先赋值,再判断赋值的结果是不是 '\0'。这样连结尾的 '\0' 也一起复制了,代码非常紧凑。

3. strcat:拼接字符串,小心越界

strcat 把源字符串追加到目标字符串的末尾。它会先找到目标字符串的 '\0',然后从那里开始复制。

char buf[20] = "Hello";
strcat(buf, " World");  // buf 变成 "Hello World"

但问题来了:如果 buf 剩余空间不够呢?strcat 照样会写出去。我建议你每次用 strcat 前,先算算还剩多少空间。

手写实现:

char *my_strcat(char *dest, const char *src) {
    char *ret = dest;
    // 先找到 dest 的末尾
    while (*dest != '\0') {
        dest++;
    }
    // 从末尾开始复制 src
    while ((*dest++ = *src++) != '\0') {
        // 空循环体
    }
    return ret;
}

说白了,strcat 就是先 strlen 定位,再 strcpy 复制。两个步骤合在一起。

4. strcmp:比较的不是长度,是字典序

strcmp 逐字节比较两个字符串的 ASCII 码值。它返回:

返回值 含义
< 0 第一个字符串小于第二个
0 两个字符串相等
> 0 第一个字符串大于第二个
if (strcmp("apple", "banana") < 0) {
    printf("apple 排在 banana 前面\n");
}

手写实现:

int my_strcmp(const char *s1, const char *s2) {
    while (*s1 != '\0' && *s2 != '\0' && *s1 == *s2) {
        s1++;
        s2++;
    }
    return (unsigned char)(*s1) - (unsigned char)(*s2);
}
🔑 关键点: 为什么最后要转成 unsigned char?因为 char 在某些平台上是有符号的,如果字符的 ASCII 码大于 127,直接相减可能得到负数。转成无符号能保证比较结果正确。

5. 常见安全问题与避坑指南

这四个函数,除了 strlen 是只读的,其他三个都可能造成缓冲区溢出。我总结了几条经验:

  • 永远不要相信输入长度。 用户输入、网络数据、文件内容,都可能超出你的预期。
  • 优先使用带长度限制的版本: strncpystrncatstrncmp。虽然它们也有自己的坑(比如 strncpy 不自动加 '\0'),但至少能控制写入量。
  • 手动加 '\0' 保平安。strncpy 后,记得在目标数组末尾手动置 '\0'
  • 检查返回值。 有些安全版本的函数会返回错误码,别忽略它们。
⚠ 我曾经踩过的坑: 有一次我写网络协议解析,用 strcat 拼接 HTTP 头部。结果某个请求的头部特别长,直接把后面的内存写坏了。从那以后,我所有字符串操作都改用 snprintf 或者自己写带长度检查的版本。

6. 知识体系图

下面这张图帮你理清这四个函数的关系和核心要点:

字符串处理函数核心知识体系 strlen 计算长度 strcpy 复制字符串 strcat 拼接字符串 strcmp 比较字符串 都依赖 '\0' 作为字符串结束标志 ⚠ 共同安全风险:缓冲区溢出 strcpy、strcat 不检查目标缓冲区大小,strcmp 可能读到非法内存 ✅ 推荐替代方案 strncpy / strncat / strncmp / snprintf(带长度限制的安全版本)
💡 我的建议: 如果你在写新代码,尽量用 snprintf 替代 strcpystrcat。它既能控制输出长度,又能自动加 '\0',省心很多。比如 snprintf(dest, sizeof(dest), "%s%s", prefix, suffix);strcat 安全得多。

好了,这四个函数的核心用法和手写实现就讲到这里。记住:字符串操作无小事,一个 '\0' 的缺失就可能让整个程序崩溃。写代码时多问自己一句:这个缓冲区够大吗?输入长度可控吗?养成这个习惯,能帮你省下无数调试时间。


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