c可变参数.docx
《c可变参数.docx》由会员分享,可在线阅读,更多相关《c可变参数.docx(10页珍藏版)》请在冰豆网上搜索。
c可变参数
第一个:
一、什么是可变参数
我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为:
intprintf(constchar*format,...);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式:
printf("%d",i);
printf("%s",s);
printf("thenumberis%d,stringis:
%s",i,s);
以上这些东西已为大家所熟悉。
但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。
本文就这个翁饨幸恍┨教?
希望能对大家有些帮助.
longsum(inti,...)
{
int*p,j;
longs=0;
p=&i+1;
for(j=0;j
s+=p[j];
returns;
}
longSum=sum(3,1,2,3);
printf("%ld",Sum);
Sum==6
二、写一个简单的可变参数的C函数
先看例子程序。
该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定.在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.
函数代码如下:
//示例代码1:
可变参数函数的使用
#include"stdio.h"
#include"stdarg.h"
voidsimple_va_fun(intstart,...)
{
va_listarg_ptr;
intnArgValue=start;
intnArgCout=0; //可变参数的数目
va_start(arg_ptr,start);//以固定参数的地址为起点确定变参的内存起始地址。
do
{
++nArgCout;
printf("the%dtharg:
%d",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;
}
下面解释一下这些代码
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
⑴由于在程序中将用到以下这些宏:
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_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)
下面我们解释这些代码的含义:
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。
而在有的机器上va_list是被定义成void*的
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大?
gt;>T谖业幕魃现苯佑胹izeof运朔创妫猿绦虻脑诵薪峁挂裁挥杏跋臁#ê笪慕吹轿易约旱氖迪郑?
3、va_start的定义为&v+_INTSIZEOF(v),这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。
所以我们运行va_start(ap,v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。
这里要知道两个事情:
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:
最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
|——————————————————————————|
| 最后一个可变参数 | ->高内存地址处
|——————————————————————————|
...................
|——————————————————————————|
| 第N个可变参数 | ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
| | 即第N个可变参数的地址。
|———————————————|
………………………….
|——————————————————————————|
| 第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变参数的地址
|———————————————|
|——————————————————————————|
| |
| 最后一个固定参数 | ->start的起始地址
|———————————————| .................
|——————————————————————————|
| |
|———————————————| ->低内存地址处
(4)va_arg():
有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到弦桓霾问钠鹗嫉刂贰?
因此,现在再来看va_arg()的实现就应该心中有数了:
#defineva_arg(ap,t) (*(t*)((ap+=_INTSIZEOF(t))-_INTSIZEOF(t)))
这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。
(5)va_end宏的解释:
x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:
由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.关于va_start,va_arg,va_end的描述就是这些了,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
(三)可变参数在编程中要注意的问题
因为va_start,va_arg,va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型.有人会问:
那么printf中不是实现了智能识别参数吗?
那是因为函数printf是从固定参数format字符串来分析出参数的类型,再调用va_arg的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.例如,在C的经典教材《thecprogramminglanguage》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。
(四)小结:
1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴在固定参数中设标志——printf函数就是用这个办法。
后面也有例子。
⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。
本文前面的代码就是采用这个办法.
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。
理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。
下面臼且桓隼?
(五)扩展——自己实现简单的可变参数的函数。
下面是一个简单的printf函数的实现,参考了中的156页的例子,读者可以结合书上的代码与本文参照。
#include"stdio.h"
#include"stdlib.h"
voidmyprintf(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(int); //等价于原来的va_arg
}
++fmt;
}while(*fmt!
='\0');
pArg=NULL; //等价于va_end
return;
}
intmain(intargc,char*argv[])
{
inti=1234;
intj=5678;
myprintf("thefirsttest:
i=%d",i,j);
myprintf("thesecendtest:
i=%d;%x;j=%d;",i,0xabcd,j);
system("pause");
return0;
}
在intel+win2k+vc6的机器执行结果如下:
thefirsttest:
i=1234
thesecendtest:
i=1234;0xabcd;j=5678;
#include//不定数目参数需要的宏
intmax(intn,intnum,...)
{
va_listx;//说明变量x
va_start(x,num);//x被初始化为指向num后的第一个参数
intm=num;
for(inti=1;i{
//将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
inty=va_arg(x,int);
if(y>m)m=y;
}
va_end(x);//清除变量x
returnm;
}
main()
{
printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
}
第二个:
C/C++语言中可变参数的用法
我们在C语言编程中会遇到一些参数个数可变的函数,例如printf()
这个函数,它的定义是这样的:
intprintf(constchar*format,...);
它除了有一个参数format固定以外,后面跟的参数的个数和类型是
可变的,例如我们可以有以下不同的调用方法:
printf("%d",i);
printf("%s",s);
printf("thenumberis%d,stringis:
%s",i,s);
究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实
现的呢?
本文就这个问题进行一些探讨,希望能对大家有些帮助.会C++的
网友知道这些问题在C++里不存在,因为C++具有多态性.但C++是C的一个
超集,以下的技术也可以用于C++的程序中.限于本人的水平,文中如果有
不当之处,请大家指正.
(一)写一个简单的可变参数的C函数
下面我们来探讨如何写一个简单的可变参数的C函数.写可变参数的
C函数要在程序中用到以下这些宏:
voidva_start(va_listarg_ptr,prev_param);
typeva_arg(va_listarg_ptr,type);
voidva_end(va_listarg_ptr);
va在这里是variable-argument(可变参数)的意思.
这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个
头文件.下面我们写一个简单的可变参数的函数,改函数至少有一个整数
参数,第二个参数也是整数,是可选的.函数只是打印这两个参数的值.
voidsimple_va_fun(inti,...)
{
va_listarg_ptr;
intj=0;
va_start(arg_ptr,i);
j=va_arg(arg_ptr,int);
va_end(arg_ptr);
printf("%d%d\n",i,j);
return;
}
我们可以在我们的头文件中这样声明我们的函数:
externvoidsimple_va_fun(inti,...);
我们在程序中可以这样调用:
simple_va_fun(100);
simple_va_fun(100,200);
从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:
1)首先在函数里定义一个va_list型的变量,这里是arg_ptr,这个变
量是指向参数的指针.
2)然后用va_start宏初始化变量arg_ptr,这个宏的第二个参数是第
一个可变参数的前一个参数,是一个固定的参数.
3)然后用va_arg返回可变的参数,并赋值给整数j.va_arg的第二个
参数是你要返回的参数的类型,这里是int型.
4)最后用va_end宏结束可变参数的获取.然后你就可以在函数里使
用第二个参数了.如果函数有多个可变参数的,依次调用va_arg获
取各个参数.
如果我们用下面三种方法调用的话,都是合法的,但结果却不一样:
1)simple_va_fun(100);
结果是:
100-123456789(会变的值)
2)simple_va_fun(100,200);
结果是:
100200
3)simple_va_fun(100,200,300);
结果是:
100200
我们看到第一种调用有错误,第二种调用正确,第三种调用尽管结果
正确,但和我们函数最初的设计有冲突.下面一节我们探讨出现这些结果
的原因和可变参数在编译器中是如何处理的.
(二)可变参数在编译器中的处理
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,
由于1)硬件平台的不同2)编译器的不同,所以定义的宏也有所不同,下
面以VC++中stdarg.h里x86平台的宏定义摘录如下(’\’号表示折行):
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)
定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.C语言的函
数是从右向左压入堆栈的,图
(1)是函数的参数在堆栈中的分布位置.我
们看到va_list被定义成char*,有一些平台或操作系统定义为void*.再
看va_start的定义,定义为&v+_INTSIZEOF(v),而&v是固定参数在堆栈的
地址,所以我们运行va_start(ap,v)以后,ap指向第一个可变参数在堆
栈的地址,如图:
高地址|-----------------------------|
|函数返回地址|
|-----------------------------|
|.......|
|-----------------------------|
|第n个参数(第一个可变参数)|
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<--&v
图
(1)
然后,我们用va_arg()取得类型t的可变参数值,以上例为int型为例,我
们看一下va_arg取int型的返回值:
j=(*(int*)((ap+=_INTSIZEOF(int))-_INTSIZEOF(int)));
首先ap+=sizeof(int),已经指向下一个参数的地址了.然后返回
ap-sizeof(int)的int*指针,这正是第一个可变参数在堆栈里的地址
(图2).然后用*取得这个地址的内容(参数值)赋给j.
高地址|-----------------------------|
|函数返回地址|
|-----------------------------|
|.......|
|-----------------------------|<--va_arg后ap指向
|第n个参数(第一个可变参数)|
|-----------------------------|<--va_start后ap指向
|第n-1个参数(最后一个固定参数)|
低地址|-----------------------------|<--&v
图
(2)
最后要说的是va_end宏的意思,x86平台定义为ap=(char*)0;使ap不再
指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不
会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.
在这里大家要注意一个问题:
由于参数的地址用于va_start宏,所
以参数不能声明为寄存器变量或作为函数或数组类型.
关于va_start,va_arg,va_end的描