在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了。
参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
C语言的函数是从下(低地址)向上(高地址)压入堆栈的,如下图所示:
栈底 高地址
| …
| 函数返回地址
| …
| 函数最后一个参数
| …
| 函数第一个可变参数 <–va_start后ap指向
| 函数最后一个固定参数
| …
| 函数第一个固定参数
栈顶 低地址
实现变参的关键数据结构:
typedef int * va_list;
//va_list等价于int *即整型指针,该变量类型应该根据具体的架构(ARM,X86)确定
#define va_start(ap, A) (ap = (int *)&(A) + 1)
//(int *)&得到A所在的地址,并强制类型转换为int ,然后这个地址加上A的大小,则使ap指向第一个可变参数!!
#define va_arg(ap, T) ((T *)ap++)
//先对指针ap(即地址)进行强制类型转换,转换为该变量实际的类型, 然后ap(即当前地址)自加1(即加一个类型的大小,指向下一个可变参数的地址),这类应该注意的是,先使用后自加,然后取出该地址的值!!
#define va_end(ap) ((void)0)
精简版prinf的例子:
#define va_start(ap, A) (ap = (int *)&(A) + 1)
#define va_arg(ap, T) (*(T *)ap++)
#define va_end(ap) ((void)0)
int printf(const char * format, ...)
{
char c;
va_list ap; //定义一个int *型的指针ap
va_start(ap, format); //初始化ap,让它指向第一个可变参数的地址
while ((c = *format++) != '\0') //开始解析printf函数中的字符串(即第一个固定参数)
{
switch (c) //在while中,依次读出字符串中的字符,在这里依依地进行判断和解析
{
case '%': //如果字符是%,即格式申明符号,则解析该格式
c = *format++; //让c等于%后的第一个字母
switch (c) //对上述字母进行解析
{
char ch;
char * p;
int a;
char buf[100];
case 'c': //如果是%c,即输出字母
ch = va_arg(ap, int); //获取当前可变参数的值,并指向下一个可变参数
putchar(ch); //调用底层(实际操作硬件的那层)来完成输出结果
break;
case 's': //如果是%s,即输出字符串
p = va_arg(ap, char *);
puts(p);
break;
case 'x': //如果是%x,即以十六进制输出
a = va_arg(ap, int);
putint_hex(a);
break;
case 'd': //如果是%d,即以十进制输出
a = va_arg(ap, int);
itoa(a, buf);
puts(buf);
break;
default:
break;
}
break;
default:
putchar(c); //输出字符串中为普通字符的字符
break;
}
}
return 0;
}
实际上就是在第一个固定参数里面解析出来各种字符输出控制变量,然后根据变量类型去栈中偏移相应的地址找到参数实际值,然后根据不同格式调用不同的输出函数即可完成变参显示printf函数。