《从printf谈可变参数的实现.pdf》由会员分享,可在线阅读,更多相关《从printf谈可变参数的实现.pdf(6页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、从从 printf 谈 可 变 参 数 函 数 的 实 现谈 可 变 参 数 函 数 的 实 现 作者:戎亚新 摘要:一直以来都觉得 printf 似乎是 c 语言库中功能最强大的函数之一,不仅因为它能格式化输出,更在于它的参数个数没有限制,要几个就给几个,来者不拒。printf 这种对参数个数和参数类型的强大适应性,让人产生了对它进行探索的浓厚兴趣。关键字:printf,可变参数 1.使用情形 int a=10;double b=20.0;char*str=Hello world;printf(begin printn);printf(a=%d,b=%.3f,str=%sn,a,b,str)
2、;.从 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)#de
3、fine _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_buf1024;int printf(char*fmt,.)va_list args;int n;va_start(args,fmt);n=vsprintf(spri
4、nt_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 的代码比较
5、复杂,也不是我们这里要讨论的重点,所以下面就不再列出了。我们这里要讨论的重点是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 大小也是
6、 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,它是 从右到左的,即函数中的最右边
7、的参数最先 入 栈。对于函数 void fun(int a,int b,int c)int d;.其栈结构为 0 x1ffc-d 0 x2000-a 0 x2004-b 0 x2008-c 对于任何编译器,每个栈单元的大小都是 sizeof(int),而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a,int b,double c,short d)对一个 32 的系统其栈的结构就是 0 x1ffc-a (4 字节)0 x2000-b (4 字节)0 x2004-c (8 字节)0 x200c-d (4 字节)对于函数 void fun1(char a,int b
8、,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);.dou
9、ble Paramn *(p+num);如果后续参数的类型是变化而且是未知的,则必须通过一个参数中设定模式来匹配后续参数的个数和类型,就像 printf 一样,当然我们可以定义自己的模式,如可以用 i 表示 int 参数,d 表示 double 参数,为了简单,我们用一个字符表示一个参数,并由该字符的名称决定参数的类型而字符的出现的顺序也表示后续参数的顺序。我们可以这样定义字符和参数类型的映射表,i-int s-signed short l-long c-char ild模式用于表示后续有三个参数,按顺序分别为 int,long,double 类型的三个参数那么这样我们可以定义自己版本的 pr
10、intf 如下 void printf(char*fmt,.)char s80=;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;iparamCount;i+)char line80=;strcpy(line,param);itoa(i+1,s,10);strcat(line,s);s
11、trcat(line,=);switch(fmti)case i:case s:itoa(*pi),s,10);strcat(line,s);pi+;break;case c:int len=strlen(line);linelen=(char)(*pi);linelen+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;
12、in;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);这种情况虽然结果可能不正确,但是确不会造成灾难性后果。因为实际指定的参数长度比要求的参数长度长,堆栈不会越界。