大家好,我是仲一。
联发科一面,面试官问了一个看起来很简单的问题:
"printf 可以传 1 个参数,也可以传 10 个参数。它是怎么做到的?"
这道题表面考 printf,实际考的是 C 语言可变参数和栈帧。
从 printf 的原型说起
int printf(const char *format, ...);
... 就是 C 语言的可变参数语法。一个函数只要在参数列表最后加上 ...,就可以接受不定数量的参数。
但问题来了:被调用的函数怎么知道传了多少个参数?怎么拿到这些参数?
可变参数的工作原理
先看一个关键事实:C 语言调用函数时,参数是从右往左依次压栈的。最后一个参数最先入栈,第一个参数最后入栈,并且靠近栈顶。
所以对 printf 来说,第一个确定的参数 format 必然在栈上固定位置。从 format 的地址向高地址方向,依次排列着后续所有参数。
拿到 format 之后,printf 通过解析 %d、%s 等格式控制符来确定后面有几个参数、每个参数是什么类型。解析一个格式,就从栈上取一个参数。
这个过程 C 标准库提供了一组宏来封装:
va_list ap; // 声明一个变参列表
va_start(ap, last); // 初始化,ap 指向 last 后面的第一个参数
va_arg(ap, type); // 取一个参数,类型是 type,自动移动指针
va_end(ap); // 清理
简化理解:va_start 算好第一个可变参数的地址,va_arg 每次取一个并把指针向后移动 sizeof(type) 个字节。
如果 format 和参数不匹配会发生什么
这是变参函数最大的风险。如果 format 写的是 %d,实际传的是一个 long long,va_arg 拿到的数据就是错的——因为它按 int 大小(4 字节)移动指针,但实际数据是 8 字节,后面所有参数的位置都偏了。
printf 不会帮你检查这些。编译器能检查出部分类型不匹配(启用了 -Wformat 的话),但不是所有情况都能发现。
自己实现一个简单的变参函数
看完原理后,可以自己写一个:
#include <stdarg.h>
void my_print(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
int val = va_arg(ap, int);
printf("value: %dn", val);
va_end(ap);
}
当然这只是演示,实际的 my_print 要像 printf 一样解析格式化字符串才能处理多个参数。
嵌入式场景
嵌入式调试中,我们自己写的串口打印函数,有时也用可变参数封装:
void uart_printf(const char *fmt, ...) {
char buf[256];
vsnprintf(buf, sizeof(buf), fmt, va_list);
uart_send(buf);
}
vsnprintf 接受 va_list 参数,配合 va_start / va_arg 机制,就能把可变参数转成格式化后的字符串,再从串口发出去。
你被问过 printf 相关的问题吗?留言聊聊。
202