C语言中可变参数的实现

来源:互联网 发布:扫码出答案的软件 编辑:程序博客网 时间:2024/05/18 02:27

      最近在学习C++,既然名叫C plus plus,自然在学习的过程中特意留意了一下,这个plus到底plus在了哪里,究竟哪里比C好用。果然,在学习了类,重载,模板这些特性之后,不得不说,C++的确比C方便了许多。但不知为什么,虽然重载、模板给C++的使用带来了极大的方便,我还是钟情于C语言,也许是因为我的“初恋”是C吧——不都说初恋是最难忘的嘛。

      言归正传。在实验室学长的介绍下,最近又接触到了一个比较新奇的事物——可变形参。说新奇也算不上,因为曾经也曾目睹过其芳容:

       intprintf(const char *format, ...);

       int fprintf(FILE *stream, const char*format, ...);

       int sprintf(char *str, const char*format, ...);

       int snprintf(char *str, size_t size,const char *format, ...);

      这是在man手册中,有关printf函数族的原型。曾看过至少五次有关printf的man手册,每次都会看到形参中的“…”,却没有一次去仔细研究过它。这一次,终于能揭开它的神秘面纱。

      这已经不是printf第一次给我惊喜了。今年五月份,参加实验室纳新笔试的时候,有一道题是怎样实现if和else后的语句块同时执行的问题,那道题的其中一种解法就是巧妙地利用了printf的返回值。那个时候,才知道原来printf还有返回值(求不喷)。

      而今,printf的形参表,给了我第二次惊喜。

     

      利用这个看着不怎么起眼的三个点“...”,可以在C语言中实现可变形参,即,参数个数不定。

      例如,定义了一个add函数,用来将形参相加求和,但事先却不知道参数的个数,这个时候,可变形参就派上了用场:

#include <stdio.h>#include <stdarg.h> void add( int n, ... ){      va_list p;      int sum=0;      va_start(p,n);      for( ; n; n-- )      {           sum += va_arg(p,int);      }          va_end(p);      printf("%d\n",sum);} int main( int argc, char *argv[] ){      add( 5, 1, 2, 3, 4, 5 );      return 0;}


      在add函数中,n即为参数的个数,具体的参数因个数不能确定,则用“…”来代替。在程序中,有几个陌生的面孔,现在一起来认识一下:

      va_list实际上是一种char *类型,即:typedef char* va_list;

      而va_start, va_arg, va_end则是定义在stdarg.h中的宏:

         #define_INTSIZEOF(n)    ((sizeof(n) + sizeof(int) - 1) &~(sizeof(int) - 1) ) 
         #defineva_start(ap,v)     ( ap = (va_list)&v + _INTSIZEOF(v)) 
         #defineva_arg(ap,type)  ( *(type *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))) 
         #defineva_end(ap)          ( ap =(va_list)0 )

 

      在程序中,我们先定义了一个指针p,接下来,可以看到,接下来主要就是va_arg宏在担任主角。va_arg有两个参数,一个是之前定义好的指针p,另一个就是要读取的参数类型type。从该宏的定义:*(type *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t))可以看出,实际上就是对指针p进行位移操作来读取参数。

我们知道,函数在传递参数时,参数都是在内存中的栈区来进行存放的。在调用一个函数时,从右到左读取形参表中的参数,从最后一个开始入栈。举个例子如下:

    void func( int x, float y, char z );
  那么,调用函数的时候,实参 char z 先进栈,然后是 float y,最后是 int x,因此在栈中,这三个变量的存放次序是 x->y->z,因此,从理论上说,我们只要探测到任意一个变量的地址,并且知道其他变量的类型,通过指针移位运算,便可以顺藤摸瓜找到其他的变量。

    也就是说,可变形参,实质上就是在栈中,使用指针来遍历堆栈段中的参数列表,用传递过来的参数个数n作为计数器,从低地址到高地址一个一个地把参数内容读出来的过程。

    下面再来总结一下可变参数的实现步骤:

    Step 1:在含有可变参数的函数中,定义一个va_list(char*)型变量p;

    Step 2:使用va_start宏来对刚刚定义好的变量p进行初始化,始其指向第一个要进行操作的参数。从操作p =(va_list)&n + _INTSIZEOF(type) 可以看出,该操作就是取出第一个形参n的地址,再加上其类型大小,使指针偏移到下一个变量;

    Step 3:使用va_arg宏来获取参数。从操作出可以看出,所接受的第一个参数为当前指针p,通过第二个参数type来确定下一次偏移大小,使p偏移到下一个要操作的参数;

    Step 4:参数都读取完毕后,即传入的计数器n归0时,调用va_end宏。从操作p =(va_list)0可以看出,就是一个将指针p进行置空的操作。

 

    明白了可变参数的实现原理,我们也不难来自己实现这样的操作,不使用stdarg.h头文件,具体操作如下:

#include <stdio.h> void add(int n, ... ){    int sum = 0;    int *p = &n;          //p指向了传入的参数n    p++;                    //p跳过参数n,挪向后一个参数2    while(n)                //n为计数器,表明将要传入5个参数    {          sum += *p;          p++,n--;    }       printf("%d\n",sum);} int main(int argc, char *argv[] ){    add( 5, 2, 3, 4, 5, 6 );    return 0;}


    只不过,使用va_start,va_arg,va_end这些宏,会方便我们的操作与使用,不用再自己来进行这些繁琐而又易错的指针位移运算,而且见名知义,程序可读性更佳。

    同理,如果可变参数是字符串,可以这样来进行操作:

#include<stdio.h>#include<stdarg.h> void show(int n, ... ){    va_list p;    va_start( p, n );    for( ; n; n-- )    {          puts( va_arg(p, char*) );    }} int main(int argc, char *argv[] ){    show( 2, "Hello,","world!\n" );    return 0;}


 

    上面的例子中,所有的形参都是同样的数据类型的。但回到开关我们提出的例子,我们在使用printf时,常常会将不同类型的参数放到一起,如:

    printf(“%d %f %s”,i, j, str );

    这个时候,再用上面的方法来进行输出,就略为麻烦了。我们可以用到vsprintf这个函数,在man手册中可以查看到它的详细的说明。它的原型为:

    int vsprintf(char *buffer, char *format,va_list param);

    实际上,printf函数的实现非常复杂,其中就封装了vsprintf。vsprintf将可变参列表param中的变量按照format中规定的格式保存到临时缓冲buffer中,最后调用 puts函数将临时缓冲中的字符串数据打印到终端中去。于是,我们“自己的printf函数”——show就可以这样来实现:

#include<stdio.h>#include<stdarg.h> void show(char *format, ... ){    char buffer[100];    va_list p;    va_start( p, format );    vsprintf( buffer, format, p );    va_end(p);    puts(buffer);} int main(int argc, char *argv[] ){    int i = 1;    float j = 3.14159;    char *str = "hello";    show( "%d %f %s", i, j, str );    return 0;}
 

    最后,还有几点要注意,可变参列表“…”不能写在第一个形参的位置,否则编译器会报错:

    错误: ISO C 要求在‘...’前有一个有名参数

    其次,利用这一特点,在运行时vsprintf函数,或自己通过指针的偏移来获取函数可变形参,这就会出现未定义的行为。如果经精心安排,即可实现变相修改函数的返回地址,程序将按设计者的安排肆意执行,其带来的危险性可想而知。

    对比到C++的cin,cin不能支持这种可变形参的函数形式,大概跟这类风险有一定的原因吧!

原创粉丝点击