请输入您要查询的百科知识:

 

词条 vadefs
释义

一、 在谈论这问题前我们先要了解什么是可变参数:

int printf( const char* format, ...);

printf知道为什么叫这个名字么?原来f就是format。所以命名也是很讲究的。

然后Win下有有好几种不同的调用约定,比如__stdcall,__pascal,__cdecl。

在Windows下__stdcall,__pascal是一样的,一般用于固定参数的函数,

表示调用端负责被调用函数引数的入栈和出栈;

__cdecl一般用于可变参数的函数,表示被调用函数自身负责函数引数的入栈和出栈,

不过效率不高,所以除非一定要用,一般都采用上一种。

这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示).而我们又可以用各种方式来调用printf,如:

printf("%d",value);

printf("%s",str);

printf("the number is %d ,string is:%s", value,str);

二.实现原理

C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:

VC2005在这些东西的定义前有这个东西“#elif defined(_M_IX86) //。。。”

觉得M代表Machine,IX86大概是Intel X86的意思吧。

然后这些东西被放到了vadefs.h中,名字前都加了个_crt_,比如va_strat就变成了_crt_va_start

最后在stdarg.h中加入了下面的东西来满足标准

#include<vadefs.h>

#define va_start_crt_va_start

//......

typedef char *va_list;

/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/

#define _INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))//简单的位操作,应该可以理解

/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统(转载者注:X86MS就是这样的系统),从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/

#define va_start(ap,v)( ap = (va_list)&v + _INTSIZEOF(v))

/*va_start的定义为 &v+_INTSIZEOF(v),这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们写va_start(ap,v)以后,ap指向第一个可变参数在的内存地址*/

#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) -_INTSIZEOF(t)) )

/*这个宏做了两个事情,

①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值

②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/

#define va_end(ap) ( ap = (va_list)0 )

/*x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.*/

以下再用图来表示:

在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的(不同的编译器的实现可能将参数从右向左压栈,也可能从左向右压栈,这个顺序我们是不能加以利用的,应该考虑到代码的移植性。所以应该用标准化的方式,在第二个例子中会给出。尽管如此,了解下VC的模式对于我们的理解也是有好处的),因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。

|——————————————————————————|

|最后一个可变参数 | ->高内存地址处

|——————————————————————————|

...................

|——————————————————————————|

|第N个可变参数 |->va_arg(arg_ptr,int)后arg_ptr所指的地方,

| | 即第N个可变参数的地址。

|——————————————— |

………………………….

|——————————————————————————|

|第一个可变参数 |->va_start(arg_ptr,start)后arg_ptr所指的地方

| | 即第一个可变参数的地址

|——————————————— |

|——————————————————————————|

| |

|最后一个固定参数 | -> start的起始地址

|—————————————— —|.................

|——————————————————————————|

| |

|——————————————— |->低内存地址处

三.printf研究

下面是一个简单的printf函数的实现,参考了书中的156页的例子,读者可以结合书上的代码与本文参照。

最好不要这样写,还是移植性的问题,但作者是全靠自己的理解写的,所以还是放着,加深印象

#include <stdio.h>

#include<stdlib.h>

void myprintf(char* fmt,...){ //一个简单的类似于printf的实现,//参数必须都是int类型

char* pArg=NULL; //等价于原来的va_list

char c;

pArg = (char*) &fmt; //注意不要写成p = fmt!!因为这里要对//参数取址,而不是取值

pArg += sizeof(fmt); //等价于原来的va_start

do{

c =*fmt;

if (c != '%'){

putchar(c); //照原样输出字符

}else{

//按格式字符输出数据

switch(*++fmt){

case 'd':printf("%d",*((int*)pArg));break;

case 'x':printf("%#x",*((int*)pArg));break;

default:break;

}

pArg += sizeof(int); //等价于原来的va_arg

}

++fmt;

}while (*fmt != '\\0');

pArg = NULL;//等价于va_end

//其实这里是不必要这么写的,反正调用好后都会失效,但是作为一个好习惯应该保留

}

int main(int argc, char* argv[]){

int i = 1234, j = 5678;

myprintf("the first test:i=%d\",i,j);

myprintf("the secend test:i=%d; %x;j=%d;\",i,0xABCD,j);

myprintf("OK\");

system("pause");

return 0;

}

在intel+win2k+vc6的机器执行结果如下:

the first test:i=1234

the secend test:i=1234; 0xabcd; j=5678;

OK

VC2005也能得到同样结果。

四.应用

求最大值:

//这个程序原作者挂得很惨,所以自己重新写了一个

#include <stdarg.h>

#include <stdio.h>

int mymax(int n,...){

int m=0,i,y;

va_list x; //说明变量x 要尽量这么写,尽量用宏名

va_start(x,n); //x被初始化为指向n后的第一个可变参数

for(i=1;i<=n;++i){

//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数

y=va_arg(x,int);

if(y>m) m=y;

}

va_end(x); //清除变量x

return m;

}

int main(){

printf("max1=%d,max2=%d\",mymax(3,5,56,50),mymax(6,0,4,32,45,12,500));

/*

*注意调用的时候千万要搞清参数的个数!

*原作者犯了个很搞笑的错误

*前面明明写了3,结果后面只跟了两个参数

*就像mymax(3,5,56)

*最后,gcc自带一个max函数,所以换个名字叫mymax

*/

return 0;

}

VC2005,DevCpp编译通过,运行结果:

max1=56,max2=500

随便看

 

百科全书收录4421916条中文百科知识,基本涵盖了大多数领域的百科知识,是一部内容开放、自由的电子版百科全书。

 

Copyright © 2004-2023 Cnenc.net All Rights Reserved
更新时间:2024/11/15 23:48:59