第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拼接时间戳和消息内容,结果某天日志消息特别长,直接覆盖了栈上相邻的返回地址。程序崩溃还算好的,更可怕的是被攻击者利用执行任意代码。
看个例子:
#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;
}
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);
}
4.5 知识体系总览
下面这张图总结了本章的核心内容,帮你快速理清思路:
4.6 实战建议与总结
说了这么多,最后给几条实在的建议:
- 新代码中,别用strcpy和strcat。用snprintf替代,它更安全、更直观。
- 如果必须用strncpy,记得手动加'\0'。这是血的教训换来的经验。
- strncat的n参数是最大追加数,不是总大小。计算缓冲区大小时要加上已有字符串长度。
- 静态分析工具是你的朋友。我习惯在CI中集成cppcheck或clang-tidy,它们能自动检测这类问题。
- 单元测试覆盖边界情况。比如源字符串正好等于缓冲区大小、源字符串为空、目标缓冲区大小为1等。
我记得有一次,一个同事自信满满地说“我用了strncpy,肯定没问题”。结果我让他测试源字符串长度等于缓冲区大小的情况——直接崩了。从那以后,团队里再没人敢说“肯定没问题”了。
字符串操作看似简单,但越是基础的东西,越容易出大问题。希望今天的分享能帮你少踩几个坑。如果你有更好的替代方案,欢迎交流。
公众号:蓝海资料掘金营,微信deep3321