
在 C 语言开发中,字符串操作是基础且高频的需求,而字符串拼接(将一个字符串追加到另一个字符串末尾)更是核心场景之一。C 标准库提供的strcat()和strncat()函数,是实现这一功能的常用工具,但二者在安全性、适用场景上差异显著。不少开发者(尤其是初学者)容易混淆二者逻辑,或忽略潜在风险(如缓冲区溢出),导致程序出现未定义行为甚至安全漏洞。
字符串连接的本质,是将 “源字符串”(src)的字符序列,从 “目标字符串”(dest)的终止符'\0'位置开始写入,最终形成一个新的连续字符串(仍以'\0'结尾)。C 标准库的strcat()和strncat()均服务于这一目标,但设计思路不同:
二者均属于<string.h>头文件,需包含该头文件才能使用;且返回值均为目标字符串dest的起始地址,支持链式调用(如printf("%s", strcat(dest, src)))。
函数原型是理解函数用法的核心,需明确每个参数的含义、类型及约束条件,避免因参数误用导致错误。
1. strcat () 原型
char *strcat(char *dest, const char *src);参数 1:char dest
参数 2:const char src
返回值:char
2. strncat () 原型
char *strncat(char *dest, const char *src, size_t n);前两个参数:与strcat()完全一致,约束条件相同(dest可修改、以'\0'结尾;src只读)。 参数 3:size_t n
返回值:与strcat()一致,返回dest的起始地址。
理解函数的实现逻辑,能帮我们更深刻地掌握其行为(如为何strcat()会溢出、strncat()为何安全)。以下伪代码基于 C 标准库的设计思路,简化了边界检查(实际库函数会有更严谨的错误处理),聚焦核心流程。
1. strcat () 实现伪代码
// 模拟strcat()逻辑:将src全部追加到dest末尾
char *my_strcat(char *dest, const char *src) {
// 1. 检查空指针(实际库函数可能直接崩溃,此处简化处理)
if (dest == NULL || src == NULL) {
return NULL; // 空指针直接返回错误
}
// 2. 保存dest的起始地址(用于最终返回)
char *temp = dest;
// 3. 找到dest的终止符'\0'(定位追加起始位置)
while (*dest != '\0') {
dest++; // 指针后移,直到指向'\0'
}
// 4. 将src的字符逐个复制到dest末尾(包括src的'\0')
while (*src != '\0') {
*dest = *src; // 复制当前字符
dest++; // dest指针后移
src++; // src指针后移
}
// 5. 确保拼接后以'\0'结尾(实际上述循环已复制src的'\0',此处冗余但安全)
*dest = '\0';
// 6. 返回dest的起始地址
return temp;
}核心逻辑总结:先 “找 dest 的末尾”,再 “复制 src 全量字符”,最后 “补终止符”。因未检查dest剩余空间,若src长度超过dest剩余容量,会直接覆盖dest后续内存(缓冲区溢出)。
2. strncat () 实现伪代码
// 模拟strncat()逻辑:最多追加n个字符到dest末尾
char *my_strncat(char *dest, const char *src, size_t n) {
// 1. 空指针或n=0直接返回dest(n=0时不追加)
if (dest == NULL || src == NULL || n == 0) {
return dest;
}
// 2. 保存dest起始地址
char *temp = dest;
// 3. 找到dest的终止符'\0'
while (*dest != '\0') {
dest++;
}
// 4. 复制最多n个字符(或到src的'\0'为止)
size_t count = 0; // 已复制的字符数
while (count < n && *src != '\0') {
*dest = *src;
dest++;
src++;
count++; // 计数+1,避免超过n
}
// 5. 强制补充'\0'(关键!无论是否复制满n个字符)
*dest = '\0';
// 6. 返回dest起始地址
return temp;
}核心逻辑总结:在strcat()基础上增加 “计数控制”(count < n),且无论复制多少字符,强制补'\0'—— 这是strncat()安全性的核心:即使src无'\0',也不会无限读取;即使复制满n个字符,也不会遗漏终止符。
二者的适用场景需结合 “源字符串长度是否确定”“目标缓冲区空间是否可控” 两个核心因素判断,避免盲目使用。
仅当源字符串长度已知,且目标缓冲区剩余空间足够容纳源字符串时,才考虑使用strcat()—— 此时它的 “简洁性” 能简化代码,无需额外计算长度。
典型场景:
示例:char dest[20] = "Hello, "; char src[] = "World!"; strcat(dest, src);(src长度 6,dest初始长度 7,剩余空间 13,足够容纳)。
当源字符串长度不确定(如用户输入、网络数据、文件读取),或目标缓冲区空间有限时,必须优先使用strncat()—— 它的 “长度限制” 能避免缓冲区溢出,是安全性的关键。
典型场景:
示例:用户输入昵称(最长 10 字符),追加到"User: "后,用strncat(dest, input, 10)确保最多追加 10 个字符。
C 语言字符串操作的 “坑” 多集中在 “终止符”“内存边界”“类型匹配” 上,strcat()和strncat()也不例外,需逐一规避。
(1)目标字符串不能是字符串常量
strcat()和strncat()都会修改dest的内容,若dest是字符串常量(如char *dest = "Hello";),则存于只读内存区(如 Linux 的.rodata段),修改会触发 “段错误(Segmentation Fault)”。
错误示例:
char *dest = "Hello"; // 字符串常量,只读
char src[] = "World";
strcat(dest, src); // 错误:修改只读内存,触发段错误正确做法:dest必须是可修改的字符数组,如char dest[20] = "Hello";。
(2)目标字符串必须以'\0'结尾
若dest无'\0'(如char dest[10] = {'H','e','l','l','o'};,未初始化剩余空间),函数会持续向后查找'\0',导致 “读取越界”(访问dest外的内存),触发未定义行为(程序崩溃、乱码)。
错误示例:
char dest[10] = {'H','e','l','l','o'}; // 无'\0',剩余空间随机值
char src[] = "World";
strcat(dest, src); // 错误:找不到'\0',读取越界正确做法:初始化时显式加'\0',或用字符串常量初始化(自动补'\0'):char dest[10] = "Hello";(自动在第 5 位加'\0')。
(3)避免源字符串与目标字符串内存重叠
若src和dest的内存区域重叠(如dest是"ABCDEF",src是dest+2,即"CDEF"),拼接时会覆盖src的未读取部分,导致结果错误。
错误示例:
char dest[20] = "ABCDEF";
char *src = dest + 2; // src指向"CD EF"(内存重叠)
strcat(dest, src); // 预期"ABCDEFCD EF",实际会覆盖src,结果乱码正确做法:确保src和dest内存不重叠,若需重叠拼接,需用临时缓冲区中转。
strcat()的最大风险是无长度检查,若dest剩余空间 < src长度(含'\0'),会直接覆盖dest后续内存,可能导致:
错误示例(溢出风险):
char dest[10] = "Hello, "; // 长度7(含'\0'),剩余空间3
char src[] = "World!"; // 长度6(含'\0'),需6字节空间
strcat(dest, src); // 错误:剩余空间3 < 6,缓冲区溢出运行结果:dest会覆盖后续内存,可能输出乱码,或直接崩溃。
(1)n 是 “最大追加字符数”,不是 “最终总长度”
n仅限制从src中读取的字符数(不含'\0'),最终dest的总长度 = 初始长度 + 实际追加字符数 + 1('\0')。若误将n设为 “目标总长度”,会导致空间不足。
错误示例:
char dest[10] = "Hi, "; // 初始长度3(含'\0'),目标总长度10
strncat(dest, src, 10); // 错误:n=10表示最多追加10个字符,总长度会超10正确做法:n = 缓冲区总大小 - 初始长度 - 1(留'\0'空间):
size_t max_add = sizeof(dest) - strlen(dest) - 1; // 10-3-1=6
strncat(dest, src, max_add); // 最多追加6个字符,总长度3+6+1=10,刚好(2)n=0 时不追加任何字符,但仍会补'\0'
若n=0,strncat()仅会在dest的当前'\0'位置重新写一个'\0'(无实际影响),不会修改src或dest的其他内容。
示例:
char dest[20] = "Test";
strncat(dest, "ABC", 0);
printf("%s", dest); // 输出"Test",无变化(3)src 无'\0'时,最多追加 n 个字符
若src是无'\0'的字符数组(如char src[] = {'A','B','C'};),strncat()会读取最多n个字符,不会无限读取(避免越界),且最终补'\0'。
示例:
char dest[10] = "Data: ";
char src[] = {'A','B','C'}; // 无'\0'
strncat(dest, src, 5); // 最多读5个,但src仅3个,实际追加3个
printf("%s", dest); // 输出"Data: ABC"(自动补'\0')结合实际场景编写示例,对比正确与错误用法,直观理解函数的使用方式。
1. strcat () 示例:正确用法与错误示范
(1)正确用法:固定长度拼接
#include <stdio.h>
#include <string.h>
int main() {
// 目标缓冲区:大小20,初始内容"Hello, "(长度7,含'\0')
char dest[20] = "Hello, ";
// 源字符串:长度6("World!" + '\0'),dest剩余空间13,足够容纳
char src[] = "World!";
// 调用strcat()拼接
char *result = strcat(dest, src);
// 输出结果(result与dest地址相同)
printf("拼接结果:%s\n", result);
printf("dest地址:%p,result地址:%p\n", dest, result);
return 0;
}输出结果:
拼接结果:Hello, World!
dest地址:0x7ffeefbff460,result地址:0x7ffeefbff460(2)错误示范:缓冲区溢出
#include <stdio.h>
#include <string.h>
int main() {
// 目标缓冲区:大小10,初始内容"Hello, "(长度7)
char dest[10] = "Hello, ";
// 源字符串:长度10("LongString" + '\0'),dest剩余空间3,不足容纳
char src[] = "LongString";
// 错误:src长度10 > dest剩余空间3,缓冲区溢出
strcat(dest, src);
// 输出结果:可能乱码或程序崩溃(未定义行为)
printf("拼接结果:%s\n", dest);
return 0;
}运行风险:dest仅剩余 3 字节空间,src需 10 字节,会覆盖dest后续内存,可能导致:
2. strncat () 示例:安全处理用户输入
(1)基础用法:限制追加长度
#include <stdio.h>
#include <string.h>
int main() {
// 目标缓冲区:"User: "(长度6,含'\0'),总大小20
char dest[20] = "User: ";
// 源字符串:长度15(超过缓冲区剩余空间)
char src[] = "ThisIsALongUsername";
// 计算最大可追加长度:总大小 - 初始长度 - 1(留'\0')
size_t max_add = sizeof(dest) - strlen(dest) - 1; // 20-6-1=13
// 调用strncat(),最多追加13个字符
strncat(dest, src, max_add);
printf("拼接结果:%s\n", dest);
printf("拼接后长度:%zu\n", strlen(dest));
return 0;
}输出结果:
拼接结果:User: ThisIsALongUse
拼接后长度:19(6+13,留1字节'\0')关键:通过max_add计算剩余空间,确保不溢出 —— 即使src更长,也仅追加 13 个字符。
(2)实战场景:处理用户输入
#include <stdio.h>
#include <string.h>
#define MAX_INPUT 10 // 用户输入最大长度
#define DEST_SIZE 20 // 目标缓冲区大小
int main() {
char dest[DEST_SIZE] = "Welcome, ";
char user_input[MAX_INPUT + 1]; // 存用户输入,+1留'\0'
// 提示用户输入
printf("请输入你的昵称(最多10个字符):");
// 读取用户输入(限制10个字符,避免溢出)
fgets(user_input, sizeof(user_input), stdin);
// 移除fgets()读取的换行符(若有)
size_t input_len = strlen(user_input);
if (input_len > 0 && user_input[input_len - 1] == '\n') {
user_input[input_len - 1] = '\0';
}
// 计算最大可追加长度
size_t max_add = DEST_SIZE - strlen(dest) - 1;
// 追加用户输入,最多max_add个字符
strncat(dest, user_input, max_add);
printf("最终欢迎语:%s\n", dest);
return 0;
}运行示例:
为更清晰地梳理二者差异,下表从 7 个核心维度进行对比,方便快速查阅:
对比维度 | strcat() | strncat() |
|---|---|---|
函数参数 | 2 个(dest, src) | 3 个(dest, src, n) |
追加长度逻辑 | 追加 src 全部字符(直到 '\0') | 追加最多 n 个字符,或到 src 的 '\0' 为止 |
缓冲区溢出风险 | 无长度检查,风险高 | 有 n 限制,风险低(需正确设 n) |
对 src 的依赖 | 必须以 '\0' 结尾,否则读取越界 | 可无 '\0',最多读 n 个字符 |
终止符处理 | 复制 src 的 '\0' 作为终止符 | 强制添加 '\0',无论复制多少字符 |
适用场景 | 源长度已知、dest 空间足够 | 源长度未知、dest 空间有限 |
代码复杂度 | 简洁,无需计算长度 | 需计算 max_add(n 的合理值) |
博主简介 byte轻骑兵,现就职于国内知名科技企业,专注于嵌入式系统研发,深耕 Android、Linux、RTOS、通信协议、AIoT、物联网及 C/C++ 等领域。乐于技术分享与交流,欢迎关注互动! 📌 主页与联系方式
⚠️ 版权声明 本文为原创内容,未经授权禁止转载。商业合作或内容授权请联系邮箱并备注来意。