《c++ 参数个数不确定》由会员分享,可在线阅读,更多相关《c++ 参数个数不确定(13页珍藏版)》请在金锄头文库上搜索。
1、c/c+支持可变参数的函数,即函数的参数是不确定的名人名言:个人如果但靠自己,如果置身于集体的关系之外,置身于任何团结民众的伟大思想的范围之外,就会变成怠惰的、保守的、与生活发展相敌对的人。高尔基 c/c+支持可变参数的函数,即函数的参数是不确定的。 一、为什么要使用可变参数的函数?一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据需要确定,因此 c 语言引入可变参数函数。这也是 c 功能强大的一个方面,其它某些语言,比如 fortran 就没有这个功能。典型的可变参数函数的例子有大家熟悉的 print
2、f()、scanf()等。二、c/c+如何实现可变参数的函数?为了支持可变参数函数,C 语言引入新的调用协议, 即 C 语言调用约定 _cdecl 。 采用 C/C+语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如 WIN32 API 使用 PASCAL 调用约定,函数名字之前必须加_stdcall 关键字。采用 C 调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。举个例子:/C 调用约定函数int _cdecl Add(int a, int b)return (a
3、+ b);函数调用:Add(1, 2);/汇编代码是:push 2 ;参数 b 入栈push 1 ;参数 a 入栈call Add ;调用函数。其实还有编译器用于定位函数的表达式这里把它省略了add esp,8 ;调用者负责清栈如果调用函数的时候使用的调用协议和函数原型中声明的不一致,就会导致栈错误,这是另外一个话题,这里不再细说。另外 c/c+编译器采用宏的形式支持可变参数函数。这些宏包括va_start、va_arg 和 va_end 等。之所以这么做,是为了增加程序的可移植性。屏蔽不同的硬件平台造成的差异。支持可变参数函数的所有宏都定义在 stdarg.h 和 varargs.h 中。例
4、如标准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) )#define va_arg(ap,t) ( *(t *)(ap += _INTSIZEOF(t) - _INTSIZEOF(t) )#define va_end(ap) ( ap = (va_list)0 )使用宏_INTSIZE
5、OF 是为了按照整数字节对齐指针,因为 c 调用协议下面,参数入栈都是整数字节(指针或者值)。三、如何定义这类的函数。可变参数函数在不同的系统下,采用不同的形式定义。1、用 ANSI 标准形式时,参数个数可变的函数的原型声明是: type funcname(type para1, type para2, );关于这个定义,有三点需要说明:一般来说,这种形式至少需要一个普通的形式参数,可变参数就是通过三个.来定义的。所以”不表示省略,而是函数原型的一部分。type 是函数返回值和形式参数的类型。例如:int MyPrintf(char const* fmt, );但是,我们也可以这样定义函数:v
6、oid MyFunc();但是,这样的话,我们就无法使用函数的参数了,因为无法通过上面所讲的宏来提取每个参数。所以除非你的函数代码中的确没有用到参数表中的任何参数,否则必须在参数表中使用至少一个普通参数。注意,可变参数只能位于函数参数表的最后。不能这样:void MyFunc(, int i);2、采用与 UNIX 兼容系统下的声明方式时,参数个数可变的函数原型是: type funcname(va_alist);但是要求函数实现的时候,函数名字后面必须加上 va_dcl。例如:#include int average( va_list );void main( void )。/代码/* UN
7、IX 兼容形式*/int average( va_alist )va_dcl。/代码这种形式不需要提供任何普通的形式参数。type 是函数返回值的类型。va_dcl是对函数原型声明中参数 va_alist 的详细声明,实际是一个宏定义。根据平台的不同,va_dcl 的定义稍有不同。在 varargs.h 中,va_dcl 的定义后面已经包括了一个分号。因此函数实现的时候,va_dcl 后不再需要加上分号了。3、采用头文件 stdarg.h 编写的程序是符合 ANSI 标准的,可以在各种操作系统和硬件上运行;而采用头文件 varargs.h 的方式 仅仅是为了与以前的程序兼容,两种方式的基本原理
8、是一致的,只是在语法形式上有一些细微的区别。 所以一般编程的时候使用 stdarg.h。下面的所有例子代码都采用 ANSI 标准格式。四、可变参数函数的基本使用方法下面通过若干例子,说明如何实现可变参数函数的定义和调用。/= 例子程序 1 =#include #include #include /* 函数原型声明,至少需要一个确定的参数,注意括号内的省略号 */int demo( char *, ); void main( void )demo(”DEMO”, “This”, “is”, “a”, “demo!”, “0);int demo( char *msg, )va_list argp;
9、 /* 定义保存函数参数的结构 */int argno = 0; /* 纪录参数个数 */char *para; /* 存放取出的字符串参数 */ 使用宏 va_start, 使 argp 指向传入的第一个可选参数,/ 注意 msg 是参数表中最后一个确定的参数,并非参数表中第一个参数va_start( argp, msg ); while (1)/取出当前的参数,类型为 char */如果不给出正确的类型,将得到错误的参数para = va_arg( argp, char *);if ( strcmp( para, “0) = 0 ) /* 采用空串指示参数输入结束 */break;prin
10、tf(”参数 #%d 是: %sn”, argno, para);argno+;/注意:栈底在高地址,栈顶在低地址,所以这里是+va_end( argp ); /* 将 argp 置为 NULL */return 0;/输出结果参数 #0 是: This参数 #1 是: is参数 #2 是: a参数 #3 是: demo!注意到上面的例子没有使用第一个参数,下面的例子将使用所有参数/= 例子程序 2 =#include #include int average( int first, ); /输入若干整数,求它们的平均值void main( void )/* 调用 3 个整数(-1 表示结尾)
11、 */printf( “Average is: %dn”, average(2,3,4, -1);/*调用 4 个整数*/printf( “Average is: %dn”, average(5,7,9, 11,-1);/*只有结束符的调用*/printf( “Average is: %dn”, average(-1) );/* 返回若干整数平均值的函数 */int average( int first, )int count = 0, sum = 0, i = first;va_list marker;va_start( marker, first ); /初始化while( i != -1
12、 )sum += i; /先加第一个参数count+;i = va_arg( marker, int);/取下一个参数va_end( marker );return( sum ? (sum / count) : 0 );/输出结果Average is: 3Average is: 8Average is: 0五、关于可变参数的传递问题有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏 va_arg 逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢?我们先看 printf 的实现:int _cdecl p
13、rintf (const char *format, )va_list arglist;int buffing;int retval;va_start(arglist, format); /arglist 指向 format 后面的第一个参数。/不关心其它代码retval = _output(stdout,format,arglist); /把 format 格式和参数传递给output 函数。/不关心其它代码return(retval);我们先模仿这个函数写一个:#include #include int mywrite(char *fmt, )va_list arglist;va_star
14、t(arglist, fmt);return printf(fmt,arglist);void main()int i=10, j=20;char buf = “This is a test”;double f= 12.345;mywrite(”String: %snInt: %d, %dnFloat :%4.2fn”, buf, i, j, f);运行一下看看,哈,错误百出。仔细分析原因,根据宏的定义我们知道 arglist 是一个指针,它指向第一个可变的参数,但是所有的参数都位于栈中,所以 arglist 指向栈中某个位置,通过 arglist 的值,我们可以直接查看栈里面的内容:argl
15、ist - 指向栈里面,内容包括0067FD78 E0 FD 67 00 /指向字符串”This is a test”0067FD7C 0A 00 00 00 /整数 i 的值0067FD80 14 00 00 00 /整数 j 的值0067FD84 71 3D 0A D7 /double 变量 f, 占用 8 个字节0067FD88 A3 B0 28 400067FD8C 00 00 00 00 如果直接调用 printf(fmt, arglist); 仅仅是把 arglist 指针的值 0067FD78入栈,然后把格式字符串入栈,相当于调用:printf(fmt, 0067FD78);自然这样的调用肯定会出现错误。我们能不能逐个把参数提取出来,再传递给其它函数呢?先考虑一次性把所有参数传递进去的问题。如果调用的是系统库函数,这种情况下是不可能的。因为提取参数是在运行态,而参数入栈是在编译的时候确定的。无法让编译器预知运行态的事情 给出正确的参数入栈代码。而我们在运行态虽然可以提取每个参数,但是无法将参数一次性全部压栈,即使使用汇编代码实现起来也是很困难的,因为不单是一个简 单的 push 代码就可以做到。如果接受参数的函数也是我们自己写的,自然我们可以把 arglist 指针入栈,然