4、字符串处理:strcpy与strncpy的安全使用规范
字符串拷贝,C语言里最基础的操作之一。但说实话,这也是我见过出bug最多的地方。你想想看,一个简单的strcpy,用不好就能让你的程序崩得莫名其妙。我在项目中遇到过好几次线上事故,追查到最后,都是字符串拷贝越界惹的祸。
今天我们就来聊聊strcpy和strncpy这对“兄弟”。怎么用才安全?有哪些坑?我把自己踩过的雷都摊开来讲。
4.1 strcpy:简单但危险
先看个最典型的例子:
char src[] = "Hello, World!";
char dst[10];
strcpy(dst, src); // 危险!dst只有10字节,src有13字节
这段代码能编译通过,运行也不一定马上报错。但dst数组后面的内存已经被污染了。嗯,这就是传说中的缓冲区溢出。
为什么会这样?因为strcpy不检查目标缓冲区的大小。它只管把源字符串一个字节一个字节地复制,直到遇到'\0'为止。至于目标空间够不够?它不管。
我曾经在一个嵌入式项目中,接手过一段遗留代码。里面用了大量strcpy,结果产品在极端工况下偶尔会死机。查了三天,最后发现是一个字符串长度超过了预期,strcpy把栈上的返回地址给覆盖了。从那以后,我对strcpy就格外警惕。
4.2 strncpy:安全吗?也不尽然
很多人觉得,strncpy比strcpy安全,因为它多了一个长度参数。但说实话,strncpy也有自己的“小脾气”。
char src[] = "Hello";
char dst[10];
strncpy(dst, src, sizeof(dst)); // 这样安全吗?
这段代码看起来没问题。但strncpy有个特点:如果源字符串长度小于n,它会用'\0'填充剩余空间。如果源字符串长度大于等于n,它不会自动添加'\0'!
看这个例子:
char src[] = "Hello, World! This is a long string.";
char dst[10];
strncpy(dst, src, sizeof(dst));
// dst现在没有'\0'结尾!
printf("%s", dst); // 危险!可能打印出乱码
我个人习惯的做法是:
char dst[10];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0'; // 手动添加结束符
这样写,既保证了不会越界,又确保了字符串有正确的结尾。说白了,就是多写一行代码,换来一份安心。
4.3 安全拷贝的最佳实践
在实际项目中,我推荐使用snprintf或者自己封装一个安全的拷贝函数。snprintf的好处是:它一定会以'\0'结尾。
char dst[10];
snprintf(dst, sizeof(dst), "%s", src); // 安全,自动加'\0'
如果项目不允许用snprintf(比如某些嵌入式环境),那就自己写一个:
void safe_strcpy(char *dst, const char *src, size_t dst_size) {
if (dst == NULL || src == NULL || dst_size == 0) {
return; // 参数检查,不能少
}
size_t i;
for (i = 0; i < dst_size - 1 && src[i] != '\0'; i++) {
dst[i] = src[i];
}
dst[i] = '\0'; // 确保结尾
}
4.4 避坑指南
我曾经踩过的坑,列出来给你参考:
- 不要用strcpy处理用户输入——用户输入的长度不可控,用strcpy就是给自己埋雷。
- strncpy的第三个参数不要用sizeof(指针)——sizeof(指针)返回的是指针本身的大小,不是数组的大小。我见过有人写
strncpy(dst, src, sizeof(dst)),结果dst是char *类型,sizeof(dst)在32位系统上是4,在64位系统上是8。这能不出问题吗? - 注意源字符串是NULL的情况——strcpy和strncpy都不检查源指针是否为NULL。如果src是NULL,程序直接崩溃。
- 不要用strncpy处理二进制数据——strncpy遇到'\0'就停止,但二进制数据中间可能有'\0'。这种情况应该用memcpy。
4.5 知识体系图
下面这张图总结了字符串安全拷贝的核心逻辑,你可以对照着理解:
4.6 总结
字符串拷贝这件事,说白了就一句话:永远不要假设目标缓冲区够大。你想想看,程序跑在嵌入式设备上,内存就那么点,一个越界拷贝就能把整个系统搞崩。
我个人的建议是:
- 新代码一律不用strcpy,用snprintf或自定义安全函数
- 如果必须用strncpy,记得手动加'\0'
- 代码审查时,重点检查字符串拷贝相关的代码
- 单元测试覆盖边界情况:空字符串、满长度、超长字符串
记住,安全拷贝不是多此一举。你多写的那一行代码,可能就避免了一次线上事故。