C:可变参数的函数

参考自:

http://www.cnblogs.com/hnrainll/archive/2011/07/06/2099494.html

http://www.cnblogs.com/MarvinGeng/archive/2012/07/19/2598923.html

1. 函数声明

   首先,要实现类似printf()的变参函数,函数的最后一个参数要用 ... 表示,如 
     int log(char * arg1, ...)
这样编译器才能知道这个函数是变参函数。这个参数与变参函数的内部实现完全没有关系,只是让编译器在编译调用此类函数的语句时不计较参数多少老老实实地把全部参数压栈而不报错,当然...之前至少要有一个普通的参数,这是由实现手段限制的。
2. 函数实现
  C语言通过几个宏实现变参的寻址。下面是linux2.18内核源码里这几个宏的定义,相信符合C89,C99标准的C语言基本都是这样定义的。

 1 typedef char *va_list;
 2 /*
 3 Storage alignment properties -- 堆栈按机器字对齐
 4 */
 5 #define _AUPBND            (sizeof (acpi_native_uint) - 1)
 6 #define _ADNBND            (sizeof (acpi_native_uint) - 1)
 7 /*
 8    Variable argument list macro definitions -- 变参函数内部实现需要用到的宏
 9 */
10 #define _bnd(X, bnd)          (((sizeof (X)) + (bnd)) & (~(bnd)))
11 #define va_arg(ap, T)        (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
12 #define va_end(ap)          (void) 0
13 #define va_start(ap, A)        (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
View Code

(c++编程使用这些宏需要包含<stdarg.h>头文件)

下面以x86 32位机为例分析这几个宏的用途

    要理解这几个宏需要对C语言如何传递参数有一定了解。与PASCAL相反,与stdcall 相同,C语言传递参数时是用push指令从右到左将参数逐个压栈,因此C语言里通过栈指针来访问参数。虽然X86的push一次可以压2,4或8个字节入 栈,C语言在压参数入栈时仍然是机器字的size为最小单位的,也就是说参数的地址都是字对齐的,这就是_bnd(X,bnd)存在的原因。(另外补充一点常识,不管是汇编还是C,编译出的X86函数一般在进入函数体后立即执行 push ebp       mov ebp, esp这两条指令。首先把ebp入栈,然后将当前栈指针赋给ebp,以后访问栈里的参数都使用ebp作为基指针。)

一一解释这几个宏的作用。

_bnd(X,bnd) ,计算类型为X的参数在栈中占据的字节数,当然是字对齐后的字节数了。acpi_native_unit是一个机器字,32位机的定义是:typedef u32 acpi_native_uint;
显然,_AUPBND ,_ADNBND 的值是 4-1 == 3 == 0x00000003 ,按位取反( ~(bnd))就是0xfffffffc 。 因此,_bnd(X,bnd) 宏在32位机下就是

( (sizeof(X) + 3)&0xfffffffc )。很明显,其作用是--倘若sizeof(X)不是4的整数倍,去余加4。
例如:  _bnd(sizeof(char),3) == 4       _bnd(sizeof(struct size7struct),3) == 8

va_start(ap,A) ,初始化参数指针ap,将函数参数A右边第一个参数的地址赋给ap,所以此种类型函数至少要有一个普通的参数。像下面的例子函数,就是将第二个参数的指针赋给ap。

va_arg(ap,T) ,获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。
注意((ap) += (_bnd (T, _AUPBND))) 是被一对括号括起来的,然后才减去(_bnd (T, _ADNBND),
而_AUPBND和_ADNBND是相等的。所以取得的值是ap当前指向的参数值,但是先给ap加了当前参数在字对齐后所占的字节数,使其指向了下一个参数。

va_end(ap), 使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 

注:从以上实现可以看出,由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型!

3. 示例

先用一个 ... 参数声明函数是变参函数,接下来在函数内部以va_start(ap,A)宏初始化参数指针,然后就可以用va_arg(ap,类型)从左到右逐个获取参数值了

分析到此处算是一清二白了,下面给一个例子

 1 int log(char * fmt,...){
 2     va_list ap;
 3     int d;
 4     char c, *p, *s;
 5     va_start(ap, fmt);
 6     while (*fmt)
 7         switch(*fmt++) {
 8             case 's': /* string */
 9                 s = va_arg(ap, char *);
10                 printf("string %s/n", s);
11                 break;
12             case 'd': /* int */
13                 d = va_arg(ap, int);
14                 printf("int %d/n", d);
15                 break;
16             case 'c': /* char */
17                 c = va_arg(ap, char);
18                 printf("char %c/n", c);
19                 break;
20         }
21     va_end(ap);
22 }
View Code

 4. 总结:

1)标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。 在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如 :

    ①在固定参数中设标志-- printf函数就是用这个办法。
    ②在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法. 
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。 
2)实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定: 
    ①函数栈的生长方向 
    ②参数的入栈顺序 
    ③CPU的对齐方式 
    ④内存地址的表达方式  
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。 

原文地址:https://www.cnblogs.com/reasno/p/4603853.html