strcat 是 C 标准库中用于拼接字符串的函数,声明在 <string.h> 头文件中。它的作用是把一个源字符串追加到目标字符串末尾,返回目标字符串的起始地址。虽然用法看起来简单,但如果目标缓冲区空间不足、源字符串和目标字符串重叠,或者传入空指针,都会引发未定义行为。要真正用好 strcat,需要先理解它的内存模型和执行过程。

一、strcat 的函数原型与执行细节
strcat 的原型是 char *strcat(char *dest, const char *src);。第一个参数 dest 指向目标缓冲区,这个缓冲区必须已经包含一个以空字符 \0 结尾的字符串;第二个参数 src 是待追加的源字符串,同样以空字符结尾。函数会从 dest 的末尾开始,把 src 中的字符逐个复制过去,最后在新字符串末尾补上一个空字符。
这里有一个关键点:strcat 并不会检查 dest 剩余空间是否足够。它只关心 dest 当前已经存在的字符串长度,然后直接向后写入。如果 dest 后面的内存不足以容纳 src 的内容,就会覆盖相邻内存,造成缓冲区溢出。这也是 strcat 被很多安全编码规范列为不推荐使用的原因。函数返回 dest 的指针,因此可以链式调用,但一般不推荐用这种写法,因为会降低可读性。
从实现角度看,strcat 通常先遍历 dest 找到末尾的 \0,再从该位置开始复制 src。这意味着如果 dest 很长,函数的执行时间与 dest 长度和 src 长度都成正比。频繁在长字符串上调用 strcat 会导致平方级累积开销,需要留意性能问题。
二、strcat 的正确用法示例
下面是一个简单但完整的示例,演示如何为 strcat 准备足够大的目标缓冲区。注意目标数组大小必须至少等于两个字符串长度之和再加 1,用于存放最后的空字符。
#include <stdio.h>
#include <string.h>
int main(void) {
char dest[50] = "Hello, ";
char src[] = "world!";
strcat(dest, src);
printf("%s\n", dest);
return 0;
}
运行结果为 Hello, world!。这里 dest 的大小是 50,初始字符串占用 7 个字符,src 占用 6 个字符,再加上末尾空字符,总需求是 14 个字符,远小于 50,所以安全。
如果改成 char dest[12] = "Hello, ";,情况就危险了。初始字符串已经占满了 dest 的全部空间,strcat 会从第 12 个字节开始写入 world!,此时写入的已经是数组边界之外的内存。这类错误可能不会立即崩溃,但会破坏栈上的其他变量,甚至导致安全漏洞。因此使用 strcat 时,要么明确知道所有字符串的边界,要么改用带长度限制的函数。
另一个容易疏忽的地方是:目标缓冲区必须初始化。如果只写 char dest[50]; 而没有赋初值,就直接调用 strcat(dest, src);,程序会从 dest 的未初始化内容中寻找第一个空字符,这是未定义行为。正确的做法是先设置 dest[0] = '\0';,或者使用 strcpy 初始化。
三、常见误用:空指针、重叠与返回值
strcat 的两个参数都不能是空指针。如果传入 NULL,函数会尝试解引用空地址,通常导致段错误。在实际项目中,外部输入可能为空,调用前需要做判断。例如从配置文件中读取字符串后,应确认指针非空且长度大于 0 再拼接。
关于内存重叠,C 标准规定 strcat 的源和目的区域不能重叠。也就是说,src 不能指向 dest 缓冲区内部。如果尝试 strcat(dest, dest + 2);,复制过程中源数据可能被提前覆盖,结果不可预知。对于可能重叠的场景,应使用 memmove 手动处理,或者先把源字符串复制到临时缓冲区。
返回值方面,strcat 返回的是 dest 的起始地址。这个返回值允许写成 strcat(strcat(dest, a), b);,但这种嵌套写法会让分配空间的计算更困难。通常建议分开调用,确保中间每一步都有充足空间。最后,不要在字符串常量上调用 strcat,例如 strcat("hello", "world");,因为字符串常量一般存储在只读内存区域,写入会引发崩溃。必须使用字符数组作为目标。
四、更安全的选择:strncat 和自定义拼接函数
为了避免 strcat 的溢出风险,C 标准库提供了 strncat。它的原型是 char *strncat(char *dest, const char *src, size_t n);,最多从 src 复制 n 个字符,然后追加空字符。注意 n 是复制的最大字符数,不是目标缓冲区剩余空间的大小,因此使用时仍然要计算好剩余容量。例如:
#include <stdio.h>
#include <string.h>
int main(void) {
char dest[20] = "Status: ";
char src[] = "active";
strncat(dest, src, sizeof(dest) - strlen(dest) - 1);
printf("%s\n", dest);
return 0;
}
上面代码用 sizeof(dest) - strlen(dest) - 1 计算出还能容纳的字符数,并把它传给 strncat。这样即使 src 比剩余空间长,也不会越界写入。需要注意 strncat 在复制完 n 个字符后总是会追加空字符,所以计算剩余空间时必须减 1,为最后的空字符留位置。
如果项目对安全要求较高,还可以自己封装一个 safe_strcat 函数。思路是接收目标缓冲区总容量,内部先计算剩余空间,再用 strncat 或逐字符复制。这样调用者不需要每次手动计算长度。下面给出一个简单实现:
#include <stddef.h>
#include <string.h>
char *safe_strcat(char *dest, const char *src, size_t dest_size) {
size_t dest_len = strlen(dest);
if (dest_len >= dest_size) {
return dest;
}
size_t max_copy = dest_size - dest_len - 1;
strncat(dest, src, max_copy);
return dest;
}
这个封装会把目标缓冲区的总大小作为参数传入,函数内部先检查 dest_len 是否已经超过或等于 dest_size,如果是就不再写入。然后计算允许复制的最大字符数,并调用 strncat。相比直接使用 strcat,这种方式在接口层面就强制调用者提供缓冲区大小,能有效减少溢出风险。当然,这种方案也不是绝对安全,如果调用者传入错误的 dest_size,仍然可能出问题,所以函数内部还可以加入断言或运行时检查。
五、strcat 与性能:长字符串拼接的优化思路
在循环中反复调用 strcat 拼接大量字符串,会导致每次都要重新扫描目标字符串末尾。比如从 1 个字符开始,每次增加 1 个字符,第 k 次调用需要扫描 k 个字符,总时间复杂度为平方级。当 n 较大时性能会明显下降。对于这种场景,更推荐维护一个当前位置指针,直接用 strcpy 或 memcpy 从指针处写入,然后移动指针。
#include <stdio.h>
#include <string.h>
int main(void) {
char buf[256];
char *p = buf;
const char *parts[] = {"The", " ", "quick", " ", "brown", " ", "fox"};
size_t count = sizeof(parts) / sizeof(parts[0]);
for (size_t i = 0; i < count; i++) {
size_t len = strlen(parts[i]);
memcpy(p, parts[i], len);
p += len;
}
*p = '\0';
printf("%s\n", buf);
return 0;
}
这段代码没有调用 strcat,而是用 memcpy 把每个片段复制到缓冲区末尾,并手动维护指针 p。循环结束后补上空字符。这样每个字符只被复制一次,整体是线性复杂度。当然,这种写法需要自己保证缓冲区足够大,通常可以在拼接前用 strlen 累计总长度来检查容量。如果对内存安全要求更高,可以配合 snprintf 一次性格式化到缓冲区,但 snprintf 也有返回值和截断处理需要考虑。
总结来说,strcat 适合简单、边界明确的字符串拼接场景。只要目标缓冲区空间足够,且源字符串和目标字符串不重叠,就能正常工作。但在涉及外部输入、动态数据或高频拼接时,应优先考虑 strncat、自定义封装或维护指针的复制方式。理解这些细节,才能避免把看似简单的拼接操作变成安全隐患。

