【软件安全】fgets / strncpy / gets(不安全) / snprintf的对比
fgets / strncpy / gets / snprintf 做一个系统对比
gets 禁用 历史罪人
fgets 输入首选
strcpy = 无上限写 → 典型溢出源(更糟)。
strncpy = 有上限但可能不终止 → 容易踩坑,不是“安全 strcpy”。
snprintf:格式化输出首选
总览速查表
函数 | 典型签名 | 作用 | 上界控制 | 是否保证以 '\0' 结尾 | 常见坑点 | 结论 |
---|---|---|---|---|---|---|
fgets | char *fgets(char *s, int n, FILE *fp) | 从流(如 stdin )读入一行 | 是:最多读 n-1 个字节 | 保证(若 n>0 且成功读到) | 可能把换行符也读进来;EOF/错误 时返回 NULL | 首选的行输入;读完可“去掉换行” |
strncpy | char *strncpy(char *dst, const char *src, size_t n) | 拷贝最多 n 个字符 | 是:最多拷 n | 不保证(当 src 长度 ≥ n 时不补 '\0' ) | 易产生无终止和静默截断;短源会用 '\0' 填充剩余部分(低效) | 不推荐当作安全版 strcpy ;仅在定长记录场景可用 |
gets | char *gets(char *s) | 从 stdin 读一行 | 否:完全无上界 | 不适用 | 经典缓冲区溢出通道;已在 C11 移除 | 禁止使用 |
snprintf | int snprintf(char *s, size_t n, const char *fmt, …) | 格式化输出到缓冲区 | 是:最多写入 n-1 个可见字符 | 保证(C99+ 当 n>0 ) | 截断会发生;须检查返回值(需要的长度) | 首选的格式化写入;可用返回值做容量检查/重分配 |
1) fgets
—— 读取字符串的首选输入 API
如何工作
- 最多读取
n-1
个字符,遇到换行或 EOF 停止,把读到的内容放进s
,并写入终止'\0'
。 - 若缓冲区足够大且读到换行,换行也会留在字符串里(常见“尾部多一个
\n
”的问题)。
返回值
- 成功:返回
s
;失败(EOF/错误且没有读取到字符
):返回NULL
。
常见坑
- 忘了去掉尾部
\n
导致比较/打印时异常。 - 把
n
写成缓冲区大小以外的值。
推荐用法(去掉尾部换行)
char buf[256];
if (fgets(buf, sizeof buf, stdin)) {// 去掉尾部换行buf[strcspn(buf, "\n")] = '\0';// 使用 buf …
}
适用场景
- 从
stdin
/文件读入文本行,有上界且自动 NUL 终止,最安全易用。
2) strncpy
—— 不是“安全 strcpy”,更像“定长区域拷贝”
如何工作
-
从
src
拷贝最多n
个字符到dst
:- 若
src
长度< n
,用'\0'
填满剩余(零填充行为)。 - 若
src
长度≥ n
,不会写入终止'\0'
(这点最危险)。
- 若
返回值
- 返回
dst
指针。
常见坑
- 当
src
较长时,dst
无'\0'
终止,随后对dst
做字符串操作(如printf("%s", dst)
/strlen(dst)
)会越界读取。 - 被误当成“有界且总是安全”的复制函数;而零填充还会造成无谓的性能损耗。
安全使用(若非用不可)
char dst[16];
strncpy(dst, src, sizeof dst - 1);
dst[sizeof dst - 1] = '\0'; // 手动确保终止
更好的替代
- POSIX:
strlcpy(dst, src, sizeof dst)
(非标准,但常见于 BSD/部分 Linux 发行版)。 - 纯标准场景:已知长度时用
memcpy
+ 手动'\0'
;或直接用snprintf
。
适用场景
- 固定长度字段(如结构体里的定长数组,二进制记录),需要零填充才能对齐/复用的情况。不适合一般字符串复制。
3) gets
—— 历史罪人,已被删除
问题本质
- 不进行边界检查,读入直到换行/EOF,极易缓冲区溢出。
- C11 起已从标准库移除;大多数现代编译器会报错或强烈警告。
替代
- 用
fgets(buf, sizeof buf, stdin)
。
结论
- 永远不要使用。
4) snprintf
—— 安全的格式化输出首选
如何工作
-
把格式化后的字符串写入
s
,最多写n-1
个字符并自动补'\0'
(C99+ 前提:n>0
)。 -
返回值:欲写入的总长度(不含终止符)。
- 若返回值
>= n
,说明发生了截断;可据此扩容并重试。
- 若返回值
示例:一次写入,检查截断
char buf[32];
int need = snprintf(buf, sizeof buf, "user=%s id=%d", user, id);
if (need < 0) {// 格式化失败(很少见)
} else if ((size_t)need >= sizeof buf) {// 截断发生:need 是所需大小(不含 '\0')// 可选择:分配 need+1 大小重试
}
避免的坑
- 忽视返回值,导致静默截断、关键信息丢失。
- 传错
n
(应当是缓冲区真实大小)。
适用场景
- 一切“打印到字符串”的需求;替代
sprintf
/vsprintf
。
该选谁?(实践建议)
- 读文本行 → 用
fgets
,随后按需去掉换行。 - 格式化拼接 → 用
snprintf
,并检查返回值是否截断。 - 二进制/定长记录拷贝 → 特殊场景可用
strncpy
(随后手动终止或只作定长字段拷贝,不当作 C 字符串)。 - 禁止 →
gets
(已移除,安全性极差)。
常见安全替代 & 小技巧
- 去换行:
buf[strcspn(buf, "\n")] = '\0';
- 拼路径/键值:统一用
snprintf
,避免strcat/strcpy
。 - 要“安全复制字符串”:优先
snprintf(dst, n, "%s", src)
或strlcpy
(若可用)。 - 读取整行且不丢字符:
fgets
循环读取直到遇到\n
,或使用 POSIXgetline
(自动扩容,非 C 标准)。
迷你示例:综合使用
// 从 stdin 读一行用户名,然后安全拼接日志前缀
char user[64];if (!fgets(user, sizeof user, stdin)) {fprintf(stderr, "input error\n");return 1;
}
user[strcspn(user, "\n")] = '\0'; // 去掉尾部换行char line[128];
int need = snprintf(line, sizeof line, "[login] user=%s", user);
if (need < 0) {fprintf(stderr, "format error\n");return 1;
}
if ((size_t)need >= sizeof line) {fprintf(stderr, "truncated: needed %d bytes\n", need + 1);// 可选择:动态分配 need+1 继续写
}puts(line);
小结
gets
:禁用;fgets
:读输入首选;snprintf
:格式化输出首选(记得看返回值);strncpy
:不是安全版strcpy
,除定长记录外尽量避免,并手动补'\0'
。