函数可变参数、va_list、va_start、va_arg、va_end

  • 在API使用过程中,总能看到methodName:(id)pra,...样式的可变参数传入。

  • 获取可变参数中参数的主要依据是: C语言的函数形参是从右向左压入堆栈的,以保证栈顶是第一个参数,而且mac os内存分配顺序是从高地址到低地址。
    例如:
    - (void)test:(NSString *)str,...
    {
    va_list ap;
    va_start(ap, str);
    id strtest = str;
    while (strtest) {
    NSLog(@"%p",strtest);
    strtest = va_arg(ap, id);
    }
    va_end(ap);
    }
    调用:
    [self test:@"111111",@"222222",@4000,@"333333",@"444444",@"555555",@"666666",nil];
    输出 :
    2016-04-07 00:21:01.053 test[64592:2915126] 0x108cb26a0
    2016-04-07 00:21:01.053 test[64592:2915126] 0x108cb26c0
    2016-04-07 00:21:01.054 test[64592:2915126] 0xb00000000000fa02
    2016-04-07 00:21:01.054 test[64592:2915126] 0x108cb26e0
    2016-04-07 00:21:01.054 test[64592:2915126] 0x108cb2700
    2016-04-07 00:21:01.054 test[64592:2915126] 0x108cb2720
    2016-04-07 00:21:01.054 test[64592:2915126] 0x108cb2740
    由上述代码,及其执行结果可得:

    • mac os内存分配顺序是从高地址到低地址。
    • 0x108cb26e0: 常量地址,0xb00000000000fa02:变量地址
  • VA_LIST的用法:
    (1)首先在函数里定义一具VA_LIST型的变量,作用:指向参数的指针;
    (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量,初始化后,VA_LIST指针指向可变参数组中第二个参数;
    (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型,执行宏定义后,VA_LIST指针依次指向下一个参数;
    (4)最后用VA_END宏结束可变参数的获取。

  • 可变参数是由宏实现的,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是VC6.0中x86平台的定义 :
    typedef char * va_list; // TC中定义为void*
    #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) //为了满足需要内存对齐的系统
    #define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) //ap指向第一个变参的位置,即将第一个变参的地址赋予ap
    #define va_arg(ap,t) ( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) /获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容/
    #define va_end(ap) ( ap = (va_list)0 ) //清空va_list,即结束变参的获取

  • 使用VA_LIST应该注意的问题:
    (1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
    (2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
    (3)由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。

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

    2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
    ⑴在固定参数中设标志-- printf函数就是用这个办法。
    ⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
    无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。

    3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
    ①函数栈的生长方向
    ②参数的入栈顺序
    ③CPU的对齐方式
    ④内存地址的表达方式
    结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。

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

可变参数的函数原理其实很简单,而va系列是以宏定义来定义的,实现跟堆栈相关.我们写一个可变函数的C函数时,有利也有弊,所以在不必要的场合,我们无需用到可变参数.如果在C++里,我们应该利用C++的多态性来实现可变参数的功能,尽量避免用C语言的方式来实现.

  • 参考文章
    C语言中可变参数的用法——va_list、va_start、va_arg、va_end参数定义
    va_list原理及用法

你可能感兴趣的:(函数可变参数、va_list、va_start、va_arg、va_end)