第一篇
C语言编程中有时会遇到一些参数个数可变的函数 ,例如 printf()函数 ,其函数原型为:
int printf( const char* format, ...);
它除了有一个参数 format固定以外 ,后面跟的参数的个数和类型是可变的(用三个点“…”做参数占位符) ,实际调用时可以有以下的形式:
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
一个简单的可变参数的 C函数
先看例子程序。该函数至少有一个整数参数 ,其后占位符…,表示后面参数的个数不定。在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值。函数代码如下:
//示例代码 1:可变参数函数的使用
#include "stdio.h"
#include "stdarg.h"
void simple_va_fun(int start, ...)
{
va_list arg_ptr;
int nArgValue =start;
int nArgCout="0"; //可变参数的数目
va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
do
{
++nArgCout;
printf("the %d th arg: %d",nArgCout,nArgValue); //输出各参数的值
nArgValue = va_arg(arg_ptr,int); //得到下一个可变参数的值
} while(nArgValue != -1);
return;
}
int main(int argc, char* argv[])
{
simple_va_fun(100,-1);
simple_va_fun(100,200,-1);
return 0;
}
下面解释一下这些代码。从这个函数的实现可以看到 ,我们使用可变参数应该有以下步骤:
⑴由于在程序中将用到以下这些宏 :
void va_start ( va_list arg_ptr, prev_param );
type va_arg ( va_list arg_ptr, type );
void va_end ( va_list arg_ptr );
va在这里是 variable-argument(可变参数 )的意思。
这些宏定义在 stdarg.h中 ,所以用到可变参数的程序应该包含这个头文件。
⑵函数里首先定义一个 va_list型的变量 ,这里是 arg_ptr,这个变量是存储参数地址的指针 .因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。
⑶然后用 va_start宏初始化⑵中定义的变量 arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数 ,即最后一个固定参数。
⑷然后依次用 va_arg宏使 arg_ptr返回可变参数的地址 ,得到这个地址之后,结合参数的类型,就可以得到参数的值。
⑸设定结束条件,这里的条件就是判断参数值是否为 -1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,在看完这几个宏的内部实现机制后,自然就会明白。
第二篇
C语言之可变参数问题
C语言中有一种长度不确定的参数,形如: "…",它主要用在参数个数不确定的函数中,我们最容易想到的例子是 printf函数。
原型:
int printf( const char *format [, argument]... );
使用例:
printf("Enjoy yourself everyday!/n");
printf("The value is %d!/n", value);
这种可变参数可以说是 C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。
注意:在 C++中有函数重载( overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。
问题: printf的实现
请问,如何自己实现 printf函数,如何处理其中的可变参数问题? 答案与分析:
在标准 C语言中定义了一个头文件专门用来对付可变参数列表,它包含了一组宏,和一个 va_list的typedef声明。一个典型实现如下:
typedef char* va_list;
#define va_start(list) list = (char*)&va_alist
#define va_end(list)
#define va_arg(list, mode)/
((mode*) (list += sizeof(mode)))[-1]
自己实现 printf:
#include
int printf(char* format, …)
{
va_list ap;
va_start(ap, format);
int n = vprintf(format, ap);
va_end(ap);
return n;
}
问题:运行时才确定的参数
有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?
答案与分析:
目前没有 "正规 "的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是 main(),它的原型是 :
int main(int argc,char *argv[]);
函数的参数是 argc和 argv。
深入想一下, "只能在运行时确定参数形式 ",也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个 void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是 main函数中 argv的含义,而 argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。
虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。
问题:可变长参数的传递
有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?
答案与分析:
目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为 va_list类型,同时在调用函数中将可变长参数列表转换为 va_list,这样就可以进行变长参数的传递了。看如下所示:
void subfunc (char *fmt, va_list argp)
{
...
arg = va_arg (fmt, argp); /* 从 argp中逐一取出所要的参数 */
...
}
void mainfunc (char *fmt, ...)
{
va_list argp;
va_start (argp, fmt); /* 将可变长参数转换为 va_list */
subfunc (fmt, argp); /* 将 va_list传递给子函数 */
va_end (argp);
...
}
问题:可变长参数中类型为函数指针
我想使用 va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?
答案与分析:
这个与 va_arg的实现有关。一个简单的、演示版的 va_arg实现如下:
#define va_arg(argp, type) /
(*(type *)(((argp) += sizeof(type)) - sizeof(type)))
其中, argp的类型是 char *。
如果你想用 va_arg从可变参数列表中提取出函数指针类型的参数,例如
int (*)(),则 va_arg(argp, int (*)())被扩展为:
(*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))
显然,( int (*)() *)是无意义的。
解决这个问题的办法是将函数指针用 typedef定义成一个独立的数据类型,例如:
typedef int (*funcptr)();
这时候再调用 va_arg(argp, funcptr)将被扩展为:
(* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))
这样就可以通过编译检查了。
问题:可变长参数的获取
有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为 float的实参:
va_arg (argp, float);
这样做可以吗?
答案与分析:
不可以。在可变长参数中,应用的是 "加宽 "原则。也就是 float类型被扩展成 double; char, short被扩展成int。因此,如果你要去可变长参数列表中原来为 float类型的参数,需要用 va_arg(argp, double)。对 char和short类型的则用 va_arg(argp, int)。
问题:定义可变长参数的一个限制
为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?
int f (...)
{
...
}
答案与分析:
不可以。这是 ANSI C 所要求的,你至少得定义一个固定参数。
我们在 C语言编程中会遇到一些参数个数可变的函数 ,例如 printf()这个函数 ,它的定义是这样的 :
int printf( const char* format, ...);
它除了有一个参数 format固定以外 ,后面跟的参数的个数和类型是可变的 ,例如我们可以有以下不同的调用方法 :
printf("%d",i);
printf("%s",s);
printf("the number is %d ,string is:%s", i, s);
究竟如何写可变参数的 C函数以及这些可变参数的函数编译器是如何实现的呢 ?本文就这个问题进行一些探讨 ,希望能对大家有些帮助 .会 C++的网友知道这些问题在 C++里不存在 ,因为 C++具有多态性 .但 C++是C的一个超集 ,以下的技术也可以用于 C++的程序中 .限于本人的水平 ,文中如果有不当之处 ,请大家指正 .
(一 )写一个简单的可变参数的 C函数
下面我们来探讨如何写一个简单的可变参数的 C函数 .写可变参数的 C函数要在程序中用到以下这些宏 :
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr );
va在这里是 variable-argument(可变参数 )的意思 .这些宏定义在 stdarg.h中 ,所以用到可变参数的程序应该包含这个头文件 .下面我们写一个简单的可变参数的函数 ,改函数至少有一个整数参数 ,第二个参数也是整数,是可选的 .函数只是打印这两个参数的值 .
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
int j=0;
va_start(arg_ptr, i);
j=va_arg(arg_ptr, int);
va_end(arg_ptr);
printf("%d %d/n", i, j);
return;
}
我们可以在我们的头文件中这样声明我们的函数 :
extern void simple_va_fun(int i, ...);
我们在程序中可以这样调用 :
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);
结果是 :100 200
3)
simple_va_fun(100,200,300);
结果是 :100 200
我们看到第一种调用有错误 ,第二种调用正确 ,第三种调用尽管结果正确 ,但和我们函数最初的设计有冲突 .下面一节我们探讨出现这些结果的原因和可变参数在编译器中是如何处理的 .
(二 )可变参数在编译器中的处理
我们知道 va_start,va_arg,va_end是在 stdarg.h中被定义成宏的 ,由于 1)硬件平台的不同 2)编译器的不同 ,所以定义的宏也有所不同 ,下面以 VC++中 stdarg.h里 x86平台的宏定义摘录如下 (’/’号表示折行 ):
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 )
定义 _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的描述就是这些了 ,我们要注意的是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的 .
(三 )可变参数在编程中要注意的问题
因为 va_start, va_arg, va_end等定义成宏 ,所以它显得很愚蠢 ,可变参数的类型和个数完全在该函数中由程序代码控制 ,它并不能智能地识别不同参数的个数和类型 .有人会问 :那么 printf中不是实现了智能识别参数吗?那是因为函数 printf是从固定参数 format字符串来分析出参数的类型 ,再调用 va_arg的来获取可变参数的 .也就是说 ,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的 .另外有一个问题 ,因为编译器对可变参数的函数的原型检查不够严格 ,对编程查错不利 .如果 simple_va_fun()改为 :
void simple_va_fun(int i, ...)
{
va_list arg_ptr;
char *s=NULL;
va_start(arg_ptr, i);
s=va_arg(arg_ptr, char*);
va_end(arg_ptr);
printf("%d %s/n", i, s);
return;
}
可变参数为 char*型 ,当我们忘记用两个参数来调用该函数时 ,就会出现 core dump(Unix) 或者页面非法的错误 (window平台 ).但也有可能不出错 ,但错误却是难以发现 ,不利于我们写出高质量的程序 .
以下提一下 va系列宏的兼容性 .System V Unix把 va_start定义为只有一个参数的宏 :
va_start(va_list arg_ptr);
而 ANSI C则定义为 :
va_start(va_list arg_ptr, prev_param);
如果我们要用 system V的定义 ,应该用 vararg.h头文件中所定义的宏 ,ANSI C的宏跟 system V的宏是不兼容的 ,我们一般都用 ANSI C,所以用 ANSI C的定义就够了 ,也便于程序的移植 .
小结 :
可变参数的函数原理其实很简单 ,而 va系列是以宏定义来定义的 ,实现跟堆栈相关 .我们写一个可变函数的 C函数时 ,有利也有弊 ,所以在不必要的场合 ,我们无需用到可变参数 .如果在 C++里 ,我们应该利用 C++的多态性来实现可变参数的功能 ,尽量避免用 C语言的方式来实现 .
c/c++支持可变参数的函数,即函数的参数是不确定的。
一、为什么要使用可变参数的函数?
一般我们编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据需要确定,因此 c语言引入可变参数函数。这也是 c功能强大的一个方面,其它某些语言,比如 fortran就没有这个功能。
典型的可变参数函数的例子有大家熟悉的 printf()、 scanf()等。
二、 c/c++如何实现可变参数的函数?
为了支持可变参数函数, C语言引入新的调用协议, 即 C语言调用约定 __cdecl 。采用 C/C++语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如 WIN32 API使用PASCAL调用约定,函数名字之前必须加 __stdcall关键字。
采用 C调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。举个例子:
//C调用约定函数
int __cdecl Add(int a, int b)
{
return (a + 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中。例如标准 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 )
使用宏 _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。例如:
#include
int average( va_list );
void main( void )
{
。。。 //代码
}
/* UNIX兼容形式 */
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的方式仅仅是为了与以前的程序兼容,两种方式的基本原理是一致的,只是在语法形式上有一些细微的区别。所以一般编程的时候使用 stdarg.h。下面的所有例子代码都采用 ANSI标准格式。
四、可变参数函数的基本使用方法
下面通过若干例子,说明如何实现可变参数函数的定义和调用。
//================================ 例子程序 1 ===============
#include < stdio.h >
#include < string.h >
#include < stdarg.h >
/* 函数原型声明,至少需要一个确定的参数,注意括号内的省略号 */
int demo( char *, … );
void main( void )
{
demo(”DEMO”, “This”, “is”, “a”, “demo!”, “/0″);
}
int demo( char *msg, … )
{
va_list argp; /* 定义保存函数参数的结构 */
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;
printf(”参数 #%d 是 : %s/n”, 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表示结尾 ) */
printf( “Average is: %d/n”, average(2,3,4, -1));
/*调用 4个整数 */
printf( “Average is: %d/n”, average(5,7,9, 11,-1));
/*只有结束符的调用 */
printf( “Average is: %d/n”, average(-1) );
}
/* 返回若干整数平均值的函数 */
int average( int first, … )
{
int count = 0, sum = 0, i = first;
va_list marker;
va_start( marker, first ); //初始化
while( i != -1 )
{
sum += i; //先加第一个参数
count++;
i = va_arg( marker, int);//取下一个参数
}
va_end( marker );
return( sum ? (sum / count) : 0 );
}
//输出结果
Average is: 3
Average is: 8
Average is: 0
五、关于可变参数的传递问题
有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏 va_arg逐个把参数提取出来使用,能否不提取,直接把它们传递给另外的函数呢?
我们先看 printf的实现:
int __cdecl printf (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_start(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: %s/nInt: %d, %d/nFloat :%4.2f/n”, buf, i, j, f);
}
运行一下看看,哈,错误百出。仔细分析原因,根据宏的定义我们知道 arglist是一个指针,它指向第一个可变的参数,但是所有的参数都位于栈中,所以 arglist指向栈中某个位置,通过 arglist的值,我们可以直接查看栈里面的内容:
arglist -> 指向栈里面,内容包括
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 40
0067FD8C 00 00 00 00
如果直接调用 printf(fmt, arglist); 仅仅是把 arglist指针的值 0067FD78入栈,然后把格式字符串入栈,相当于调用:
printf(fmt, 0067FD78);
自然这样的调用肯定会出现错误。
我们能不能逐个把参数提取出来,再传递给其它函数呢?先考虑一次性把所有参数传递进去的问题。
如果调用的是系统库函数,这种情况下是不可能的。因为提取参数是在运行态,而参数入栈是在编译的时候确定的。无法让编译器预知运行态的事情给出正确的参数入栈代码。而我们在运行态虽然可以提取每个参数,但是无法将参数一次性全部压栈,即使使用汇编代码实现起来也是很困难的,因为不单是一个简单的 push代码就可以做到。
如果接受参数的函数也是我们自己写的,自然我们可以把 arglist指针入栈,然后在函数中自己解析 arglist指针里面的参数,逐个提取出来处理。但是这样做似乎没有什么意义,一方面,这个函数没有必要也做成可变参数函数,另一方面直接在第一个函数中解析参数,然后处理不是更简单么?
我们唯一可以做到的是,逐个解析参数,然后循环中调用其它可变参数函数,每次传递一个参数。这里又有一个问题,就是参数表中的不可变参数的传递问题,有些情况下不能简单的传递,以上面的例子为例, 通常我们解析参数的同时,还需要解析格式字符串:
#include
#include
#include
//测试一下这个,开个玩笑
void t(…)
{
printf(”/n”);
}
int mywrite(char *fmt, …)
{
va_list arglist;
va_start(arglist, fmt);
char temp[255];
strcpy(temp, fmt); //Copy the Format string
char Format[255];
char *p = strchr(temp,’%');
int i=0;
int iParam;
double fParam;
while(p != NULL)
{
while((*p< 'a' || *p>‘z’) && (*p!=0) ) p++;
if(*p == 0)break;
p++;
//格式字符串
int nChar = p - temp;
strncpy(Format,temp, nChar);
Format[nChar] = 0;
//参数
if(Format[nChar-1] != ‘f’)
{
iParam = va_arg( arglist, int);
printf(Format, iParam);
}
else
{
fParam = va_arg( arglist, double);
printf(Format, fParam);
}
i++;
if(*p == 0) break;
strcpy(temp, p);
p = strchr(temp, ‘%’);
}
if(temp[0] != 0)
printf(temp);
return i;
}
void main()
{
int i=10, j=20;
char buf[] = “This is a test”;
double f= 123.456;
mywrite(”String: %s/nInt: %d, %d/nFloat :%4.2f/nEnd”, buf, i, j, f, 0);
t(”aaa”, i);
}
//输出:
String: This is a test
Int: 10, 20
Float :123.46
End
当然这里的解析是不完善的
此文献给如我一般还在探索 C语言之路的朋友们 。
测试程序的编译环境为 win2000和 VC6.0
缘起:
作为一个程序员,我没有写过参数可变的函数,我相信大部分朋友也没有涉及过,或者我的境界层次太低了。那么缘何我要去揭这一层面纱呢?因为好奇!
我是个思维具有极大惰性的人,曾经识得参数可变函数,也懒得去深究,但是它的三点(函数声明时参数列表中的“…”)却深刻的映入了我的记忆里,而且是带着若干个闪耀的问号。可是就在昨天,在拜读某君的高论时,它再一次出现了。我的资质真的是不太够,因为某君在谈到它时只是给出了 <stdarg.h>中关于它的宏定义,我想大概在高手眼里,点这一下就神会了吧。可是他这么轻轻一点却使留在记忆里曾经的那几个问号无限的膨胀,以至于我这个又菜又懒的所谓程序员也萌生了莫大的好奇。
破题:
但凡所谓“实现”都是从没有到有的过程,但是我只是想去解惑它的实现,因为它原本就是好端端的正为成千上万的程序员们服务。
还是从我们熟悉的 printf说起:
如果你是个 C语言的程序员,无论你是初学者还是高高手,对于 printf都不会陌生,甚至你已经用了无数次了。我已经说过我是个有极大惰性的人,所以每次用 printf都是照本宣科,规规矩矩的按教科书上说的做,从来没有问过一个为什么,这就是所谓的“熟视无睹”吧。
其实, printf函数是一个典型的参数可变的函数。在保证它的第一个参数是字符串的条件下,你可以输任意数量任意合法类型的参数。只要你在第一个字符串参数中使用了对应的格式化字符串,你就可以输出正确的值。这难道不是件很有趣的事吗?那它是怎么做到的?
1,首先,怎么得到参数的值。对于一般的函数,我们可以通过参数对应在参数列表里的标识符来得到。但是参数可变函数那些可变的参数是没有参数标识符的,它只有“…”,所以通过标识符来得到是不可能的,我们只有另辟途径。
我们知道函数调用时都会分配栈空间,而函数调用机制中的栈结构如下图所示:
| ...... |
------------------
| 参数 2 |
------------------
| 参数 1 |
------------------
| 返回地址 |
------------------
|调用函数运行状态 |
------------------
可见,参数是连续存储在栈里面的,那么也就是说,我们只要得到可变参数的前一个参数的地址,就可以通过指针访问到那些可变参数。但是怎么样得到可变参数的前一个参数的地址呢?不知道你注意到没有,参数可变函数在可变参数之前必有一个参数是固定的,并使用标识符,而且通常被声明为 char*类型, printf函数也不例外。这样的话,我们就可以通过这个参数对应的标识符来得到地址,从而访问其他参数变得可能。我们可以写一个测试程序来试一下:
#include <stdio.h>
void va_test(char* fmt,...);//参数可变的函数声明
void main()
{
int a=1,c=55;
char b='b';
va_test("",a,b,c);//用四个参数做测试
}
void va_test(char* fmt,...) //参数可变的函数定义,注意第一个参数为 char* fmt
{
char *p=NULL;
p=(char *)&fmt;//注意不是指向 fmt,而是指向 &fmt,并且强制转化为 char *,以便一个一个字节访问
for(int i = 0;i<16;i++)//16是通过计算的值(参数个数 *4个字节),只是为了测试,暂且将就一下
{
printf("%.4d ",*p);//输出 p指针指向地址的值
p++;
}
}
编译运行的结果为
0056 0000 0066 0000 | 0001 0000 0000 0000 | 0098 0000 0000 0000 | 0055 0000 0000 0000
由运行结果可见,通过这样方式可以逐一获得可变参数的值。
至于为什么通常被声明为 char*类型,我们慢慢看来。
2,怎样确定参数类型和数量
通过上述的方式,我们首先解决了取得可变参数值的问题,但是对于一个参数,值很重要,其类型同样举足轻重,而对于一个函数来讲参数个数也非常重要,否则就会产生了一系列的麻烦来。通过访问存储参数的栈空间,我们并不能得到关于类型的任何信息和参数个数的任何信息。我想你应该想到了——使用 char *参数。Printf函数就是这样实现的,它把后面的可变参数类型都放到了 char *指向的字符数组里,并通过 %来标识以便与其它的字符相区别,从而确定了参数类型也确定了参数个数。其实,用何种方式来到达这样的效果取决于函数的实现。比如说,定义一个函数,预知它的可变参数类型都是 int,那么固定参数完全可以用 int类型来替换 char*类型,因为只要得到参数个数就可以了。
3,言归正传
我想到了这里,大概的轮廓已经呈现出来了。本来想就此作罢的(我的惰性使然),但是一想到如果不具实用性便可能是一堆废物,枉费我打了这么些字,决定还是继续下去。
我是比较抵制用那些不明所以的宏定义的,所以在上面的阐述里一点都没有涉及定义在 <stdarg.h>的va(variable-argument)宏。事实上,当时让我产生极大疑惑和好奇的正是这几个宏定义。但是现在我们不得不要去和这些宏定义打打交道,毕竟我们在讨生计的时候还得用上他们,这也是我曰之为“言归正传”的理由。
好了,我们来看一下那些宏定义。
打开 <stdarg.h>文件,找一下 va_*的宏定义,发现不单单只有一组,但是在各组定义前都会有宏编译。宏编译指示的是不同硬件平台和编译器下用怎样的 va宏定义。比较一下,不同之处主要在偏移量的计算上。我们还是拿个典型又熟悉的—— X86的相关宏定义:
1)typedef char * va_list;
2)#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
3)#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
4)#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
5)#define va_end(ap) ( ap = (va_list)0 )
我们逐一看来:
第一个我想不必说了,类型定义罢了。第二个是颇有些来头的,我们也不得不搞懂它,因为后面的两个关键的宏定义都用到了。不知道你够不够细心,有没有发现在上面的测试程序中,第二个可变参数明明是 char类型,可是在输出结果中占了 4个 byte。难道所有的参数都会占 4个 byte的空间?那如果是 double类型的参数,且不是会丢失数据!如果你不嫌麻烦的话,再去做个测试吧,在上面的测试程序中用一个 double类型(长度为 8byte)和一个 long double类型(长度为 10byte)做可变参数。发现什么? double类型占了 8byte,而 long double占了12byte。好像都是 4的整数倍哦。不得不引出另一个概念了“对齐( alignment)” ,所谓对齐,对 Intel80x86 机器来说就是要求每个变量的地址都是 sizeof(int)的倍数。原来我们搞错了, char类型的参数只占了 1byte,但是它后面的参数因为对齐的关系只能跳过 3byte存储,而那 3byte也就浪费掉了。那为什么要对齐?因为在对齐方式下, CPU 的运行效率要快得多(举个例子吧,要说明的是下面的例子是我从网上摘录下来的,不记得出处了。
示例:如下图,当一个 long 型数(如图中 long1)在内存中的位置正好与内存的字边界对齐时, CPU 存取这个数只需访问一次内存,而当一个 long 型数(如图中的 long2)在内存中的位置跨越了字边界时, CPU 存取这个数就需要多次访问内存,如 i960cx 访问这样的数需读内存三次(一个 BYTE、一个 SHORT、一个 BYTE,由 CPU 的微代码执行,对软件透明),所以对齐方式下 CPU 的运行效率明显快多了。
1 8 16 24 32
------- ------- ------- ---------
| long1 | long1 | long1 | long1 |
------- ------- ------- ---------
| | | | long2 |
------- ------- ------- ---------
| long2 | long2 | long2 | |
------- ------- ------- ---------
| ....)。好像扯得有点远来,但是有助于对 _INTSIZEOF(n)的理解。位操作对于我来说是玄的东东。单个位运算还应付得来,而这样一个表达式摆在面前就晕了。怎么办?菜鸟自有菜的办法
深入浅出可变参数函数的使用技巧
本文主要介绍可变参数的函数使用,然后分析它的原理,程序员自己如何对它们实
现和封装,最后是可能会出现的问题和避免措施。
VA函数( variable argument function),参数个数可变函数,又称可变参数函数
。 C/C++编程中,系统提供给编程人员的 va函数很少。 *printf()/*scanf()系列函数
,用于输入输出时格式化字符串; exec*()系列函数,用于在程序中执行外部文件 (
main(int argc,char*argv[]算不算呢,与其说 main()也是一个可变参数函数,倒不
如说它是 exec*()经过封装后的具备特殊功能和意义的函数,至少在原理这一级上有
很多相似之处 )。由于参数个数的不确定,使 va函数具有很大的灵活性,易用性,对
没有使用过可变参数函数的编程人员很有诱惑力;那么,该如何编写自己的 va函数
, va函数的运用时机、编译实现又是如何。作者借本文谈谈自己关于 va函数的一些
浅见。
一、 从 printf()开始
从大家都很熟悉的格式化字符串函数开始介绍可变参数函数。
原型: int printf(const char * format, ...);
参数 format表示如何来格式字符串的指令,…
表示可选参数,调用时传递给 "..."的参数可有可无,根据实际情况而定。
系统提供了 vprintf系列格式化字符串的函数,用于编程人员封装自己的 I/O函数。
int vprintf / vscanf(const char * format, va_list ap); // 从标准输入 /输出
格式化字符串
int vfprintf / vfsacanf(FILE * stream, const char * format, va_list ap);
// 从文件流
int vsprintf / vsscanf(char * s, const char * format, va_list ap); // 从
字符串
// 例 1:格式化到一个文件流,可用于日志文件
FILE *logfile;
int WriteLog(const char * format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
return nWrittenBytes;
}
…
// 调用时,与使用 printf()没有区别。
WriteLog("%04d-%02d-%02d %02d:%02d:%02d %s/%04d logged out.",
nYear, nMonth, nDay, nHour, nMinute, szUserName, nUserID);
同理,也可以从文件中执行格式化输入;或者对标准输入输出,字符串执行格式化
。
在上面的例 1中, WriteLog()函数可以接受参数个数可变的输入,本质上,它的实现
需要 vprintf()的支持。如何真正实现属于自己的可变参数函数,包括控制每一个传
入的可选参数。
二、 va函数的定义和 va宏
C语言支持 va函数,作为 C语言的扩展 --C++同样支持 va函数,但在 C++中并不推荐使
用, C++引入的多态性同样可以实现参数个数可变的函数。不过, C++的重载功能毕
竟只能是有限多个可以预见的参数个数。比较而言, C中的 va函数则可以定义无穷多
个相当于 C++的重载函数,这方面 C++是无能为力的。 va函数的优势表现在使用的方
便性和易用性上,可以使代码更简洁。 C编译器为了统一在不同的硬件架构、硬件平
台上的实现,和增加代码的可移植性,提供了一系列宏来屏蔽硬件环境不同带来的
差异。
ANSI C标准下, va的宏定义在 stdarg.h中,它们有: va_list, va_start(), va_ar
g(), va_end()。
// 例 2:求任意个自然数的平方和:
int SqSum(int n1, ...)
{
va_list arg_ptr;
int nSqSum = 0, n = n1;
va_start(arg_ptr, n1);
while (n > 0)
{
nSqSum += (n * n);
n = va_arg(arg_ptr, int);
}
va_end(arg_ptr);
return nSqSum;
}
// 调用时
int nSqSum = SqSum(7, 2, 7, 11, -1);
可变参数函数的原型声明格式为:
type VAFunction(type arg1, type arg2, … );
参数可以分为两部分:个数确定的固定参数和个数可变的可选参数。函数至少需要
一个固定参数,固定参数的声明和普通函数一样;可选参数由于个数不确定,声明
时用 "…"表示。固定参数和可选参数公同构成一个函数的参数列表。
借助上面这个简单的例 2,来看看各个 va_xxx的作用。
va_list arg_ptr:定义一个指向个数可变的参数列表指针;
va_start(arg_ptr, argN):使参数列表指针 arg_ptr指向函数参数列表中的第一个
可选参数,说明: argN是位于第一个可选参数之前的固定参数,(或者说,最后一
个固定参数;…之前的一个参数),函数参数列表中参数在内存中的顺序与函数声
明时的顺序是一致的。如果有一 va函数的声明是 void va_test(char a, char b, c
har c, …),则它的固定参数依次是 a,b,c,最后一个固定参数 argN为 c,因此就是
va_start(arg_ptr, c)。
va_arg(arg_ptr, type):返回参数列表中指针 arg_ptr所指的参数,返回类型为 ty
pe,并使指针 arg_ptr指向参数列表中下一个参数。
va_copy(dest, src): dest, src的类型都是 va_list, va_copy()用于复制参数列表
指针,将 dest初始化为 src。
va_end(arg_ptr):清空参数列表,并置参数指针 arg_ptr无效。说明:指针 arg_pt
r被置无效后,可以通过调用 va_start()、 va_copy()恢复 arg_ptr。每次调用 va_st
art() / va_copy()后,必须得有相应的 va_end()与之匹配。参数指针可以在参数列
表中随意地来回移动,但必须在 va_start() … va_end()之内。
一、关于可变参数的函数定义方法
注:本节原文摘自互联网,笔者对它进行了必要的编辑和扩展。原作者尚未查知,在此表示歉意和感谢。
某些情况下希望函数的参数个数可以根据需要确定。典型的例子有大家熟悉的函数 printf()、 scanf()和系统调用 execl()等。那么它们是怎样实现的呢? C编译器通常提供了一系列处理这种情况的宏,以屏蔽不同的硬件平台造成的差异,增加程序的可移植性。这些宏包括 va_start、 va_arg和 va_end等。
1. 采用 ANSI标准形式时,参数个数可变的函数的原型声明是:
view plaincopy to clipboardprint?
type funcname(type para1, type para2, ...)
type funcname(type para1, type para2, ...)
这种形式至少需要一个普通的形式参数,后面的省略号不表示省略,而是函数原型的一部分。 type是函数返回值和形式参数的类型。
2. 采用与 UNIX System V兼容的声明方式时,参数个数可变的函数原型是:
view plaincopy to clipboardprint?
type funcname(va_alist)
va_dcl
type funcname(va_alist)
va_dcl
这种形式不需要提供任何普通的形式参数。 type是函数返回值的类型。 va_dcl是对函数原型声明中参数va_alist的详细声明,实际是一个宏定义,对不同的硬件平台采用不同的类型来定义,但在最后都包括了一个分号。因此 va_dcl后不再需要加上分号了。 va_dcl在代码中必须原样给出。 va_alist在 VC中可以原样给出,也可以略去。
此外,采用头文件 stdarg.h编写的程序是符合 ANSI标准的,可以在各种操作系统和硬件上运行;而采用头文件 varargs.h的方式仅仅是为了与以前的程序兼容。所以建议大家使用前者。以下主要就前一种方式对参数的处理做出说明。两种方式的基本原理是一致的,只是在语法形式上有一些细微的区别。
view plaincopy to clipboardprint?
/* 取第一个可变参数的指针给 arg_ptr
last_firm_arg是函数声明中最后一个固定参数,该
宏参方便编译器定位第一个可变参数的地址,因为在函数
调用的栈结构中,可变参数总是在因定参数的后面 */
void va_start( va_list arg_ptr, last_firm_arg );
/* 返回 arg_ptr指定的当前可变参数的值,然后 arg_ptr指向下一参数
cur_arg_type是当前参数的类型,如 int,该
宏参方便编译器定位下一可变参数的地址
注意支持的类型为 int和 double,这是一个陷井,下文将详述
可循环调用此宏得到 N个参数值 */
cur_arg_type va_arg( va_list arg_ptr, cur_arg_type );
/* arg_ptr置为 NULL */
void va_end( va_list arg_ptr );
/* 取第一个可变参数的指针给 arg_ptr
last_firm_arg是函数声明中最后一个固定参数,该
宏参方便编译器定位第一个可变参数的地址,因为在函数
调用的栈结构中,可变参数总是在因定参数的后面 */
void va_start( va_list arg_ptr, last_firm_arg );
/* 返回 arg_ptr指定的当前可变参数的值,然后 arg_ptr指向下一参数
cur_arg_type是当前参数的类型,如 int,该
宏参方便编译器定位下一可变参数的地址
注意支持的类型为 int和 double,这是一个陷井,下文将详述
可循环调用此宏得到 N个参数值 */
cur_arg_type va_arg( va_list arg_ptr, cur_arg_type );
/* arg_ptr置为 NULL */
void va_end( va_list arg_ptr );
va_start使 argp指向第一个可选参数。 va_arg返回参数列表中的当前参数并使 argp指向参数列表中的下一个参数。 va_end把 argp指针清为 NULL。函数体内可以多次遍历这些参数,但是都必须以 va_start开始,并以va_end结尾。
调用者在实际调用参数个数可变的函数时,要通过一定的方法指明实际参数的个数(编注:实际上,每个参数的数据类型 (占用字节数 )也需要以一定的方法指明,如采用默认类型或以固参指明类型, printf()的首参——格式化字串中的类型格式符 %d、 %f、 %s等就是显式指明的),例如把最后一个参数置为空字符串(系统调用execl()就是这样的)、 -1或其他的方式(函数 printf()就是通过第一个参数,即输出格式的定义来确定实际参数的个数的)。
下面给出一个具体的例子。是采用了符合 ANSI标准的形式的代码。代码中加了一些注释,这里就不再解释了。该例子已经在 VC/Windows XP、 CC/AIX4.3.2.0、 GCC/SUSE7.3环境下编译并正常运行。
演示如何使用参数个数可变的函数,采用 ANSI标准形式
view plaincopy to clipboardprint?
#include < stdio.h >;
#include < string.h >;
#include < stdarg.h >;
/* 函数原型声明,至少需要一个确定的参数, 注意括号内的省略号 */
int demo( char *, ... );
void main( void )
{
demo("DEMO", "This", "is", "a", "demo!", "/0");
}
/* ANSI标准形式的声明方式,括号内的省略号表示可选参数 */
int demo( char *msg, ... )
{
va_list argp; /* 定义保存函数参数的结构 */
int argno = 0; /* 纪录参数个数 */
char *para; /* 存放取出的字符串参数 */
/* argp指向传入的第一个可选参数,
msg是最后一个确定的参数 */
va_start( argp, msg );
while (1)
{
/* 取出当前的参数,类型为 char *. */
para = va_arg( argp, char *);
/* 采用空串指示参数输入结束 */
if ( strcmp( para, "/0") == 0 )
break;
printf("Parameter #%d is: %s/n", argno, para);
argno++;
}
va_end( argp ); /* 将 argp置为 NULL */
return 0;
}
#include < stdio.h >;
#include < string.h >;
#include < stdarg.h >;
/* 函数原型声明,至少需要一个确定的参数, 注意括号内的省略号 */
int demo( char *, ... );
void main( void )
{
demo("DEMO", "This", "is", "a", "demo!", "/0");
}
/* ANSI标准形式的声明方式,括号内的省略号表示可选参数 */
int demo( char *msg, ... )
{
va_list argp; /* 定义保存函数参数的结构 */
int argno = 0; /* 纪录参数个数 */
char *para; /* 存放取出的字符串参数 */
/* argp指向传入的第一个可选参数,
msg是最后一个确定的参数 */
va_start( argp, msg );
while (1)
{
/* 取出当前的参数,类型为 char *. */
para = va_arg( argp, char *);
/* 采用空串指示参数输入结束 */
if ( strcmp( para, "/0") == 0 )
break;
printf("Parameter #%d is: %s/n", argno, para);
argno++;
}
va_end( argp ); /* 将 argp置为 NULL */
return 0;
}
二、可变参类型陷井
下面的代码是错误的,运行时得不到预期的结果:
view plaincopy to clipboardprint?
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为 double,不支持 float
va_end(pArg);
va_start(pArg, plotNo);
fValue = va_arg(pArg, float); // 类型应改为 double,不支持 float
va_end(pArg);
下面列出 va_arg(argp, type)宏中不支持的 type:
—— char、 signed char、 unsigned char
—— short、 unsigned short
—— signed short、 short int、 signed short int、 unsigned short int
—— float
在 C语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“默认实际参数提升 (default argument promotions)”。该规则同样适用于可变参数函数——对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到 double
——char、 short和相应的 signed、 unsigned类型的实际参数提升到 int
——如果 int不能存储原值,则提升到 unsigned int
然后,调用者将提升后的参数传递给被调用者。
所以,可变参函数内是绝对无法接收到上述类型的实际参数的。
关于该陷井, C/C++著作中有以下描述:
在《 C语言程序设计》对可变长参数列表的相关章节中,并没有提到这个陷阱。但是有提到默认实际参数提升的规则:
在没有函数原型的情况下, char与 short类型都将被转换为 int类型, float类型将被转换为 double类型。
——《 C语言程序设计》第 2版 2.7 类型转换 p36
在其他一些书籍中,也有提到这个规则:
事情很清楚,如果一个参数没有声明,编译器就没有信息去对它执行标准的类型检查和转换。
在这种情况下,一个 char或 short将作为 int传递, float将作为 double传递。
这些做未必是程序员所期望的。
脚注:这些都是由 C语言继承来的标准提升。
对于由省略号表示的参数,其实际参数在传递之前总执行这些提升(如果它们属于需要提升的类型),将提升后的值传递给有关的函数。——译者注
——《 C++程序设计语言》第 3版 -特别版 7.6 p138
…… float类型的参数会自动转换为 double类型, short或 char类型的参数会自动转换为 int类型 ……
——《 C陷阱与缺陷》 4.4 形参、实参与返回值 p73
这里有一个陷阱需要避免:
va_arg宏的第 2个参数不能被指定为 char、 short或者 float类型。
因为 char和 short类型的参数会被转换为 int类型,而 float类型的参数会被转换为 double类型 ……
例如,这样写肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个 char类型参数,如果传递了,它将会被自动转化为 int类型。上面的式子应该写成:
c = va_arg(ap,int);
——《 C陷阱与缺陷》 p164