标准库函数陷阱:printf/scanf格式字符串不匹配、gets函数的危险、setjmp/longjmp的未定义行为

标准库函数,听起来很安全对吧?毕竟是「标准」的。但我得说句实话——标准库恰恰是未定义行为的重灾区。很多开发者觉得库函数是「官方出品」,不会出问题。嗯,这个想法本身就很危险。

今天咱们就聊聊三个最常见的陷阱:printf/scanf的格式字符串不匹配gets函数的危险、以及setjmp/longjmp的诡异行为。这三个坑,我在项目里都踩过,有的还踩了不止一次。

标准库函数三大陷阱 printf/scanf 格式字符串不匹配 gets函数 缓冲区溢出风险 setjmp/longjmp 非局部跳转陷阱 栈破坏 / 数据错乱 程序崩溃 / 安全漏洞 缓冲区溢出 任意代码执行风险 栈帧状态不一致 未定义行为 ✅ 规避建议 使用 snprintf / fgets / 检查格式匹配 / 避免 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 就像见到蟑螂一样——必须立刻消灭。

⚠️ 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 后「变回」了旧值,导致状态机逻辑混乱。调试了整整两天才找到原因。

✅ 安全使用 setjmp/longjmp 的规则:
  1. 只使用 volatile 局部变量 — 告诉编译器不要优化
  2. 或者使用全局变量/静态变量 — 这些不受 longjmp 影响
  3. 不要在 longjmp 后访问自动存储期的局部变量(除非是 volatile)
  4. 避免在信号处理函数中使用 longjmp — 这本身也是未定义行为
💡 我个人建议:

除非你非常清楚自己在做什么,否则尽量避免使用 setjmp/longjmp。在嵌入式系统中,我更推荐用状态机 + 错误码的方式处理异常流程。setjmp/longjmp 的隐式控制流太容易出问题了。

总结一下

这三个标准库陷阱,说白了都是「信任过度」的问题:

  • printf/scanf — 别信格式符,自己检查
  • gets — 别用,永远别用
  • setjmp/longjmp — 慎用,用之前想清楚

标准库函数不是「免死金牌」。恰恰相反,正因为大家都在用,这些陷阱才更容易被忽视。嗯,记住一句话:C 语言不会保护你,它只会忠实地执行你写的代码——哪怕你写的是错的。


公众号:蓝海资料掘金营,微信 deep3321