可变实参函数和变参的类型提升问题总结

因为网上关于可变实参的资料很多,所以我只做简单讲解,想了解更深,可以再去网上查查其他资料。在文章最后,我也会推荐几篇网上的相关资料。

正文:

某些情况下希望函数的参数个数可以根据需要确定。典型的例子有大家熟悉的函数printf()、scanf()和系统调用execl()等。那么它们是怎样实现的呢?C编译器通常提供了一系列处理这种情况的宏,以屏蔽不同的硬件平台造成的差异,增加程序的可移植性。这些宏包括va_start、va_arg和va_end等。

va_list, va_start这些都是包含在头文件stdarg.h中,所以用到这些宏时需要包含头文件stdarg.h。

首先先来看看va_start, va_list, va_arg, va_end几个宏的意义:

va_list:一个char链表(实际上应该是一个连续的内存块,像数组一样),在使用时表现为一个指向char类型的指针;

va_start:初始化va_list。通过最后的固定参数实现对可变参数初始位置的定位,并为va_list分配内存,将可变参数复制该内存块中,使va_list指向该内存块的初始位置;

va_arg:通过移动指针va_list获取由参数Type指定的变量并返回该变量。

va_end:释放va_list拥有的内存块所占据的内存空间。

在MSDN里是这么解释的:

va_arg 

Macro to retrieve current argument

va_end
Macro to reset arg_ptr

va_list
typedef for pointer to list of arguments defined in STDIO.H

