函数中省略号的意义和用法

        某些情况下希望函数的参数个数可以根据需要确定。典型的例子有大家熟悉的函数printf()、scanf()和系统调用execl()等。那么它们是怎样实现的呢?C编译器通常提供了一系列处理这种情况的宏,以屏蔽不同的硬件平台造成的差异,增加程序的可移植性。这些宏包括va_start、va_arg和va_end等。       
    
    
  ----   采用ANSI标准形式时,参数个数可变的函数的原型声明是:       
    
  type   funcname(type   para1,   type   para2,   ...)       
  ----   这种形式至少需要一个普通的形式参数,后面的省略号不表示省略,而是函数原型的一部分。type是函数返回值和形式参数的类型。       
  ----   采用与UNIX   System   V兼容的声明方式时,参数个数可变的函数原型是:       
    
  type   funcname(va_alist)       
  va_dcl       
    
  ----   这种形式不需要提供任何普通的形式参数。type是函数返回值的类型。va_dcl是对函数原型声明中参数va_alist的详细声明,实际是一个宏定义,对不同的硬件平台采用不同的类型来定义,但在最后都包括了一个分号。因此va_dcl后不再需要加上分号了。va_dcl在代码中必须原样给出。va_alist在VC中可以原样给出,也可以略去。       
  ----   此外,采用头文件stdarg.h编写的程序是符合ANSI标准的,可以在各种操作系统和硬件上运行;而采用头文件varargs.h的方式仅仅是为了与以前的程序兼容。所以建议大家使用前者。以下主要就前一种方式对参数的处理做出说明。两种方式的基本原理是一致的,只是在语法形式上有一些细微的区别。       
    
  ----   va_start使argp指向第一个可选参数。va_arg返回参数列表中的当前参数并使argp指向参数列表中的下一个参数。va_end把argp指针清为NULL。函数体内可以多次遍历这些参数,但是都必须以va_start开始,并以va_end结尾。       
    
  ----   调用者在实际调用参数个数可变的函数时,要通过一定的方法指明实际参数的个数,例如把最后一个参数置为空字符串(系统调用execl()就是这样的)、-1或其他的方式(函数printf()就是通过第一个参数,即输出格式的定义来确定实际参数的个数的)。       
    
  ----   下面给出一个具体的例子。是采用了符合ANSI标准的形式的代码。代码中加了一些注释,这里就不再解释了。该例子已经在VC/Windows   XP、CC/AIX4.3.2.0、GCC/SUSE7.3环境下编译并正常运行。       
    
  ----   1、演示如何使用参数个数可变的函数,采用ANSI标准形式       
    
  #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)   {       
  para   =   va_arg(   argp,   char   *);    /*   取出当前的参数,类型为char   *.   */        
  if   (   strcmp(   para,   "\0")   ==   0   )       
  /*   采用空串指示参数输入结束   */        
  break;       
  printf("Parameter   #%d   is:   %s\n",   argno,   para);       
  argno++;       
  }       
  va_end(   argp   );    /*   将argp置为NULL   */        

  return


二、写一个简单的可变参数的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("%dth arg: %d\n",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。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。
至于为什么它不会知道参数的数目,读者在看完下面这几个宏的内部实现机制后,自然就会明白。

三、可变参数在编译器中的处理 
我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中
stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\include\stdarg.h)

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 ) 
下面我们解释这些代码的含义: 
1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的 
2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。
   在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。 
3、va_start的定义为&v+_INTSIZEOF(v),而&v是最后一个固定参数的起始地址,再加上其大小后,就得到了第一个可变参数的起始内存地址。
   所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。 
这里要知道两个事情: 
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。 
(2)在VC等绝大多数C编译器中,参数进栈的顺序是由右向左的,因此, 
参数进栈以后的内存模型如下图所示:最后一个固定参数的地址正好位于第一个可变参数之下,并且是连续存储的。 
|—— —————————————| 
| 最后一个固定参数 | ->高内存地址处 
|— ——————————————|

........................ 
|-------------------------------| 
| 第N个可变参数 | ->va_arg(arg_ptr,datatype)后arg_ptr所指的地方 
|-------------------------------|

................... 
|——— ————————————| 
| 第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方 
| | 即第一个可变参数的地址 
|——————————————— | 
|————————————— ——| 
| | 
| 最后一个固定参数 | -> start的起始地址 
|—————————————— —| 
............... 
|——————————————- | 
| | 
|——————————————— | -> 低内存地址处

(4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,
并且把指针调到下一个参数的起始地址。 
因此,现在再来看va_arg()的实现就应该心中有数了: 
#define va_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的经典教材《the c programming》的7.3节中
就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

五、小结: 
1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。 
2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如 
⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。 
⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,
在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法——当可变参数的值为-1时,即认为得到参数列表的结尾。 
无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。这是一个不太方便 
3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定: 
①函数栈的生长方向 
②参数的入栈顺序 
③CPU的对齐方式 
④内存地址的表达方式 
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,
最后va_end的存在则是良好编程风格的体现—将不再使用的指针设为NULL,这样可以防止以后的误操作。 
4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信有经验的读者就可以写出适合于自己机器的实现来。
下面就是一个例子 
六、实践——自己实现简单的可变参数的函数。 
下面是一个简单的printf函数的实现,参考了<The C programming language>中的156页的例子,读者可以结合书上的代码与本文参照。 
#include "stdio.h" 
#include "stdlib.h" 
void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,参数必须都是int 类型 

char* pArg=NULL; //等价于原来的va_list 
char c; 

pArg = &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; 

int main(int argc, char* argv[]) 

int i = 1234; 
int j = 5678; 

myprintf("the first test:i=%d\n",i,j); 
myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xabcd,j); 
system("pause"); 
return 0; 

在intel+win2k+vc6的机器执行结果如下: 
the first test:i=1234 
the secend test:i=1234; 0xabcd;j=5678;

又如:
#include <stdarg.h>
char buffer[80];
int vspf(char *fmt, ...) 

va_list argptr; 
int cnt; 
va_start(argptr, fmt); 
cnt = vsprintf(buffer, fmt, argptr); 
va_end(argptr); 
return(cnt); 

int main(void) 

int inumber = 30; 
float fnumber = 90.0; 
char string[4] = "abc"; 
vspf("%d %f %s", inumber, fnumber, string); 
printf("%s\n", buffer); 
return 0; 
}
程序结果为
30 90.000000 abc



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值