一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据需要确定,因此c语言引入可变参数函数。这也是c功能强大的一个方面,其它某些语言,比如fortran就没有这个功能。 典型的可变参数函数的例子有大家熟悉的printf()、scanf()等。 二、c/c++如何实现可变参数的函数? 为了支持可变参数函数,C语言引入新的调用协议, 即C语言调用约定 __cdecl 。 采用C/C++语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如WIN32 API使用PASCAL调用约定,函数名字之前必须加__stdcall关键字。 采用C调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。举个例子: //C调用约定函数 如果调用函数的时候使用的调用协议和函数原型中声明的不一致,就会导致栈错误,这是另外一个话题,这里不再细说。 另外c/c++编译器采用宏的形式支持可变参数函数。这些宏包括va_start、va_arg和va_end等。之所以这么做,是为了增加程序的可移植性。屏蔽不同的硬件平台造成的差异。 支持可变参数函数的所有宏都定义在stdarg.h 和 varargs.h中。例如标准ANSI形式下,这些宏的定义是: typedef char * va_list; //字符串指针 #define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) 使用宏_INTSIZEOF是为了按照整数字节对齐指针,因为c调用协议下面,参数入栈都是整数字节(指针或者值)。
可变参数函数在不同的系统下,采用不同的形式定义。 1、用ANSI标准形式时,参数个数可变的函数的原型声明是: type funcname(type para1, type para2, ...); 关于这个定义,有三点需要说明: 一般来说,这种形式至少需要一个普通的形式参数,可变参数就是通过三个'.'来定义的。所以"..."不表示省略,而是函数原型的一部分。type是函数返回值和形式参数的类型。 int MyPrintf(char const* fmt, ...); 但是,我们也可以这样定义函数: void MyFunc(...); 但是,这样的话,我们就无法使用函数的参数了,因为无法通过上面所讲的宏来提取每个参数。所以除非你的函数代码中的确没有用到参数表中的任何参数,否则必须在参数表中使用至少一个普通参数。 注意,可变参数只能位于函数参数表的最后。不能这样: void MyFunc(..., int i); 2、采用与UNIX 兼容系统下的声明方式时,参数个数可变的函数原型是: type funcname(va_alist); 但是要求函数实现的时候,函数名字后面必须加上va_dcl。例如: #i nclude <varargs.h> void main( void ) /* UNIX兼容形式*/ 这种形式不需要提供任何普通的形式参数。type是函数返回值的类型。va_dcl是对函数原型声明中参数va_alist的详细声明,实际是一个宏定义。根据平台的不同,va_dcl的定义稍有不同。 在varargs.h中,va_dcl的定义后面已经包括了一个分号。因此函数实现的时候,va_dcl后不再需要加上分号了。
下面通过若干例子,说明如何实现可变参数函数的定义和调用。 //================================ 例子程序1 =============== /* 函数原型声明,至少需要一个确定的参数,注意括号内的省略号 */ void main( void ) int demo( char *msg, ... ) // 使用宏va_start, 使argp指向传入的第一个可选参数, if ( strcmp( para, "/0") == 0 ) /* 采用空串指示参数输入结束 */ //输出结果 注意到上面的例子没有使用第一个参数,下面的例子将使用所有参数 //================================ 例子程序2 =============== #i nclude <stdio.h> void main( void ) /*调用4个整数*/ /*只有结束符的调用*/ /* 返回若干整数平均值的函数 */ va_start( marker, first ); //初始化 //输出结果
有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏va_arg逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢? 我们先看printf的实现: int __cdecl printf (const char *format, ...) va_start(arglist, format); //arglist指向format后面的第一个参数 。。。//不关心其它代码 。。。//不关心其它代码 我们先模仿这个函数写一个: #i nclude <stdio.h> int mywrite(char *fmt, ...) void main() 运行一下看看,哈,错误百出。仔细分析原因,根据宏的定义我们知道 arglist是一个指针,它指向第一个可变的参数,但是所有的参数都位于栈中,所以arglist指向栈中某个位置,通过arglist的值,我们可以直接查看栈里面的内容: arglist -> 指向栈里面,内容包括 0067FD78 E0 FD 67 00 //指向字符串"This is a test" 如果直接调用 printf(fmt, arglist); 仅仅是把arglist指针的值0067FD78入栈,然后把格式字符串入栈,相当于调用: printf(fmt, 0067FD78); 自然这样的调用肯定会出现错误。 我们能不能逐个把参数提取出来,再传递给其它函数呢?先考虑一次性把所有参数传递进去的问题。 如果调用的是系统库函数,这种情况下是不可能的。因为提取参数是在运行态,而参数入栈是在编译的时候确定的。无法让编译器预知运行态的事情给出正确的参数入栈代码。而我们在运行态虽然可以提取每个参数,但是无法将参数一次性全部压栈,即使使用汇编代码实现起来也是很困难的,因为不单是一个简单的push代码就可以做到。 如果接受参数的函数也是我们自己写的,自然我们可以把arglist指针入栈,然后在函数中自己解析arglist指针里面的参数,逐个提取出来处理。但是这样做似乎没有什么意义,一方面,这个函数没有必要也做成可变参数函数,另一方面直接在第一个函数中解析参数,然后处理不是更简单么? 我们唯一可以做到的是,逐个解析参数,然后循环中调用其它可变参数函数,每次传递一个参数。这里又有一个问题,就是参数表中的不可变参数的传递问题,有些情况下不能简单的传递,以上面的例子为例, 通常我们解析参数的同时,还需要解析格式字符串: #i nclude <windows.h> //测试一下这个,开个玩笑 int mywrite(char *fmt, ...) char temp[255]; char *p = strchr(temp,'%'); //格式字符串 i++; return i; } void main() //输出: 当然这里的解析是不完善的。 |
c/c++支持可变参数的函数
最新推荐文章于 2022-03-30 20:32:24 发布