C语言可变参数函数,有很多介绍。本文只想从C语言和编译器角度,借助一点逻辑思考的方式,深入浅出理解一下可变参数函数的实现。
说到可变参数函数,最先想到的就是int printf(const char *format, ...)。刚开始看到可变参数函数感觉特别神奇,为什么这个函数就可以传入的参数就可以是任意个,类型也是任意的。对于固定参数的函数,形参,实参是一一对应的,调用函数的时候,参数一一传递进去,十分容易理解,但是对于可变参数呢,试想你在调用printf传入5个参数,那么printf在执行的时候,它要是想正确的执行你的调用,它必须有办法知道你输入的参数的个数和参数类型,这是关键中的关键。从表面上看printf没有办法知道啊,因为参数不是固定的,它是被调用方,它如何知道调用者输入了几个参数呢。让我们举例来分析:
printf("int = %d, c = %c\n, s = %s", 10 , 'A' ,"hello world!");
注意第一个输入参数,是一个字符串"int = %d, c = %c\n, s = %s",且称它为*fmt。作为一个C语言的使用者,光从这个字符串,我们很容易推导出,这个printf 有四个输入参数,第一个就是字符串,第二个是整型,第三个是字符型,第四个是字符串。是的,这是我们通过第一个字符串自己推导出来的。因为我们作为使用者知道%是个特殊字符,它和某些字符的结合,如%d,%s,%c,就能确定后面必然有对应的参数输入。好了,既然我们能推导出来,那么计算机自然也会推导的。printf只需要对第一个字符串*fmt进行解析,找出特殊符号组合,根据规则,就可以确定,函数后续输入参数的个数及其类型,这个并不难,就是分析字符串。与固定参数函数的区别在于,对于可变参数函数,想要得到参数,需要自己分析。
当然还有其他类型的可变参数函数,如int sum(int count, ...);这个函数可以计算任意个数据之和。可能有人会说,这个和printf不一样,它的第一个参数是个整数,如何确定参数个数和类型。注意了,这个函数的第一个参数,表示后续输入参数的个数,至于参数的类型,那是默认的int型。这样参数的类型和数据都知道了。
当然,可变参数还要有很多形式,但是有一点可以肯定,就是参数的数量和个数,要么通过函数的一个参数传递进去,要么就是针对该函数有一套约定的默认规则,函数根据传递的参数或者约定的规则,最终完成参数的解析。
说到这里,下一个问题来了,既然知道参数的个数和类型了,怎的得到参数的值呢?这个问题比较复杂,这里我不作展开。对于不同的cpu架构,不同的编译器,底层传参操作遵循的规则是不一样的。比如对于arm的apcs规则,参数数量较少,使用r0~r3来传递参数,参数太多,后续的会使用stack空间来传递参数。而对于x86架构,很明显是没有r0~r3的。幸运的是,一般编译器都帮我们编写好了用于定位参数,取出参数需要用的函数,它们一般定义在头文件<stdarg.h>中:
void va_start(va_list ap, last); //移动ap指向第一个参数
type va_arg(va_list ap, type); //返回一个参数值,并将ap指向下一个参数
void va_end(va_list ap); //重置ap
void va_copy(va_list dest, va_list src);
上述函数通常用宏来实现,例如标准ANSI形式下,这些宏的定义是:
typedef char * va_list;
//字符串指针
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//移动ap指向第一个参数
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
//返回一个参数值,并将ap指向下一个参数
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
//重置ap
#define va_end(ap) ( ap = (va_list)0 )
具体可以参考《C语言可变参数函数详解示例》的介绍。如果需要更深入了解底层的实现可参见《亲密接触C可变参数函数》。
这里我们以一个例子来说明这些函数的使用方法:
#include <stdio.h>
#include <stdarg.h>
void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s;
va_start(ap, fmt);//移动ap指向第一个参数
while (*fmt)
{
switch(*fmt++)
{
case 's': /* 字符串 */
s = va_arg(ap, char *); //返回一个参数值,并将ap指向下一个参数
printf("string %s/n", s);
break;
case 'd': /* 整数 */
d = va_arg(ap, int); //返回一个参数值,并将ap指向下一个参数
printf("int %d/n", d);
break;
case 'c' //字符
c = va_arg(ap, char); //返回一个参数值,并将ap指向下一个参数
printf("char %c/n", c);
break;
}
}
va_end(ap);
}
int main()
{
foo("dcsd",15, 'A',"hello world", 18);
}
执行结果如下:
15
A
hello world
18
这个函数要实现一个可变参数的函数,第一个传入字符串,用于定义后续参数的个数和类型,例中“dcsd”,表示,后面有四个参数,分别是整型,字符型,字符串型,整型。
函数首先调用va_start();将ap指针指向后续第一个输入参数;然后foo函数开始了自己分析参数的过程:
- 逐个扫描字符
- 根据不同的字符,确定下一个参数的类型
- 调用va_arg()获取参数的值,同时将ap指针指向下一个参数
- 重复此过程,直至扫描完整个字符串
最后重置ap指针——va_end(ap)。
如果把函数的调用者比作领导,函数本身比作下属。那么,一般函数的领导,下命令就是逐条列出,思路清晰。而可变参数函数的领导,下的命令就是一锅大杂烩,需要下属自己去抽丝剥茧,逐条理清,或者是命令过于简单,需要下属自己根据日常规则去领悟。
总之,理解可变参数函数就两条:
1.如何确定输入参数的个数,以及对应的类型。这个需要函数自己根据规则去分析——自己动手,丰衣足食。
2.如何获取参数的值。必须的底层函数,编译器帮忙封装好了,能做的就是合理调用——按部就班,他山之石,可以攻玉。