va_start
Macro to set arg_ptr to beginning of list of optional arguments (UNIX version only)

  • va_start sets arg_ptr to the first optional argument in the list of arguments passed to the function. The argument arg_ptr must have va_list type. The argument prev_param is the name of the required parameter immediately preceding the first optional argument in the argument list. If prev_param is declared with the register storage class, the macro’s behavior is undefined. va_start must be used before va_arg is used for the first time.
  • va_arg retrieves a value of type from the location given by arg_ptr and increments arg_ptr to point to the next argument in the list, using the size of type to determine where the next argument starts. va_arg can be used any number of times within the function to retrieve arguments from the list.
  • After all arguments have been retrieved, va_end resets the pointer to NULL.

    下面来实现并使用一个模拟printf的函数my_printf:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    
    // Author: Tanky Woo
    // Blog:     www.WuTianQi.com
    #include <iostream> 
    #include <stdarg.h> 
    using namespace std; 
     
    void my_printf(char *fmt, ...) 
    { 
        va_list ap; 
        char *p, *sval; 
        int ival; 
        double dval; 
        char cval; 
        va_start(ap, fmt); 
        for(p=fmt; *p; ++p) 
        { 
            if(*p != '%') 
                putchar(*p); 
            else 
                switch(*++p) 
                { 
                    case 'd': 
                        ival = va_arg(ap, int); 
                        printf("%d", ival); 
                        break; 
                    case 'f': 
                        dval = va_arg(ap, double); 
                        printf("%f", dval); 
                        break; 
                    case 's': 
                        sval = va_arg(ap, char*); 
                        for(; *sval; ++sval) 
                            putchar(*sval); 
                        break; 
                    case 'c':                  // 注意这里有问题,下面会讲到 
                        cval = va_arg(ap, char); 
                        printf("%c", cval); 
                        break; 
                    default: 
                        putchar(*p); 
                        break; 
     
                } 
        } 
        va_end(ap); 
    } 
     
    int _tmain(int argc, _TCHAR* argv[]) 
    { 
        int a = 5; 
        char b = 'm'; 
        my_printf("%d %c", a, b); 
        return 0; 
    }

    最后说一下一个容易被忽略的问题

    变参的提升问题

    在C语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“默认实际参数提升(default argument promotions)”。该规则同样适用于可变参数函数——对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。

    提升工作如下:
    ——float类型的实际参数将提升到double
    ——char、short和相应的signed、unsigned类型的实际参数提升到int
    ——如果int不能存储原值,则提升到unsigned int

    然后,调用者将提升后的参数传递给被调用者。

    所以,可变参函数内是绝对无法接收到上述类型的实际参数的。

    关于这个问题,我在CSDN上专门和一名叫we_sky2008的牛人讨论过:

    http://topic.csdn.net/u/20101210/21/fe6f0072-94eb-4b7b-8e11-4416a44dc4cb.html

    就以上面我写的代码讨论,我在遇到%c时,注释加了一句“注意这里有问题,下面会讲到”,问题就出在提升上面。

    我当时不明白,依然输出5 m,那么提升在哪里了?

    we_sky2008给我列出了va_arg等几个的宏:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    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 )

    #define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) – 1) )
    的目的在于把sizeof(n)的结果变成至少是sizeof(int)的整倍数,这个一般用来在结构中实现按int的倍数对齐。

    注意这里的va_arg,虽然ap提升了,但是宏没变,_INTSIZEOF(t)抵消了。这就导致显示的没变,但是他的类型提升了。

    他当时给我列了这个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    
     
    #include <iostream> 
     
    using namespace std; 
     
    void my_printf(char *fmt, ...) 
    { 
        char *arg = (char *)(&fmt + 1); 
        char *p, *sval; 
        int ival; 
        double dval; 
        char cval; 
     
        for(p=fmt; *p; ++p) 
        { 
            if(*p != '%') 
                putchar(*p); 
            else 
            { 
                switch(*++p) 
                { 
                    case 'd': 
                        ival = *(int *)arg; 
                        arg += sizeof(int); 
                        printf("%d", ival); 
                        break; 
                    case 'f': 
                        dval = *(double *)arg; 
                        arg += sizeof(double); 
                        printf("%f", dval); 
                        break; 
                    case 's': 
                        sval = *(char **)arg; 
                        arg += sizeof(char **); 
                        for(; *sval; ++sval) 
                            putchar(*sval); 
                        break; 
                    case 'c': 
                        cval = *(char *)arg; 
                        arg += sizeof(int);//这里因为参数传递时有类型提升,所以此处应该是arg += sizeof(char);,移动一个字节的话就会有问题 
                        printf("%c", cval); 
                        break; 
                    default: 
                        putchar(*p); 
                        break; 
                } 
            } 
        } 
    } 
     
    int main() 
    { 
        int a = 5; 
        char b = 'm'; 
        my_printf("%d %c %c %c %c %c", a, b, b, b, b, b);   //这里就会有问题了 
        system("pause"); 
        return 0; 
    }

    问题就出来了,在case ‘c’时,因为char提升了,所以arg+=sizeof(char)是错误的,应该是arg+=sizeof(int)。

    具体还是看看我们当时讨论的帖子。

    另外,这里有几个总结的很好的帖子,大家可以去学习一下:

    http://blog.csdn.net/jinkui2008/archive/2007/12/25/1967064.aspx

    http://blog.csdn.net/flyoxs/archive/2009/04/22/4099317.aspx

    http://blog.csdn.net/dexingchen/archive/2008/11/29/3411686.aspx

    http://topic.csdn.net/u/20101210/21/fe6f0072-94eb-4b7b-8e11-4416a44dc4cb.html

  • Tanky Woo 标签:

     

  • 发布者

    Tanky Woo

    Tanky Woo,[个人主页:https://tankywoo.com] / [新博客:https://blog.tankywoo.com]

    《可变实参函数和变参的类型提升问题总结》有440个想法

    1. vanxining :呵呵,我曾经查资料写过一个类似的、用于输出调试信息的 printf 函数,起因好像是那个编译器不提供 Unicode 版本(wprintf),不过现在都忘光了

      就像楼主现在做的事情一样,这种东西,又繁又难又小众,很快就会忘掉的

      1. 嗯,他的文件比一般主题多了N多。我至少下载过50,60多个主题,他是我见过最繁琐的,但是看我博客,谁也不会想到会那么多功能。。。

      1. 变参我个人感觉其实属于基础知识的,用不用先不要管,先得知道有这个概念,并且了解基本用法,这样在以后真正用到时,才可以手到擒来。

        1. 呵呵,我曾经查资料写过一个类似的、用于输出调试信息的 printf 函数,起因好像是那个编译器不提供 Unicode 版本(wprintf),不过现在都忘光了

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注