C 语言的变长参数在平时做开发时很少会在自己设计的接口中用到,但我们最常用的接口
printf 就是使用的变长参数接口,在感受到 printf 强大的魅力的同时,是否想挖据一下到底
printf 是如何实现的呢?这里我们一起来挖掘一下 C 语言变长参数的奥秘。
先考虑这样一个问题:如果我们不使用 C 标准库(libc)中提供的 Facilities,我们自己是否可
以实现拥有变长参数的函数呢?我们不妨试试。
一步一步进入正题,我们先看看固定参数列表函数,
void fixed_args_func(int a, double b, char *c)
{
printf("a = 0x%p", &a);
printf("b = 0x%p", &b);
printf("c = 0x%p", &c);
}
对于固定参数列表的函数,每个参数的名称、类型都是直接可见的,他们的地址也都是可
以直接得到的,比如:通过&a 我们可以得到 a 的地址,并通过函数原型声明了解到 a 是 int
类型的; 通过&b 我们可以得到 b 的地址,并通过函数原型声明了解到 b 是 double 类型的; 通
过&c 我们可以得到 c 的地址,并通过函数原型声明了解到 c 是 char*类型的。
但是对于变长参数的函数,我们就没有这么顺利了。还好,按照 C 标准的说明,支持变长
参数的函数在原型声明中,必须有至少一个最左固定参数(这一点与传统 C 有区别,传统 C
允许不带任何固定参数的纯变长参数函数),这样我们可以得到其中固定参数的地址,但是
依然无法从声明中得到其他变长参数的地址,比如:
void var_args_func(const char * fmt, ... )
{
... ...
}
这里我们只能得到 fmt 这固定参数的地址,仅从函数原型我们是无法确定"..."中有几个参数、
参数都是什么类型的,自然也就无法确定其位置了。那么如何可以做到呢?在大脑中回想
一下函数传参的过程,无论"..."中有多少个参数、每个参数是什么类型的,它们都和固定
参数的传参过程是一样的,简单来讲都是栈操作,而栈这个东西对我们是开放的。这样一
来,一旦我们知道某函数帧的栈上的一个固定参数的位置,我们完全有可能推导出其他变
长参数的位置,顺着这个思路,我们继续往下走,通过一个例子来诠释一下:(这里要说明
的是:函数参数进栈以及参数空间地址分配都是"实现相关"的,不同平台、不同编译器都
可能不同,所以下面的例子仅在 IA-32,Windows XP, MinGW gcc v3.4.2 下成立)
我们先用上面的那个 fixed_args_func 函数确定一下这个平台下的入栈顺序。
int main()
{
fixed_args_func(17, 5.40, "hello world");
return 0;
}
a = 0x0022FF50
b = 0x0022FF54