Tanky WooRSS

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

12 Dec 2010
这篇博客是从旧博客 WordPress 迁移过来,内容可能存在转换异常。

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

正文:

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

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

首先先来看看vastart, valist, vaarg, vaend几个宏的意义:

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)

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

// Author: Tanky Woo
// Blog:     www.WuTianQi.com
#include  
#include  
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,那么提升在哪里了?

wesky2008给我列出了vaarg等几个的宏:

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的倍数对齐。

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

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

#include  

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 标签: 可变形参类型提升