Chinaunix首页 | 论坛 | 博客
  • 博客访问: 31647
  • 博文数量: 5
  • 博客积分: 1462
  • 博客等级: 上尉
  • 技术积分: 55
  • 用 户 组: 普通用户
  • 注册时间: 2009-12-29 13:47
文章分类

全部博文(5)

文章存档

2009年(5)

我的朋友
最近访客

分类: C/C++

2009-12-31 15:42:48

 
从printf谈可变参数函数
1 函数声明
首先,要实现类似printf()的变参函数,函数的最后一个参数要用 ... 表示,如
    int log(char * arg1, ...)
这样编译器才能知道这个函数是变参函数。这个参数与变参函数的内部实现完全没有关系,只是让编译器在编译调用此类函数的语句时不计较参数多少老老实实地把全部参数压栈而不报错,当然...之前至少要有一个普通的参数,这是由实现手段限制的。
2 函数实现
C语言通过几个宏实现变参的寻址。下面是linux2.18内核源码里这几个宏的定义,相信符合C89,C99标准的C语言基本都是这样定义的。
 
     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 /*此处就是将指针ap指向空指针,释放掉*/
#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作为基指针。
下边解释这几个宏的作用:
l)_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
2)va_start(ap,A),初始化参数指针ap,将函数参数A右边第一个参数的地址赋给ap。 A必须是一个参数的指针,所以此种类型函数至少要有一个普通的参数啊。像下面的例子函数,就是将第二个参数的指针赋给ap。
3)va_arg(ap,T),获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。
     注意((ap) += (_bnd (T, _AUPBND))) 是被一对括号括起来的,然后才减去(_bnd (T, _ADNBND),
而_AUPBND和_ADNBND是相等的。所以取得的值是ap当前指向的参数值,但是先给ap加了当前参数在字对齐后所占的字节数,使其指向了下一个参数。
4)  va_end(ap),作用是美观。
3 总结
先用一个 ... 参数声明函数是变参函数,接下来在函数内部以va_start(ap,A)宏初始化参数指针,然后就可以用va_arg(ap,类型)从左到右逐个获取参数值了分析到此处算是一清二白了,下面给一个例子
int log(char * fmt,...)
{
va_list ap;
int d;
char c, *p, *s;
 
va_start(ap, fmt);
while (*fmt) {
         switch(*fmt++) {
         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;
}
}
va_end(ap);
}
 
    其实大家可以多看看Linux内核源码。一般的工程文件也都有自己定义的可变参函数,最常见的是打印debug调试信息时候会用到。如果大家感兴趣也可以和形如 int functiton(int a;int b = 2){、、、};这样的函数比较起来看还是比较有意思的。
 
阅读(2062) | 评论(0) | 转发(0) |
0

上一篇:Linux高级技巧集

下一篇:没有了

给主人留下些什么吧!~~