C语言可变参数.docx
- 文档编号:13181170
- 上传时间:2023-06-11
- 格式:DOCX
- 页数:14
- 大小:61.44KB
C语言可变参数.docx
《C语言可变参数.docx》由会员分享,可在线阅读,更多相关《C语言可变参数.docx(14页珍藏版)》请在冰点文库上搜索。
C语言可变参数
一、是什么
我们学习C语言时最经常使用printf()函数,但我们很少了解其原型。
其实printf()的参数就是可变参数,想想看,我们可以利用它打印出各种类型的数据。
下面我们来看看它的原型:
intprintf(constchar*format,...);
它的第一个参数是format,属于固定参数,后面跟的参数的个数和类型都是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式:
printf("%d",i);
printf("%s",s);
printf("thenumberis%d,stringis:
%s",i,s);
那么它的原型是怎样实现的呢?
我今天在看内核代码时碰到了vsprintf,花了大半天时间,终于把它搞的有点明白了。
二、先看两个例子
不必弄懂,先大致了解其用法,继续往下看。
①一个简单的可变参数的C函数
在函数simple_va_fun参数列表中至少有一个整数参数,其后是占位符…表示后面参数的个数不定.。
在这个例子里,所有输入参数必须都是整数,函数的功能只是打印所有参数的值。
#include
#include
voidsimple_va_fun(intstart,...)
{
va_listarg_ptr;
intnArgValue=start;
intnArgCout=0; //可变参数的数目
va_start(arg_ptr,start);//以固定参数的地址为起点确定变参的内存起始地址。
do
{
++nArgCout;
printf("the%dtharg:
%d\n",nArgCout,nArgValue); //输出各参数的值
nArgValue=va_arg(arg_ptr,int); //得到下一个可变参数的值
}while(nArgValue!
=-1);
return;
}
intmain(intargc,char*argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return0;
}
②格式化到一个文件流,可用于日志文件
FILE*logfile;
intWriteLog(constchar*format,...)
{
va_listarg_ptr;
va_start(arg_ptr,format);
intnWrittenBytes=vfprintf(logfile,format,arg_ptr);
va_end(arg_ptr);
returnnWrittenBytes;
}
稍作解释上面两个例子。
【这部分的引用地址
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
⑴在程序中用到了以下这些宏:
voidva_start(va_listarg_ptr,prev_param);
typeva_arg(va_listarg_ptr,type);
voidva_end(va_listarg_ptr);
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.
⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数.
⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。
⑸设定结束条件,①是判断参数值是否为-1。
注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。
②是调用宏va_end。
三、剖析可变参数真相
1.va_*宏定义
我们已经知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1)硬件平台的不同
2)编译器的不同,所以定义的宏也有所不同。
下面看一下VC++6.0中stdarg.h里的代码
(文件的路径为VC安装目录下的\vc98\include\stdarg.h)
typedefchar* va_list;
#define_INTSIZEOF(n)((sizeof(n)+sizeof(int)-1)&~(sizeof(int)-1))
#defineva_start(ap,v) (ap=(va_list)&v+_INTSIZEOF(v))
#defineva_arg(ap,t) (*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))
#defineva_end(ap) (ap=(va_list)0)
再来看看linux中的定义
typedefchar*va_list;
#define__va_rounded_size(TYPE)(((sizeof(TYPE)+sizeof(int)-1)/sizeof(int))*sizeof(int))
#defineva_start(AP,LASTARG)(AP=((char*)&(LASTARG)+__va_rounded_size(LASTARG))
voidva_end(va_list);
#defineva_end(AP)(AP=(char*)0)
#defineva_arg(AP,TYPE)(AP+=__va_rounded_size(TYPE),\
*((TYPE*)(AP-__va_rounded_size(TYPE))))
要理解上面这些宏定义的意思,需要首先了解:
①栈的方向②参数的入栈顺序③CPU的对齐方式④内存地址的表达方式。
2.栈——以Intel32位的CPU为分析基础
在IntelCPU中,栈的生长方向是向下的,即栈底在高地址,而栈顶在低地址;从栈底向栈顶看过去,地址是从高地址走向低地址的,因为称它为向下生长,如图。
【图1 引用自 http:
//www.yuanma.org/data/2008/0504/article_3027_1.htm,这部分内容,我认为作者讲的很详细,所以引来共享】
从上面压栈前后的两个图可明显看到栈的生长方向,在Intel32位的CPU中,windown或linux都使用了它的保护模式,ss指定栈所有在的段,ebp指向栈基址,esp指向栈顶。
显然执行push指令后,esp的值会减4,而pop后,esp值增加4。
栈中每个元素存放空间的大小决定push或pop指令后esp值增减和幅度。
Intel32位CPU中的栈元素大小为16位或32位,由定义堆栈段时定义。
在Window和Linux系统中,内核代码已定义好栈元素的大小为32位,即一个字长(sizeof(int))。
因此用户空间程栈元素的大小肯定为32位,这样每个栈元素的地址向4字节对齐。
C语言的函数调用约定对编写可变参数函数是非常重要的,只有清楚了,才更欲心所欲地控制程序。
在高级程序设计语言中,函数调用约定有如下几种,stdcall,cdecl,fastcall,thiscal,nakedcall。
cdel是C语言中的标准调用约定,如果在定义函数中不指明调用约定(在函数名前加上约定名称即可),那编译器认为是cdel约定,从上面的几种约定来看,只有cdel约定才可以定义可变参数函数。
下面是cdel约定的重要特征:
如果函数A调用函数B,那么称函数A为调用者(caller),函数B称为被调用者(callee)。
caller把向callee传递的参数存放在栈中,并且压栈顺序按参数列表中从右向左的顺序;callee不负责清理栈,而是由caller清理。
我们用一个简单的例子来说明问题,并采用Nasm的汇编格式写相应的汇编代码,程序段如下:
voidcallee(inta,intb)
{
intc=0;
c=a+b;
}
voidcaller()
{
callee(1,2);
}
来分析一下在调用过程发生了什么事情。
程序执行点来到caller时,那将要执行调用callee函数,在跳到callee函数前,它先要把传递的参数压到栈上,并按右到左的顺序,即翻译成汇编指令就是push2;push1;
图2
函数栈如图中(a)所示。
接着跳到callee函数,即指令callcalle。
CPU在执行call时,先把当前的EIP寄存器的值压到栈中,然后把EIP值设为callee(地址),这样,栈的图变为如图2(b)。
程序执行点跳到了callee函数的第一条指令。
C语言在函数调用时,每个函数占用的栈段称为stackframe。
用ebp来记住函数stackframe的起始地址。
故在执行callee时,最前的两条指令为:
pushebp
movesp,ebp
经过这两条语句后,callee函数的stackframe就建好了,栈的最新情况如图2(c)所示。
函数callee定义了一个局部变量intc,该变量的储存空间分配在callee函数占用的栈中,大小为4字节(insizeofint)。
那么callee会在如下指令:
subesp,4
mov[ebp-4],0
这样栈的情况又发生了变化,最新情况如图2(d)所示。
注意esp总是指向栈顶,而ebp作为函数的stackframe基址起到很大的作用。
ebp地址向下的空间用于存放局部变量,而它向上的空间存放的是caller传递过来的参数,当然编译器会记住变量c相对ebp的地址偏移量,在这里为-4。
跟着执行c=a+b语句,那么指令代码应该类似于:
moveax,[ebp+ 8];这里用eax存放第一个传递进来的参数,记住第一个参数与ebp的偏移量肯定为8
addeax, [ebp+12];第二个参数与ebp的偏移量为12,故计算eax=a+b
mov[ebp-4],eax ;执行c=eax,即c=a+b
栈又有了新了变化,如图2(e)。
至此,函数callee的计算指令执行完毕,但还要做一些事情:
释放局部变量占用的栈空间,销除函数的stack-frame过程会生成如下指令:
movesp,ebp;把局部变量占用的空间全部略过,即不再使用,ebp以下的空间全部用于局部变量
popebp;弹出caller函数的stack-frame基址
在IntelCPU里上面两条指令可以用指令leave来代替,功能是一样。
这样栈的内容如图2(f)所示。
最后,要返回到caller函数,因此callee的最后一条指令是
ret
ret指令用于把栈上的保存的断点弹出到EIP寄存器,新的栈内容如图2(g)所示。
函数callee的调用与返回全部结束,跟着下来是执行callcallee的下一条语句。
从caller函数调用callee前,把传递的参数压到栈中,并且按从右到左的顺序;函数返回时,callee并不清理栈,而是由caller清楚传递参数所占用的栈(如上图,函数返回时,1和2还放在栈中,让caller清理)。
栈元素的大小为4个字节,每个参数占用栈空间大小为4字节的倍数,并且任何两个参数都不能共用同一个栈元素。
从C语言的函数调用约定可知,参数列表从右向左依次压栈,故可变参数压在栈的地址比最后一个命名参数还大,如下图3所示:
由图3可知,最后一个命名参数a上面都放着可变参数,每个参数占用栈的大小必为4的倍数。
因此:
可变参数1的地址=参数a的地址+a占用栈的大小,可变参数2的地址=可变参数1的地址+可变参数1占用栈的大小,可变参数3的地址=可变参数2的地址+可变参数2占用栈的大小,依此类推。
如何计算每个参数占用栈的大小呢?
3.数据对齐问题
对于两个正整数x,n总存在整数q,r使得
x=nq+r,其中 0
q,r是唯一确定的。
q=[x/n],r=x-n[x/n].这个是带余除法的一个简单形式。
在c语言中,q,r容易计算出来:
q=x/n,r=x%n.
所谓把x按n对齐指的是:
若r=0,取qn,若r<0,取(q+1)n.这也相当于把x表示为:
x=nq+r',其中-n
nq是我们所求。
关键是如何用c语言计算它。
由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:
x+n=qn+(n+r'),其中0
x+n-1=qn+(n+r'-1),其中0
所以qn=[(x+n-1)/n]n.用c语言计算就是:
((x+n-1)/n)*n
若n是2的方幂,比如2^m,则除为右移m位,乘为左移m位。
所以把x+n-1的最低m个二进制位清0就可以了。
得到:
(x+n-1)&(~(n-1))
【来自CSDN博客:
根据这些推导,相信已经了解#define__va_rounded_size(TYPE) (((sizeof(TYPE)+sizeof(int)-1)/sizeof(int))*sizeof(int))的涵义。
4.再看va_*宏定义
va_start(va_listap,last)
last为最后一个命名参数,va_start宏使ap记录下第一个可变参数的地址,原理与“可变参数1的地址=参数a的地址+a占用栈的大小”相同。
从ap记录的内存地址开始,认为参数的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址+=occupy_stack(type)
va_arg(va_litap,type)
这里是获得可变参数的值,具体工作是:
从ap所指向的栈内存中读取类型为type的参数,并让ap根据type的大小记录它的下一个可变参数地址,便于再次使用va_arg宏。
从ap记录的内存地址开始,认为存的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址+=occupy_stack(type)
va_end(va_listap)
用于“释放”ap变量,它与va_start对称使用。
在同一个函数内有va_start必须有va_end。
5.可变参数函数问题
考虑了参数大小和数据对齐问题,使得可变参数的类型不但可以是基本类型,同样适用于用户定义类型。
值的注意的是,如果是用户定义类型,最好用typedef定义的名字作为类型名,这样就会减少在va_arg进行宏展开时出错的机率。
在可变参数函数中,由va_list变量来记录(或获得)可变参数部分,但是va_list中并没有记录下它们的名字,事实上也是不可能的。
要想把可变参数部分传递给下一个函数,唯有通过va_list变量去传递,而原来定义的函数用"..."来表示可变参数部分,而不是用va_list来表示。
为了方便程序的标准化,ANSIC在标准库代码中就作出了很好的榜样:
在任何形如:
typefun(typearg1,typearg2,...)的函数,都同时定义一个与它功能完全一样的函数,但用va_list类型来替换"...",即typefun(typearg1,typearg2,va_listap)。
以printf函数为例:
intprintf(constchar*format,...);
intvprintf(constchar*format,va_listap);
第一个函数用"..."表示可变参数,第二个用va_list类型表示可变参数,目的是用于被其它可变参数调用,两者在功能功能上是完全上一样。
只是在函数名字相差一个'"v"字母。
四、可变参数函数的应用
一个中的例子:
一个简单的实现printf函数的例子:
#include
#include
#include
/*minprintf:
minimalprintfwithvariableargumentlist*/
voidminprintf(char*fmt,...)
{
va_listap;/*pointstoeachunnamedarginturn*/
char*p,*sval;
intival;
doubledval;
va_start(ap,fmt);/*makeappointto1stunnamedarg*/
for(p=fmt;*p;p++){
if(*p!
='%'){
putchar(*p);
continue;
}
switch(*++p){
case'd':
ival=va_arg(ap,int);
printf("%d",ival);
break;
case'x':
ival=va_arg(ap,int);
printf("%#x",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);/*cleanupwhendone*/
}
intmain(intargc,char*argv[])
{
inti=1234;
intj=5678;
char*s="nihao";
doublef=0.11f;
minprintf("thefirsttest:
i=%d\n",i,j);
minprintf("thesecendtest:
i=%d;%x;j=%d;",i,0xabcd,j);
minprintf("the3rdtest:
s=%s\n",s);
minprintf("the4thtest:
f=%f\n",f);
minprintf("the5thtest:
s=%s,f=%f\n",s,f);
system("pause");
return0;
}
不使用va_*宏定义的实现:
voidminprintf(char*fmt,...)//一个简单的类似于printf的实现不过参数必须都是int类型
{
char*pArg=NULL; //等价于原来的va_list
charc;
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
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 语言 可变 参数
![提示](https://static.bingdoc.com/images/bang_tan.gif)