作者:戎亚新
摘要:一直以来都觉得printf似乎是c语言库中功能最强大的函数之一,不仅因为它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。
关键字:printf, 可变参数
1. 使用情形
int a =10;
double b = 20.0;
char *str = "Hello world";
printf("begin print\n");
printf("a=%d, b=%.3f, str=%s\n", a, b, str);
...
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);
}
从上面的代码来看,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类型大小地址对齐
}
    void fun(double d,...)
{
va_list args;
int n;
va_start(args, d);
}
    char *args = (char *)&d + sizeof(double);
可变参数函数的实现与函数调用的栈结构有关,正常情况下c/c++的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。对于函数
    void fun(int a, int b, int c)
{
int d;
...
}
    0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c
    0x1ffc-->a  (4字节)
0x2000-->b  (4字节)
0x2004-->c  (8字节)
0x200c-->d  (4字节)
如果知道了参数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);
}
i---int
s---signed short
l---long
c---char 
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] = '\0';
}
break;
case 'l':
ltoa((*(long *)pi),s,10);
strcat(line, s);
pi++;
break;
default:
break;
}
}
}
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);
对于可变参数函数的调用有一点需要注意,实际的可变参数的个数必须比前面模式指定的个数要多,或者不小于, 也即后续参数多一点不要紧,但不能少, 如果少了则会访问到函数参数以外的堆栈区域,这可能会把程序搞崩掉。前面模式的类型和后面实际参数的类型不匹配也有可能造成把程序搞崩溃,只要模式指定的数据长度大于后续参数长度,则这种情况就会发生。如:
printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);