c 语言可变参讲解(很精细)

本文转自:http://blog.csdn.net/smstong/article/details/50751121

1 C语言中函数调用的原理

函数是大多数编程语言都实现的编程要素,调用函数的实现原理就是:执行跳转+参数传递。对于执行跳转,所有的CPU都直接提供跳转指令;对于参数传递,CPU会提供多种方式,最常见的方式就是利用栈来传递参数。C语言标准实现了函数调用,但是却没有限定实现细节,不同的C编译器厂商可以根据底层硬件环境自行确定实现方式。

函数调用的一般实现原理,请参考我的博文 C语言中利用setjmp和longjmp做异常处理中的第一段。

2 可变参实现思路

2.1 如何取得后续实参地址

我们以X86架构上的VC++编译器为例进行举例说明。例子代码如下。

void f(int x, int y, int z)
{
    printf("%p, %p, %p\n", &x, &y, &z);
}
int main()
{
    f(100, 200, 300);
    return 0;
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可能的执行结果:

00FFF674, 00FFF678, 00FFF67C
 
 
  • 1

VC++中函数的参数是通过堆栈传递的,参数按照从右向左的顺序入栈。调用f时参数在堆栈中的情况如下图所示: 
这里写图片描述

可见,我们只要知道x的地址,就可以推算出y,z的地址,从而通过其地址取得参数y,z的值,而不用其参数名称取值。如下代码所示。

void f(int x, int y, int z)
{
    char* px = (char*)&x;
    char *py = px + sizeof(x);
    char *pz = py + sizeof(int);

    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
}
int main()
{
    f(100, 200, 300);
    return 0;
}

 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

可见根据函数的第一个参数,以及后续参数的类型,就可以根据偏移量计算出后续参数的地址,从而取得后续参数值。 
于是可以把上述代码改写成可变参数的形式。

void f(int x, ...)
{
    char* px = (char*)&x;
    char *py = px + sizeof(x);
    char *pz = py + sizeof(int);

    printf("x=%d, y=%d, z=%d\n", x, *(int*)py, *(int*)pz);
}
int main()
{
    f(100, 200, 300);
    return 0;
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

2.2 如何标识后续参数个数和类型

虽然写成了可变参形式,但是函数如何判断后续实参的个数和类型呢?这就需要在固定参数中携带这些信息,如printf(char*, …)使用的格式化字符串方法,通过第一个参数来携带后续参数个数以及类型的信息。我们实现一个简单点的,只能识别%s,%d,%f三种标志。

void f(char* fmt, ...)
{
    char* p0 = (char*)&fmt;
    char* ap = p0 + sizeof(fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("参数类型为int,值为 %d\n", *((int*)ap));
            ap += sizeof(int);
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("参数类型为double,值为 %f\n", *((double*)ap));
            ap += sizeof(double);
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("参数类型为char*,值为 %s\n", *((char**)ap));
            ap += sizeof(char*);
        }
        p++;
    }

}
int main()
{
    f("%d,%f,%s", 100, 1.23, "hello world");
    return 0;
}
 
 
  • 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

输出:

参数类型为int,值为 100
参数类型为double,值为 1.230000
参数类型为char*,值为 hello world
 
 
  • 1
  • 2
  • 3

为简化分析参数代码,定义一些宏来简化,如下。

#define va_list char*   /* 可变参数地址 */
#define va_start(ap, x) ap=(char*)&x+sizeof(x) /* 初始化指针指向第一个可变参数 */
#define va_arg(ap, t)   (ap+=sizeof(t),*((t*)(ap-sizeof(t)))) /* 取得参数值,同时移动指针指向后续参数 */
#define va_end(ap)  ap=0 /* 结束参数处理 */

void f(char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("参数类型为int,值为 %d\n", va_arg(ap, int));
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("参数类型为double,值为 %f\n", va_arg(ap, double));
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("参数类型为char*,值为 %s\n", va_arg(ap, char*));
        }
        p++;
    }
    va_end(ap);
}
int main()
{
    f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
    return 0;
}

 
 
  • 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

3 正确的变参函数实现方法

