第4章:字符串拷贝与拼接——strcpy、strncpy、strcat、strncat 的安全隐患与替代方案

大家好,我是老刘。今天咱们聊聊C语言里最基础、也最容易翻车的话题——字符串拷贝和拼接。

说实话,我见过太多新手(甚至工作两三年的工程师)在这几个函数上栽跟头。我自己刚入行时也踩过坑,有一次线上服务因为一个strcpy导致内存越界,半夜被叫起来修bug。嗯,从那以后,我对这几个函数就格外小心了。

4.1 strcpy:最熟悉的陌生人

先看strcpy。它的原型很简单:

char *strcpy(char *dest, const char *src);

作用就是把src指向的字符串(包括结尾的'\0')拷贝到dest指向的内存空间。

但问题来了——它不检查目标缓冲区的大小。你想想看,如果src比dest长,会发生什么?

⚠️ 核心隐患: strcpy 不会做边界检查。一旦源字符串长度超过目标缓冲区,就会发生缓冲区溢出。这是C语言最经典的安全漏洞之一。

我曾在项目中遇到过这样一个案例:一个日志模块用strcpy拼接时间戳和消息内容,结果某天日志消息特别长,直接覆盖了栈上相邻的返回地址。程序崩溃还算好的,更可怕的是被攻击者利用执行任意代码。

看个例子:

#include <stdio.h>
#include <string.h>

int main() {
    char dest[10];
    char *src = "Hello, this is a very long string!";
    
    strcpy(dest, src);  // 危险!dest只有10字节,src远超10字节
    
    printf("dest = %s\n", dest);
    return 0;
}

这段代码编译不会报错,运行也不会立即崩溃——但dest后面的内存已经被污染了。这种bug最难排查,因为它可能过几个小时才暴露出来。

4.2 strncpy:看似安全,实则暗藏玄机

为了解决strcpy的问题,C标准引入了strncpy:

char *strncpy(char *dest, const char *src, size_t n);

它多了一个参数n,表示最多拷贝n个字符。听起来很安全,对吧?

但我要告诉你——strncpy也有坑,而且坑还不小

第一个坑:如果src长度大于等于n,strncpy不会在dest末尾添加'\0'。这意味着你拷贝完的字符串可能没有终止符!

第二个坑:如果src长度小于n,strncpy会用'\0'填充剩余空间。这在性能敏感的场景下是个浪费。

看代码:

#include <stdio.h>
#include <string.h>

int main() {
    char dest[10];
    char *src = "Hello!";
    
    strncpy(dest, src, sizeof(dest));  // 拷贝6个字符 + 4个'\0'填充
    
    printf("dest = %s\n", dest);  // 正常打印
    
    // 但如果是这样:
    char *long_src = "Hello World!";
    strncpy(dest, long_src, sizeof(dest));  // 拷贝10个字符,不追加'\0'
    dest[sizeof(dest) - 1] = '\0';  // 必须手动加!否则printf会越界
    
    printf("dest = %s\n", dest);
    return 0;
}
💡 我的习惯: 每次用strncpy之后,我都会手动在dest[n-1]位置写'\0'。这已经成了肌肉记忆。

4.3 strcat 与 strncat:拼接的陷阱

strcat和strcpy类似,只是把拷贝改成了拼接:

char *strcat(char *dest, const char *src);

它从dest的'\0'位置开始,把src的内容追加过去。同样的问题——不检查剩余空间

我见过最经典的bug是这样的:

char path[256] = "/home/user/";
char *filename = "a_very_long_filename_that_exceeds_buffer_size.txt";
strcat(path, filename);  // 如果拼接后超过256字节,就炸了

strncat稍微好一点:

char *strncat(char *dest, const char *src, size_t n);

它最多追加n个字符,并且总会追加'\0'。这一点比strncpy强。

但要注意:n是最大追加字符数,不包括'\0'。所以目标缓冲区至少要有 strlen(dest) + n + 1 的空间。

