从printf谈可变参数函数的实现

1. 使用情形


int a =10;
double b = 20.0;
char *str = "Hello world";
printf("begin print
");
printf("a=%d, b=%.3f, str=%s
", a, b, str);
...
  从printf的使用情况来看,我们不难发现一个规律,就是无论其可变的参数有多少个,printf的第一个参数总是一个字符串。而正是这第一个参数,使得它可以确认后面还有有多少个参数尾随。而尾随的每个参数占用的栈空间大小又是通过第一个格式字符串确定的。然而printf到底是怎样取第一个参数后面的参数值的呢,请看如下代码
  2. printf 函数的实现


//acenv.h
typedef char *va_list;
#define _AUPBND    (sizeof (acpi_native_int) - 1)
#define _ADNBND    (sizeof (acpi_native_int) - 1)
            
#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))))
//start.c
static char sprint_buf[1024];
int printf(char *fmt, ...)
{
  va_list args;
  int n;
  va_start(args, fmt);
  n = vsprintf(sprint_buf, fmt, args);
  va_end(args);
  write(stdout, sprint_buf, n);
  return n;
}
//unistd.h
static inline long write(int fd, const char *buf, off_t count)
{
  return sys_write(fd, buf, count);
}
3. 分析
  从上面的代码来看,printf似乎并不复杂,它通过一个宏va_start把所有的可变参数放到了由args指向的一块内存中,然后再调用vsprintf. 真正的参数个数以及格式的确定是在vsprintf搞定的了。由于vsprintf的代码比较复杂,也不是我们这里要讨论的重点,所以下面就不再列出了。我们这里要讨论的重点是va_start(ap, A)宏的实现,它对定位从参数A后面的参数有重大的制导意义。现在把 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 的含义解释一下如下:
双击代码全选


va_start(ap, A)
{
   char *ap = ((char *)(&A)) + sizeof(A)并int类型大小地址对齐
}
  在printf的va_start(args, fmt)中,fmt的类型为char *, 因此对于一个32为系统 sizeof(char *) = 4, 如果int大小也是32,则va_start(args, fmt);相当于 char *args = (char *)(&fmt) + 4; 此时args的值正好为fmt后第一个参数的地址。对于如下的可变参数函数
双击代码全选


void fun(double d,...)
{
  va_list args;
  int n;
  va_start(args, d);
}
则 va_start(args, d);相当于
双击代码全选
1
char *args = (char *)&d + sizeof(double);
  此时args正好指向d后面的第一个参数。
  可变参数函数的实现与函数调用的栈结构有关,正常情况下c/c++的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。对于函数
双击代码全选


void fun(int a, int b, int c)
{
  int d;
  ...
}
其栈结构为


0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c
  对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是


0x1ffc-->a (4字节)
0x2000-->b (4字节)
0x2004-->c (8字节)
0x200c-->d (4字节)
  对于函数void fun1(char a, int b, double c, short d)
  如果知道了参数a的地址,则要取后续参数的值则可以通过a的地址计算a后面参数的地址,然后取对应的值,而后面参数的个数可以直接由变量a指定,当然也可以像printf一样根据第一个参数中的%模式个数来决定后续参数的个数和类型。如果参数的个数由第一个参数a直接决定,则后续参数的类型如果没有变化并且是已知的,则我们可以这样来取后续参数, 假定后续参数的类型都是double;


void fun1(int num, ...)
{
  double *p = (double *)((&num)+1);
  double Param1 = *p;
  double Param2 = *(p+1);
  ...
  double Paramn *(p+num);
}
  如果后续参数的类型是变化而且是未知的,则必须通过一个参数中设定模式来匹配后续参数的个数和类型,就像printf一样,当然我们可以定义自己的模式,如可以用i表示int参数,d表示double参数,为了简单,我们用一个字符表示一个参数,并由该字符的名称决定参数的类型而字符的出现的顺序也表示后续参数的顺序。 我们可以这样定义字符和参数类型的映射表,


i---int
s---signed short
l---long
c---char
"ild"模式用于表示后续有三个参数,按顺序分别为int, long, double类型的三个参数那么这样我们可以定义自己版本的printf 如下


void printf(char *fmt, ...)
{
  char s[80] = "";
  int paramCount = strlen(fmt);
  write(stdout, "paramCount = " , strlen(paramCount = ));
  itoa(paramCount,s,10);
  write(stdout, s, strlen(s));
  char *p = (char *)(&fmt) + sizeof(char *);
  int *pi = (int *)p;
  for (int i=0; i<paramCount; i++)
  {
    char line[80] = "";
    strcpy(line, "param");
    itoa(i+1, s, 10);
    strcat(line, s);
    strcat(line, "=");
    switch(fmt[i])
    {
      case 'i':
      case 's':
        itoa((*pi),s,10);
        strcat(line, s);
        pi++;
        break;
      case 'c':
        {
          int len = strlen(line);
          line[len] = (char)(*pi);
          line[len+1] = '';
        }
        break;
      case 'l':
        ltoa((*(long *)pi),s,10);
        strcat(line, s);
        pi++;
        break;
      default:
        break;
    }
  }
}
也可以这样定义我们的Max函数,它返回多个输入整型参数的最大值


int Max(int n, ...)
{
  int *p = &n + 1;
  int ret = *p;
  for (int i=0; i<n; i++)
  {
    if (ret < *(p + i))
      ret = *(p + i);
  }
  return ret;
}
可以这样调用, 后续参数的个数由第一个参数指定


int m = Max(3, 45, 12, 56);
int m = Max(1, 3);
int m = Max(2, 23, 45);
int first = 34, second = 45, third=5;
int m = Max(5, first, second, third, 100, 4);
结论
  对于可变参数函数的调用有一点需要注意,实际的可变参数的个数必须比前面模式指定的个数要多,或者不小于, 也即后续参数多一点不要紧,但不能少, 如果少了则会访问到函数参数以外的堆栈区域,这可能会把程序搞崩掉。前面模式的类型和后面实际参数的类型不匹配也有可能造成把程序搞崩溃,只要模式指定的数据长度大于后续参数长度,则这种情况就会发生。如:
双击代码全选
1
printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);

  参数1,2,3,4的默认类型为整型,而模式指定的需要为double型,其数据长度比int大,这种情况就有可能访问函数参数堆栈以外的区域,从而造成危险。但是printf("%d, %d, %d", 1.0, 20., 3.0);这种情况虽然结果可能不正确,但是确不会造成灾难性后果。因为实际指定的参数长度比要求的参数长度长,堆栈不会越界。


分类:  C/C++2009-08-24 19:09  382人阅读  评论(1)  收藏  举报

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

#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

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

l          va_arg(ap,T),获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。

     注意((ap) += (_bnd (T, _AUPBND))) 是被一对括号括起来的,然后才减去(_bnd (T, _ADNBND),

而_AUPBND和_ADNBND是相等的。所以取得的值是ap当前指向的参数值,但是先给ap加了当前参数在字对齐后所占的字节数,使其指向了下一个参数。

l          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);

}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值