当调用printf时参数的个数是不限定的,那么该函数是如何实现的呢?来看一下该函数的定义
int printf(const char *format,[argument]...)
printf的第一个参数就是那个字符指针即为被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数(%d等等)来判断参数个数及数据类型。例如printf("%d,%d",a,b);汇编代码为:
.section
.data
string out = "%d,%d"
push b
push a
push $out
call printf
注意参数的入栈顺序,最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的
该函数的实现涉及到几个宏:
C中变长实参头文件stdarg.h提供了一个数据类型va_list和三个宏(va_start、va_arg和va_end),用它们在被调用函数不知道参数个数和类型时对可变参数表进行测试,从而为访问可变参数提供了方便且有效的方法。
typedef char * va_list;
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1)&~(sizeof(int) - 1))
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
#define va_end(ap) ( ap = (va_list)0 )
- va_list是一个char类型的指针,当被调用函数使用一个可变参数时,它声明一个类型为va_list的变量,该变量用来指向va_arg和va_end所需信息的位置。
- _INTSIZEOF(n):为了字节对齐,将n的长度化为int长度的整数倍。在32位系统中,~(sizeof(int)–1) )展开为~(4-1)=~(00000011b)=11111100b,这样任何数& ~(sizeof(int)–1) )后最后两位肯定为0,也就是4的整数倍。
- va_start:获取到可变参数表的首地址,并将该地址赋给指针ap
- va_arg获取当前ap所指向的可变参数,并将指针ap指向下一个可变参数。注意,该宏的第二个参数为类型
- va_end: 结束可变参数的获取
下面是printf的实现
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;
}
从上面的代码来看,printf似乎并不复杂,它通过一个宏va_start把所有的可变参数放到了由args指向的一块内存中,然后再调用
vsprintf真正的参数个数以及格式的确定是在vsprintf搞定的。
int vsprintf(char *string, char *format, va_list args)
该函数将args按格式format写入字符串string中,该函数中会用到va_arg来获取各个参数,正常情况下返回生成字串的长度(除去\0),错误情况返回负值
下面是各个宏的一个简单的应用例子,实现一个多参数的printf
void minprintf(const char *fmt, ...)
{
va_list ap;
const char *p, *sval;
int ival;
double dval;
va_start(ap, fmt);
for (p = fmt; *p; p++) {
if (*p != '%') {
putchar(*p);
continue;
}
switch (*++p) {
case 'd':
ival = va_arg(ap, int);
printf("%d", ival);
break;
case 'f':
dval = va_arg(ap, double);
printf("%f", dval);
break;
case 's':
for (sval = va_arg(ap, char *); *sval; sval++)
putchar(*sval);
break;
default:
putchar(*p);
break;
}
}
va_end(ap);
}