C语言实现参数个数可变的函数(转)

来源:互联网 发布:java calendar.set 编辑:程序博客网 时间:2024/05/09 17:16
参考文档一:
采用C语言编程的时候,函数中形式参数的数目通常是确定的,在调用时要依次
给出与形式参数对应的所有实际参数。但在某些情况下希望函数的参数个数可以根据
需要确定。典型的例子有大家熟悉的函数printf()、scanf()和系统调用execl()等。
那么它们是怎样实现的呢?
  C编译器通常提供了一系列处理这种情况的宏,以屏蔽不同的硬件平台造成的差
异,增加程序的可移植性。这些宏包括va—start、va—arg和va—end等。
  使用这些宏有两种不同的形式,二者在程序中包括的头文件不同,宏的定义也存
在一些差别。这两种方式对应的头文件和宏的声明见表1。
  采用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中可以原样给出,也可以略去,
但在UNIX上的CC或Linux上的GCC中都要省略掉。此外,采用头文件stdarg.h编写的程
序是符合ANSI标准的,可以在各种操作系统和硬件上运行,而采用头文件varargs.h的
方式仅仅是为了与以前的程序兼容。所以建议大家使用前者。两种方式的基本原理是
一致的,只是在语法形式上有一些细微的区别。以下主要就前一种方式对参数的处理
做出说明。
  va—start使argp指向第一个可选参数。va—arg返回参数列表中的当前参数并
使argp指向参数列表中的下一个参数。va—end把argp指针清为NULL。函数体内可以
多次遍历这些参数,但是都必须以va—start开始,并以va—end结尾。
  调用者在实际调用参数个数可变的函数时,要通过一定的方法指明实际参数的个
数,例如把最后一个参数置为空字符串(系统调用execl()就是这样的)、-1或其他
的方式。
  下面给出一个具体的例子,前一部分是采用了符合ANSI标准形式的代码,后一部
分是采用了与UNIX System V兼容方式的代码。代码中加了一些注释,这里就不再解
释了。该例子已经在VC/Windows NT4.0、CC/AIX4.3.2.0、GCC/Redhat Linux 6.0环
境下编译并正常运行。

  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 0;
  }

  2.演示如何使用参数个数可变的函数,采用与UNIX System V兼容的方式
  #include 〈stdio.h〉
  #include 〈string.h〉
  #include 〈varargs.h〉
  /* 函数原型声明,括号内的类型va—list在VC/Windows NT4.0可以保留,但在AIX和Linux下需要去掉,即改成int demo( ) */
  int demo( va—list );

  void main( void )
  {
   demo(″This″, ″is″, ″a″, ″demo!″, ″/0″);
  }

  /* UNIX System V采用的声明方式,括号内是va—alist,不是va—list,而且va—dcl后面不需要分号 */
  int demo( va—alist )
  va—dcl  
  {
  va—list argp;
   /* 定义保存函数参数的结构 */
     int argno = 0; /* 纪录参数个数 */
     char para;
  /* 存放取出的字符串参数 */
  va—start( argp );
  /* argp指向第一个可选参数 */

  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 0;
  }

表1
+=================================================================+
|   | ANSI标准形式 | UNIX SystemⅤ兼容方式 |
+--------+----------------------------+---------------------------+
| 头文件 | #include <stdarg.h> | #include <varargs.h> |
+--------+----------------------------+---------------------------+
|va_start| void va_start(argp, paran) |void va_start(argp) |
| | va_list argp; | va_list argp; |
+--------+----------------------------+---------------------------+
|va_arg | type va_arg(argp, type) |type va_arg(argp, type) |
| | va_list argp; | va_list argp; |
+--------+----------------------------+---------------------------+
|va_end |void va_end(argp) |void va_end(argp) |
| | va_list argp; | va_list argp; |
+--------+----------------------------+---------------------------+

参考文档二

// 下文转自网络

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函数

先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定. 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.

函数代码如下:

//示例代码1:可变参数函数的使用

#i nclude "stdio.h"

#i nclude "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/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指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。

这里要知道两个事情:

    [1]在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()的实现就应该心中有数了:

#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 language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

(四)小结:

1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。

2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如

⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。

⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.

无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。

3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:

①函数栈的生长方向

②参数的入栈顺序

③CPU的对齐方式

④内存地址的表达方式

结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。

4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。

(五)扩展——自己实现简单的可变参数的函数。

下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的156页的例子
#i nclude "stdio.h"

#i nclude "stdlib.h"

void myprintf(char* fmt, ...)//一个简单的类似于printf的实现,//参数必须都是int 类型

{

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

    char c;

    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;

}

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;








   
原创粉丝点击