从printf谈可变参数函数的实现
副标题#e#
一直以来都以为printf好像是c语言库中成果最强大的函数之一,不只因为它能名目化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。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);
...
从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);
}
#p#副标题#e#
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);相当于
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);
}
#p#分页标题#e#
假如后续参数的范例是变革并且是未知的,则必需通过一个参数中设定模式来匹配后续参数的个数和范例,就像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] = '\0';
}
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);
结论
对付可变参数函数的挪用有一点需要留意,实际的可变参数的个数必需比前面模式指定的个数要多,可能不小于, 也即后续参数多一点没干系,但不能少, 假如少了则会会见到函数参数以外的仓库区域,这大概会把措施搞崩掉。前面模式的范例和后头实际参数的范例不匹配也有大概造成把措施搞瓦解,只要模式指定的数据长度大于后续参数长度,则这种环境就会产生。如:
printf("%.3f, %.3f, %.6e", 1, 2, 3, 4);
参数1,2,3,4的默认范例为整型,而模式指定的需要为double型,其数据长度比int大,这种环境就有大概会见函数参数仓库以外的区域,从而造成危险。可是printf("%d, %d, %d", 1.0, 20., 3.0);这种环境固然功效大概不正确,可是确不会造成劫难性效果。因为实际指定的参数长度比要求的参数长度长,仓库不会越界。