C 语言变参函数解析


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

/*
   Storage alignment properties -- 堆栈按机器字对齐
*/
#define _AUPBND            (sizeof (acpi_native_uint) - 1)
#define _ADNBND            (sizeof (acpi_native_uint) - 1)

/*
   Variable argument list macro definitions -- 变参函数内部实现需要用到的宏
*/
#define _bnd(X, bnd)          (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T)        (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap)          (void) 0
#define va_start(ap, A)        (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

   下面以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。 A必须是一个参数的指针,所以此种类型函数至少要有一个普通的参数啊。像下面的例子函数,就是将第二个参数的指针赋给ap。

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

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


4、在编程中应该注意的问题和解决办法
    虽然可以通过在堆栈中遍历参数列表来读出所有的可变参数,但是由于不知道可变参数
多少个,什么时候应该结束遍历,如果在堆栈中遍历太多,那么很可能读取一些无效的数据.
解决办法:
a.可以在第一个起始参数中指定参数个数,那么就可以在循环还中读取所有的可变参数;
b.定义一个结束标记,在调用函数的时候,在最后一个参数中传递这个标记,这样在遍历
  可变参数的时候,可以根据这个标记结束可变参数的遍历;
下面是一段相关示例代码:
#include <stdarg.h>
#include <stdio.h>

int sum(int, ...);

int main()
{
   printf("Sum of 15 and 56 = %d\n",  sum(2, 15, 56) );
   return 0;
}

int sum(int num_args, ...)
{
   int val = 0;
   va_list ap;
   int i;

   va_start(ap, num_args);
   for(i = 0; i < num_args; i++)
   {
      val += va_arg(ap, int);
   }
   va_end(ap);
 
   return val;
}
 
虽然可以根据上面两个办法解决读取参数个数的问题,但是如果参数类型都是不定的,
该怎么办,如果不知道参数的类型,即使读到了参数也没有办法进行处理.

解决办法:可以自定义一些可能出现的参数类型,这样在可变参数列表中,传入可变参
数列表中的那类型,然后根据类型,读取可变参数值,并进行准确地转换.传递参数的
时候可以这样传递:参数数目,可变参数类型1,可变参数值1,可变参数类型2,可变参数值2,....
看一个类似的例子:
int log(int arg_num,...)
{
    va_list ap;
    int d;
    char c, *p, *s;

    va_start(ap, fmt);
    for(i = 0; i < num; i++)
    {
        switch(va_arg(ap, char *))
        {
            case 's':        /* string */
            s = va_arg(ap, char *);
            printf("string %s/n", s);
        break;
       
        case 'd':        /* int */
            d = va_arg(ap, int);
            printf("int %d/n", d);
        break;
       
        case 'c':        /* char */
            c = va_arg(ap, char);
            printf("char %c/n", c);
        break;
       
        default:
             return -1;    /*err*/
        break;
        }
    }
    va_end(ap);
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值