上面的例子中,我们没有使用任何库函数就轻松实现了可变参数函数。别高兴太早,上述代码在X86平台的VC++编译器下可以顺利编译、正确执行。但是在gcc编译后,运行却是错误的。可见GCC对于可变参数的实参传递实现与VC++并不相同。

gcc下编译运行:
[smstong@cf-19 ~]$ ./a.out
参数类型为int,值为 0
参数类型为double,值为 0.000000
Segmentation fault
 
 
  • 1
  • 2
  • 3
  • 4
  • 5

可见,上述代码是不可移植的。为了在使得可变参函数能够跨平台、跨编译器正确执行,必须使用C标准头文件stdarg.h中定义的宏,而不是我们自己定义的。(这些宏的名字和作用与我们自己定义的宏完全相同,这绝不是巧合!)每个不同的C编译器所附带的stdarg.h文件中对这些宏的定义都不相同。再次重申一下这几个宏的使用范式:

va_list ap;
va_start(ap, 固定参数名); /* 根据最后一个固定参数初始化 */
可变参数1类型 x1 = va_arg(ap, 可变参数类型1); /* 根据参数类型,取得第一个可变参数值 */
可变参数2类型 x2 = va_arg(ap, 可变参数类型2); /* 根据参数类型,取得第二个可变参数值 */
...
va_end(ap);     /* 结束 */
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

这次,把我们自己的宏定义去掉,换成#include

#include <stdio.h>
#include <stdarg.h>
void f(char* fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);

    char* p = fmt;
    while (*p) {
        if (*p == '%' && *(p+1) == 'd') {
            printf("参数类型为int,值为 %d\n", va_arg(ap, int));
        }
        else if (*p == '%' && *(p+1) == 'f') {
            printf("参数类型为double,值为 %f\n", va_arg(ap, double));
        }
        else if (*p == '%' && *(p+1) == 's') {
            printf("参数类型为char*,值为 %s\n", va_arg(ap, char*));
        }
        p++;
    }
    va_end(ap);

}
int main()
{
    f("%d,%f,%s,%d", 100, 1.23, "hello world", 200);
    return 0;
}

 
 
  • 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

代码在VC++和GCC下均可以正确执行了。

4 几个需要注意的问题

4.1 va_end(ap); 必须不能省略

也许在有些编译器环境中,va_end(ap);确实没有什么作用,但是在其他编译器中却可能涉及到内存的回收,切不可省略。

4.2 可变参数的默认类型提升

《C语言程序设计》中提到:

在没有函数原型的情况下,char与short类型都将被转换为int类型,float类型将被转换为double类型。实际上,用...标识的可变参数总是会执行这种类型提升。
 
 
  • 1

引用《C陷阱与缺陷》里的话:

**va_arg宏的第2个参数不能被指定为charshort或者float类型**。
因为charshort类型的参数会被转换为int类型,而float类型的参数会被转换为double类型 ……
例如,这样写肯定是不对的:
c = va_arg(ap,char);
因为我们无法传递一个char类型参数,如果传递了,它将会被自动转化为int类型。上面的式子应该写成:
c = va_arg(ap,int);
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

4.3 编译器无法进行参数类型检查

对于可变参数,编译器无法进行任何检查,只能靠调用者的自觉来保证正确。

4.4 可变参数函数必须提供一个或更多的固定参数

可变参数必须靠固定参数来定位,所以函数中至少需要提供固定参数,f(固定参数,…)。 
当然,也可以提供更多的固定参数,如f(固定参数1,固定参数2,…)。注意的是,当提供2个或以上固定参数时,va_start(ap, x)宏中的x必须是最后一个固定参数的名字(也就是紧邻可变参数的那个固定参数)。

5 C的可变参函数与C++的重载函数

C++的函数重载特性,允许重复使用相同的名称来定义函数,只要同名函数的参数(类型或数量)不同。例如,

void f(int x);
void f(int x, double d);
void f(char* s);
 
 
  • 1
  • 2
  • 3

虽然源代码中函数名字相同,其实编译器处理后生成的是三个具有不同函数名的函数(名字改编name mangling)。虽然在使用上有些类似之处,但这显然与C的可变参数函数完全不是一个概念。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值