关于c中可变参数函数的实现及默认参数提升

先来讲可变参数函数,这个最常见的就是printf,scanf

一般来说这两个函数应该算是最早接触的那一批才是,但是其参数数目可变的特殊性却被我忽略了好久,直到前一阵程老师上课才发现

于是就稍微整理下好了,主要是va_list, va_start, va_arg, va_end的使用

不多说,直接上代码:

sum是一个求和的函数,其第一个参数为参数数目,后面是要求和的数

#include <stdio.h>
#include <stdarg.h>

int sum(int num,...);

int main(void)
{
    int a=sum(8,2,5,3,6,4,7,8,5);
    int b=sum(4,5,1,2,3);
    printf("a=%d, b=%d\n",a,b);
    getch();
    return 0;
}

int sum(int num,...){
    va_list ap;
    int s=0;
    va_start(ap,num);/* ap初始化,从num开始*/
    while (num>0){
        s+=va_arg(ap,int);/*从参数里取一个新值,同时ap向后移至下一个参数的位置*/
        --num;
    }
    va_end(ap);
    return s;
}

输出结果为a=40,b=11,好用

可变参数函数的声明特点是,参数列表里有“...”出现,表示此处可以接纳无限多个参数

我们都知道函数传参的时候是从右往左依次压栈的,所以参数的地址是连续的,只要从头往下一直移啊移的又不越界,多少个参数的值都能取得到。

 

在具体代码里,首先定义一个va_list指针ap,取各个参数的值就靠他了;

va_start给ap的地址初始化;注意此时ap指向的地址其实是num的下一个参数

之后每次调用va_arg时,会返回ap指向的参数的值,同时ap会移动至下一个参数的地址

所以在调用va_arg时要传入你要读入的变量的类型(即大小)使之能移动合适的距离

最后使用va_end来释放资源。

 

通过翻头文件,发现其实va系列的全都是宏:

va_list的:

typedef char * va_list;

(原来就是一个char*的指针啦)

va_start的:

#define va_start(ap,v)  (ap = (va_list)&v + _INTSIZEOF(v))

(可以看到ap指向的是v的地址+v的大小,即v的下一个参数)
va_arg的:

#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))

(可以看到,会让ap挪到下一个参数,同时返回ap挪之前那个参数的值)
va_end的:

#define va_end(ap) (ap = (va_list)0)

(看上去好像没什么用,但是说不准哪个平台下va_list是分配的堆内存,那届时这里的释放操作就很必要了,所以va_end不能省)
同时注意到他取变量的大小,使用的不是sizeof而是另一个宏_INTSIZEOF:

#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) -1))

n的大小都取整到int的倍数了

这意味着就算你输入了va_arg(ap,char),他也会向后移4个字节指向下一个变量。即:传进来的char变量,实际上占用了int的大小!

令人不禁想起之前提到的struct内存对齐。

 

上网查询资料,得到了“默认参数提升”(Default Argument Promotions)这个说法:

当函数的参数数量不定时,会发生默认参数提升。默认参数提升,会使char,short变量转换为int类型再压进栈,而float变量会转换为double类型再压进栈。

这样就可以理解为什么printf里%d同时支持int到char,%f同时支持float和double了。scanf里面有%lf来对应double,是因为scanf传递的是地址而不是变量,不受默认参数提升的影响。

 

可以看到,因为无法得知具体参数大小,出现了默认参数提升这种做法。那如果连参数的类型都不告诉你,会发生什么事呢?

看如下代码:

#include "stdio.h"

printit(a,b,c){
printf("%u\n%u\n%u\n",sizeof(a),sizeof(b),sizeof(c));
}

int main(void)
{
float a=3.45f;
char b='C';
double c=6.6;
printit(a,b,c);
return 0;
}

注意到函数printit(a,b,c),其参数都完全没有说明其类型。这个能通过tc和gcc的编译

最后执行的结果竟然是3个int的大小!

可以看到,在参数类型未知的情况下,会全部当做int来处理。鉴于浮点数硬转整数的奇葩性,且该特性未写入标准,我认为没有人会使用这种写法。

 

当然程老师课上的演示代码中也有缺失类型的函数出现。不得不赞叹程老师的课还是有很多可以挖掘的细节。

最后关于默认参数提升的编译器细节实现,可以移步看某位大牛的博客:

C语言中变长参数中的一个有趣的现象。关于默认参数提升(default argument promotion)

4 条评论

  1. Jack 说道:

    博客写的挺专业的!

  2. shuimx 说道:

    printit(a,b,c)这个函数为什么我用VC6.0不能实现呢?
    报错说不认识 a,b,c。
    一般书上都是写函数一定要定义参数的类型。
    你这里却这样写,还实现了。你是进行过什么设置吗?

    • suika 说道:

      好久没用vc6了= = 刚刚重新装了并进行了试验,的确无法通过编译
      那个时候我记得是做了试验才写的,至少vc9(vs)就编译不了,所以才会列出能通过的编译器

      关于不写参数类型而默认视为int,这个应该没有纳入到规范里,所以既没实用价值也没什么参考价值,只是顺道提一下
      还是谢谢你的指正

Jack 进行回复 取消回复

电子邮件地址不会被公开。

您可以使用这些 HTML 标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>