📌 关键区别:
  • strncpy:n表示最大拷贝字符数,不保证'\0'终止
  • strncat:n表示最大追加字符数,保证'\0'终止

4.4 安全替代方案

说了这么多问题,那到底该用什么?我推荐几个方案:

方案一:snprintf(我最常用的)

#include <stdio.h>

char dest[100];
snprintf(dest, sizeof(dest), "%s", src);  // 安全拷贝
snprintf(dest, sizeof(dest), "%s%s", prefix, suffix);  // 安全拼接

snprintf的好处是:永远保证输出以'\0'结尾,并且不会超过指定大小。它返回的是如果空间足够本该写入的字符数,你可以据此判断是否被截断。

我个人习惯用snprintf替代所有strcpy和strcat的场景。虽然多写几个字符,但心里踏实。

方案二:strlcpy / strlcat(BSD系)

如果你在BSD或macOS上开发,可以用这两个函数:

size_t strlcpy(char *dest, const char *src, size_t size);
size_t strlcat(char *dest, const char *src, size_t size);

它们的设计更合理:保证'\0'终止,返回源字符串长度。如果返回值大于等于size,说明被截断了。

但注意:这不是标准C函数,Linux上默认没有。你需要自己实现或使用第三方库。

方案三:自己封装安全函数

我曾经在一个嵌入式项目中,因为平台限制不能依赖标准库扩展,就自己写了个简单的封装:

#include <string.h>
#include <stdio.h>

// 安全拷贝,返回实际拷贝字符数(不含'\0')
size_t safe_strcpy(char *dest, size_t dest_size, const char *src) {
    if (dest_size == 0) return 0;
    
    size_t src_len = strlen(src);
    size_t copy_len = (src_len < dest_size - 1) ? src_len : (dest_size - 1);
    
    memcpy(dest, src, copy_len);
    dest[copy_len] = '\0';
    
    return copy_len;
}

// 安全拼接
size_t safe_strcat(char *dest, size_t dest_size, const char *src) {
    size_t dest_len = strlen(dest);
    size_t remaining = dest_size - dest_len;
    
    if (remaining <= 1) return dest_len;  // 没有剩余空间
    
    return dest_len + safe_strcpy(dest + dest_len, remaining, src);
}
💡 避坑指南: 我曾经在封装时忘记处理dest_size为0的情况,结果线上出现除零错误。所以边界条件一定要考虑周全。

4.5 知识体系总览

下面这张图总结了本章的核心内容,帮你快速理清思路:

字符串拷贝与拼接:函数对比与安全方案 strcpy 不检查边界,高危 strncpy 不保证'\0'终止 strcat 不检查剩余空间 strncat 相对安全,注意n的含义 核心问题:缓冲区溢出、缺少'\0'终止、语义不一致 snprintf 标准C,保证'\0'终止 推荐首选 strlcpy/strlcat BSD系,设计合理 非标准,需移植 自定义封装 完全可控 适合嵌入式/特殊场景 核心原则:永远假设缓冲区不够大,永远手动保证'\0'终止

4.6 实战建议与总结

说了这么多,最后给几条实在的建议:

  1. 新代码中,别用strcpy和strcat。用snprintf替代,它更安全、更直观。
  2. 如果必须用strncpy,记得手动加'\0'。这是血的教训换来的经验。
  3. strncat的n参数是最大追加数,不是总大小。计算缓冲区大小时要加上已有字符串长度。
  4. 静态分析工具是你的朋友。我习惯在CI中集成cppcheck或clang-tidy,它们能自动检测这类问题。
  5. 单元测试覆盖边界情况。比如源字符串正好等于缓冲区大小、源字符串为空、目标缓冲区大小为1等。

我记得有一次,一个同事自信满满地说“我用了strncpy,肯定没问题”。结果我让他测试源字符串长度等于缓冲区大小的情况——直接崩了。从那以后,团队里再没人敢说“肯定没问题”了。

字符串操作看似简单,但越是基础的东西,越容易出大问题。希望今天的分享能帮你少踩几个坑。如果你有更好的替代方案,欢迎交流。


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