在C语言里,字符串拼接是极常见的操作。标准库中的strcat和strncat都能把源字符串追加到目标字符串后面,但它们对内存边界和结束符的处理方式完全不同。理解这些差异,是避免缓冲区溢出漏洞的基础。

strcat的基本行为与隐患
strcat的函数原型为char *strcat(char *dest, const char *src)。它会先找到dest的当前结束符位置,然后从该位置开始逐个复制src中的字符,直到遇见src的结束符,最后在拼接结果后补一个' '。这个过程完全不关心dest指向的数组到底还有多少空闲空间。
下面是一段典型用法,看似正常,实则埋雷:
#include <stdio.h>
#include <string.h>
int main() {
char buf[8] = "ab";
char *more = "cdefghij";
strcat(buf, more); /* buf只有6字节剩余,却要接9字节 */
printf("%sn", buf);
return 0;
}
上述代码中,buf总长为8,已用掉"ab"加结束符共3字节,剩余5字节。而more长度为9(含结束符),strcat会越界写入,破坏栈上其他数据。这类错误在测试时可能不崩溃,但在不同编译优化下就会暴露,且常被攻击者利用。
因此,strcat只适合在开发者能绝对保证目标缓冲区足够大的场景使用,例如长度在编译期就确定的常量拼接。任何涉及用户输入或可变长度的场景,都应避免直接使用它。
strncat的受限拼接机制
strncat的原型是char *strncat(char *dest, const char *src, size_t n)。相比strcat,它多了参数n,表示最多从src复制n个字符到dest。更重要的是,无论是否复制满n个字符,strncat都会保证dest以结束符结尾。
需要注意,n的语义是“最多复制的字符数”,而不是“dest的总容量”。实际编码时应传入“剩余空间减一”,为结束符留位置。示例:
#include <stdio.h>
#include <string.h>
int main() {
char buf[8] = "ab";
char *more = "cdefghij";
size_t remain = sizeof(buf) - strlen(buf) - 1; /* 剩余可写字符数 */
strncat(buf, more, remain);
printf("%sn", buf); /* 输出 abcdefg */
return 0;
}
这里remain计算为4,strncat只复制"cdef"四个字符,然后补结束符,buf内容变为"abcdefg ",长度正好8,安全无越界。如果误把sizeof(buf)直接当n传入,则可能漏算结束符,虽不至于越界,但字符串可能未正常终止。
另外,若src长度小于n,strncat复制完src的结束符即停止,不会凑满n个字符。这个特性让它既安全又高效,是替代strcat的首选。
二者核心差异对比
从接口约束、结束符处理、安全性三方面可清晰区分两者:
| 对比维度 | strcat | strncat |
|---|---|---|
| 长度参数 | 无,依赖src结束符 | 有,限制复制字符数 |
| 边界检查 | 不检查目标剩余空间 | 按n限制,需调用者算对 |
| 结束符保证 | 总会补一个 | 总会补一个 |
| 典型风险 | 缓冲区溢出 | n算错导致截断或漏终止 |
可以看到,strncat把“安全阀门”交给了程序员:你必须正确计算剩余空间。如果算多了,仍会溢出;算少了,只是截断,不会崩。相对strcat的“必然危险”,strncat是“可控危险”。
在代码审查中,很多团队直接禁用strcat,规定所有拼接必须用strncat或更为现代的snprintf。例如用snprintf(buf, sizeof(buf), "%s%s", buf, more)也能达到受限拼接效果,且不易算错长度。
实践中的选用建议
当你面对一个已知固定长度且来源可信的拼接,比如拼接两个编译期常量路径片段,strcat简洁明了。但一旦目标缓冲区来自栈上数组、且源串来自外部,请立刻改用strncat,并养成用sizeof减strlen减一的习惯。
下面给出一个封装函数,降低手动计算出错概率:
#include <string.h>
/* 安全拼接,返回0表示完整拼接,非0表示被截断 */
int safe_cat(char *dest, size_t dest_size, const char *src) {
size_t dlen = strlen(dest);
if (dlen >= dest_size) return -1; /* 已无空间 */
size_t remain = dest_size - dlen - 1;
strncat(dest, src, remain);
return strlen(src) > remain ? 1 : 0;
}
该函数先确认dest当前长度未超限,再算剩余空间并调用strncat,最后通过源串长度与剩余空间比较判断是否发生截断。如此封装后,业务代码不再反复写长度计算逻辑,也统一了拼接失败的处理路径。
总结来说,strcat与strncat的根本区别在于“是否限制复制量”。在C语言没有原生边界检查的现实下,主动使用带长度参数的strncat,是写出健壮系统级代码的必备素养。
strcatstrncatstring_concatenation修改时间:2026-08-09 10:36:14