zoukankan      html  css  js  c++  java
  • 深度探索C语言函数可变长参数

     通常我们使用的C函数的参数个数都是固定的,但也有不固定的。比如printf()与scanf()。如何自己动手实现一个可变参数函数,这个还是有点技巧的。

    我们最常用的就是定义一个宏,使用printf或者printk,如下

    #define wwlogk(fmt, args...) printk(fmt, ## args)

    现在我们自己动手实现一个可变参数的函数,后面分析原理。首先看一个例子:

    #include <stdio.h>

    #include <stdarg.h>

    int Sum(int first, int second, ...)//当无法列出传递函数的所有实

    //参的类型和数目时,可用省略号指定参数表

    {

    int sum = 0, t = first;

    va_list vl;

    va_start(vl, first);

    while (t != -1){

    sum += t;

    t = va_arg(vl, int); //将当前参数转换为int类型

    }

    va_end(vl);

    return sum;

    }

     

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

    {

    printf("The sum is %d ", Sum(30, 20, 10, -1)); //-1是参数结束标志

    return 0;

    }

    在上面的例子中,实现了一个参数个数不定的求int型和的函数Sum()。 

    其中有几个变量需要说明一下。va_list、va_start()、va_end和va_arg。

    Va_list:该类型变量用来访问可变参数,实际上就是指针。

    Va_start():是一个宏,用来获取参数列表中的参数,使vl指向第一个可变参数,使用完毕后调用va_end()结束。

    va_end:也是一个宏,用来结束va_start()的调用。

    va_arg:宏,用来获参数列表中的取下一个值。

    在linux源代码中,include/acpi/platform/acenv.h,头文件有详细描述。

    1、 va_list vl;

    typedef char *va_list; //定义了一个新的类型,指向字符串的指针。其实真实意图是当指针移动是以"1"单位,因为sizeof(char) =1;即char类型占一个字节,int型占4个字节。

    2、 va_start(vl, first) 使vl指向第一个可变参数,即。

    #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

    #define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))

    _bnd(X, bnd)的定义主要是为了某些需要内存的对齐的系统,这个宏的目的是为了得到最后一个固定参数的实际内存大小。直接用sizeof也没有影响。

    #define _AUPBND (sizeof (acpi_native_int) - 1)

    其中acpi_native_int是根据硬件平台来决定的,即一个int类型的宽度,即字节。

    typedef s64 acpi_native_int;

    typedef s32 acpi_native_int;

    typedef int s32;

    typedef long long s64;

    下面就这段宏代码解释一下:

    进程运行时,将变量压入栈,而(char *) &(A)指向first,即栈顶;

           使用宏_bnd (A,_AUPBND),主要是为了某些系统需要内存按照整数字节对齐,因为C调用协议下面,参数入栈都是整数字节(指针或者值)--- 所谓对齐,对Intel80x86 机器来说就是要求每个变量的地址都是sizeof(int)的倍数。那为什么要对齐?因为在对齐方式下,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 |        |
    ------- ------- ------- ---------

           因为_AUPBND为int宽度,那么_bnd(A, _AUPBND)的意思就是不够一个int宽度的数据,将还是跳过一个int宽度。比如char、short类型的数据sizeof后为1和2,假设现在是32位系统char类型,_bnd(X, bnd) = (1 + 3 ) & (~3) = 0x4; 2也一样。因为32位系统int宽度为4。跳过以后,ap指向second,而first的值已经保存在t中。这里需要说明的是,因为题设已经指定first为int型。若为其他型(如char)则会出现错误,因为这里首先跳过了4个字节。

    图1 栈的结构

    另外这里还需要注意几点:

    1. 因为C语言压栈顺序为从右到左。
    2. 栈的扩展方向是向下扩展,所以栈底为高地址,栈顶为低地址。

      比如假设f(a,b,c,d)按照从右到左压栈,那么d应该是第一个进栈的,a是最后一个进栈的,所以d的地址应该比a的高。在intel+ windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。

    3. C语言压栈的时候,第一个进栈的是主函数中第一条指令的地址,然后依次是函数参数和局部变量。

        如上所述,va_start(vl,first)后,vl指向first后面的第一个可变参数。我们都知道Pascal的参数入栈顺序时自左向右的但是C语言会是自右向左。为什么呢?这也是C语言比pascal高级的一个地方--C语言通过这种参数入栈的顺序实现了对变长参数函数的支持!

    为了支持可变参数函数,C语言引入新的调用协议, 即C语言调用约定 __cdecl . 采用C/C++语言编程的时候,默认使用这个调用约定。如果要采用其它调用约定,必须添加其它关键字声明,例如WIN32 API使用PASCAL调用约定,函数名字之前必须加__stdcall关键字。 采用C调用约定时,函数的参数是从右到左入栈,个数可变。由于函数体不能预先知道传进来的参数个数,因此采用本约定时必须由函数调用者负责堆栈清理。

    3、#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))

        把这个宏展开可以看的更清楚:

    1. ap = ap + _bnd(T,_AUPBND) --首先将ap的跳过指定宽度,即指向下一个可变参数。
    2. *(T *)(ap - _bnd(T,_AUPBND)) --然后还原ap后,将其转化为"T"型指针并求指针的值。

    现在可以看清楚这个宏的意思是求当前ap指向的值,并将ap指向下一目标。

    4、va_end(vl) 把vl指针清为NULL

        #define va_end(ap)    (void) 0

    注意:这段代码只有在windows下编译和运行后,才能输出正确的结果,在linux下,需要将int Sum(int first, int second, ...)修改为int Sum(int first, ...),即删掉int second,因为second如果写出来,就不是可变参数,first也不是可变参数。修改后的函数在linux下和windows下都可以正常运行。

        搞清楚上面示例代码的原理后,我们可以自己手动实现这样一个函数。 

    #include <stdio.h>

    int Sum(int first, int second,...)

    {

    int sum = 0, t = first;

    char * vl;//定义一个指针

     

    vl = (char *)&first;//使指针指向第一个参数

    while (*vl != -1)//-1是预先给定的结束符

    {

    sum += *(int *)vl;//类型转换

    vl += sizeof(int);//移动指针,使指针指向下一个参数

    }

    return sum;

    }

     

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

    {

    printf("The sum is %d ", Sum(30, 20, 10, -1));//-1是参数结束标志

    return 0;

    }

        实际上声明一个可变参数有两种方式:建议使用第一种。

        第一种:包含头文件stdarg.h,采用ANSI标准形式,参数个数可变的函数的原型声明是:

    type funcname(type para1, type para2, ...)

    第二种:包含头文件varargs.h,采用与UNIX System V兼容的声明方式时,参数个数可变的函数原型是:

    type funcname(va_alist)

          va_dcl

    va_dcl为宏,宏定义原型后已经包含分号,所以使用时不用加分号。Va_dcl是对va_alist的详细声明。Va_dcl在代码中必须原样给 出,va_alist在VC中可以原样给出,也可以略去,但在UNIX上的CC或Linux上的GCC中都要省略掉。

     关于可变参数的传递问题

        有人问到这个问题,假如我定义了一个可变参数函数,在这个函数内部又要调用其它可变参数函数,那么如何传递参数呢?上面的例子都是使用宏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 <stdio.h>
    #include <stdarg.h>

    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 Int: %d, %d Float :%4.2f ", 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代 码就可以做到。

    ---------------------------------------------------------

    问题一:

     上面这段代码经测试可以正常输出。也就是说,我们通过使用指针,实现了参数不定的函数。但这里还有一个问题,就是sum函数的所有参数都是int类型的,事先我们知道要移动sizeof(int)位的指针,可是如果参数类型不同呢?

     答案与分析:这的确是个比较麻烦的问题,因为不同的数据类型占用的字节数可能是不一样的(如double型为8个字符,short int型为2个),所以很难事先确定应该移动多少个字节!但是办法还是有的,这就是使用指针了,无论什么类型的指针,都是占用4个字节,所以,可以把所有的传如入参数都设置为指针,这样一来,就可以通过移动固定的4个字节来实现遍历可变参数的目的了,至于如何取得指针中的内容并使用他们,当然也是无法预先得知的了。所以这大概也就是像printf(),scanf()之类的函数还需要一个格式控制符的原因吧^_^!不过实现起来还是有不少麻烦,暂且盗用vprintf()来实现一个与printf()函数一样功能的函数了,代码如下:

    void myPrint(const char *frm, ...)

    {

        va_list vl;

        va_start(vl, frm);

        vprintf(frm, vl);

        va_end(vl);

    }  

    -----------------------------------------------------------

    问题二: 还有一个问题,是上述问题的变体,不过意思相同:有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?  

    答案与分析:目前没有"正规"的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是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);
           ...
    }

    -------------------------------------------------------------

    问题四:如何判别可变参数函数的参数类型?
    函数形式如下:
    void   fun(char*   str,...)
    {
    ......
    }
    若传的参数个数大于1,如何判别第2个以后传参的参数类型???最好有源码说明! 
       答案与分析:无法判断。可变参数实现主要通过三个宏实现:va_start,va_arg,va_end。
       如楼上所说,例如printf( "%d%c%s ",   ....)是通过格式串中的%d,%c,%s来确定后面参数的类型,其实你也可以参考这种方法来判断不定参数的类型。

    -------------------------------------------------------------
    问题五:定义可变长参数的一个限制
       为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?
    int f (...)
    {
       ...
    }

    答案与分析:不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。
       这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。

    ---------------------------------------------------------------------
    问题六:可变长参数的获取     
        有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:
    va_arg (argp, float);

    这样做可以吗?
    答案与分析:不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。
    ---------------------------------------------------------------------
    问题七:可变长参数中类型为函数指针
       我想使用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)))
    这样就可以通过编译检查了。

    知识扩展

    可能大家也猜到了,我扩展要扩展什么了?!^_^

    简单介绍两种函数调用约定

    __stdcall (C++默认)

    1. 参数从右向左压入堆栈
    2. 函数被调用者修改堆栈
    3. 函数名(在编译器这个层次)自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数的尺寸

    __cdecl (C语言默认)

    1. 参数从右向左压入堆栈
    2. 参数由调用者清楚,手动清栈,被调用函数不会要求调用者传递多少参数,调用者传递过多或者过少的参数,甚至完全不同的参数都不会产生编译阶段的错误。

    那么,变参函数的调用方式为(也只能是):__cdecl 。

  • 相关阅读:
    Mybatis 内置 Java 类型别名与 typeHandlers
    泛型方法前为什么要加<T>
    jdbcTemplate学习(四)
    jdbcTemplate学习(三)
    jdbcTemplate学习(二)
    jdbcTemplate学习(一)
    博客园markdown toc
    office,ps 等入门教程链接
    mysql 手动加锁测试
    拆机联想ideapad s500
  • 原文地址:https://www.cnblogs.com/twlqx/p/4340581.html
Copyright © 2011-2022 走看看