标准库函数陷阱:printf/scanf格式字符串不匹配、gets函数的危险、setjmp/longjmp的未定义行为
标准库函数,听起来很安全对吧?毕竟是「标准」的。但我得说句实话——标准库恰恰是未定义行为的重灾区。很多开发者觉得库函数是「官方出品」,不会出问题。嗯,这个想法本身就很危险。
今天咱们就聊聊三个最常见的陷阱:printf/scanf的格式字符串不匹配、gets函数的危险、以及setjmp/longjmp的诡异行为。这三个坑,我在项目里都踩过,有的还踩了不止一次。
1. printf/scanf:格式字符串不匹配
先问个问题:printf("%d", 3.14) 会输出什么?
很多人觉得会输出 3,或者输出 3.14。实际上——这是未定义行为。编译器可能给你 3,可能给你一个随机数,也可能直接崩掉。
为什么会这样?因为 printf 是可变参数函数。它不知道你传进来的参数到底是什么类型,全靠格式字符串来「猜」。你告诉它 %d,它就按 int 去栈上取数据。但你传了个 double,占 8 字节,它只取 4 字节,剩下的数据就错位了。
我在项目中遇到过最离谱的一次:一个同事用 printf("%s", some_int) 来调试,结果程序直接段错误。查了半天才发现是格式符写错了。你想想看,一个 %s 就让 printf 把 int 值当成地址去解引用,不崩才怪。
%d对应 int,%ld对应 long,%lld对应 long long%f对应 double(注意:float 会被提升为 double)%x对应 unsigned int,%p对应指针%zu对应 size_t(很多人用 %d 打印 size_t,这是错的)
// 错误
printf("%d", (size_t)100); // 未定义行为
// 正确
printf("%zu", (size_t)100); // 输出 100
// 错误
printf("%f", 3.14f); // 虽然 float 会提升,但 %f 期望 double,没问题
// 但 %lf 在 printf 中与 %f 等价,在 scanf 中则不同
// scanf 中必须严格匹配
float f;
double d;
scanf("%f", &f); // 正确
scanf("%lf", &d); // 正确
scanf("%f", &d); // 未定义行为!
我个人习惯:所有 printf/scanf 的格式符,写完之后逐对检查一遍。特别是团队协作时,变量类型改了,格式符没改,这种 bug 最难查。
2. gets函数:标准库里的「定时炸弹」
gets 函数在 C11 标准中已经被正式移除。为什么?因为它不检查缓冲区长度。
你想想看:
char buf[16];
gets(buf); // 用户输入超过15个字符,直接溢出
就这么简单。gets 会一直读,直到遇到换行符。它不管你的 buf 有多大。输入 100 个字符,它就写 100 个字符到栈上,后面的 84 个字符全部写到别人的内存里去了。
我曾经在一个嵌入式项目里接手过遗留代码,里面用了 gets。那个产品偶尔会莫名其妙地重启,查了两个月才发现是 gets 导致的栈溢出。从那以后,我见到 gets 就像见到蟑螂一样——必须立刻消灭。
- fgets(buf, size, stdin) — 标准替代,会读取换行符
- getline() — POSIX 标准,自动分配缓冲区
- 自定义读取函数 — 嵌入式场景下常用
我曾经在代码审查中看到有人用 fgets(buf, sizeof(buf), stdin) 后直接使用 buf,没注意 fgets 会保留换行符。结果字符串比较总是失败。记得去掉末尾的 \n:
if (fgets(buf, sizeof(buf), stdin)) {
buf[strcspn(buf, "\n")] = '\0'; // 去掉换行符
}
3. setjmp/longjmp:非局部跳转的未定义行为
setjmp 和 longjmp 是 C 语言里最「反直觉」的机制之一。它们允许你从一个函数直接跳回到另一个函数的某个位置——说白了就是「跨函数 goto」。
但这里有个大坑:longjmp 跳回 setjmp 时,局部变量的状态是不确定的。
#include <setjmp.h>
#include <stdio.h>
jmp_buf buf;
void func() {
int local_var = 42;
longjmp(buf, 1);
// local_var 在 longjmp 后可能被恢复,也可能不被恢复
}
int main() {
int local_main = 100;
if (setjmp(buf) == 0) {
local_main = 200;
func();
} else {
// 这里 local_main 的值是 100 还是 200?
// 答案是:未定义行为!
printf("%d\n", local_main);
}
return 0;
}
为什么会这样?因为 setjmp 保存的是「执行环境」(寄存器、栈指针等),但不保证保存局部变量的值。编译器可能把局部变量优化到寄存器里,longjmp 恢复寄存器时,变量的值就「回到过去」了。
我在项目中遇到过类似问题:一个状态机用 setjmp/longjmp 做异常处理,结果某个状态变量在 longjmp 后「变回」了旧值,导致状态机逻辑混乱。调试了整整两天才找到原因。
- 只使用 volatile 局部变量 — 告诉编译器不要优化
- 或者使用全局变量/静态变量 — 这些不受 longjmp 影响
- 不要在 longjmp 后访问自动存储期的局部变量(除非是 volatile)
- 避免在信号处理函数中使用 longjmp — 这本身也是未定义行为
除非你非常清楚自己在做什么,否则尽量避免使用 setjmp/longjmp。在嵌入式系统中,我更推荐用状态机 + 错误码的方式处理异常流程。setjmp/longjmp 的隐式控制流太容易出问题了。
总结一下
这三个标准库陷阱,说白了都是「信任过度」的问题:
- printf/scanf — 别信格式符,自己检查
- gets — 别用,永远别用
- setjmp/longjmp — 慎用,用之前想清楚
标准库函数不是「免死金牌」。恰恰相反,正因为大家都在用,这些陷阱才更容易被忽视。嗯,记住一句话:C 语言不会保护你,它只会忠实地执行你写的代码——哪怕你写的是错的。
公众号:蓝海资料掘金营,微信 deep3321