C语言中的可变参数

1. 简介

在C语言中可以使用printf进行格式化输出,函数声明如下:

int __cdecl printf(const char * _Format, ...);

其中第一个参数format代表需要格式化的字符串,第二个参数...代表任意个参数的集合,在C语言中叫做可变参数,使用它声明的函数可以在调用该函数的时候传入任意多的参数。

具体是如何实现的?

2. 实现原理

假如现在要实现printf函数,首先要定义该函数的实现。

int __cdecl custom_printf(const char * _format, ...)
{
    //具体代码逻辑
}

这里我们不关心该函数逻辑是如何实现的,只关心在函数内部是如何获取通过...传入的参数。

函数代码本质上就是一段机器指令,为了了解本质,可以使用vs2008调试界面的汇编功能进行分析:

int main()
{
    custom_printf("test", 1, 3, 4, 5);
    return 0;
}

跳转到反汇编进行查看:

C语言中的可变参数_第1张图片
main.png

在执行函数调用语句的时候,会先将函数的参数进行压栈(push),接着执行函数内部的逻辑代码(call)

注意压栈顺序,参数从右向左依次入栈,这是由__cdecl决定的。

接着查看一下函数的汇编代码:

C语言中的可变参数_第2张图片
custom_printf.png

目前custom_printf内部没有任何代码,生成的汇编代码和main函数的前面完全一致。当然其实在这里我们无需知道代码本身的含义,只需要知道如何拿到函数调用参数的值即可。

想要拿到函数参数的值是非常简单的,因为在执行call之前,参数已经被保存到栈,现在只需要到相应的位置拿即可,而_format参数的地址正是我们要找的参数位置的最后面。

可能就是为什么可变参数之前要有固定参数的原因吧……

当前栈中的参数布局如下:

+-------------------+
|    5   address    |  高
+-------------------+
|    4   address    |
+-------------------+
|    3   address    |
+-------------------+
|    1   address    |  低
+-------------------+
|   test address    | <----- 已知条件
+-------------------+

因为x86机器的栈是由高到低增长,所以test在最下面。

如果想要获取format以外的其他参数,结果很简单,只要将指针按照参数类型增加。案例代码实现如下:

int __cdecl custom_printf(const char * _format, ...)
{
    int i = 0;
    int param[4] = {0};
    int *p = &_format;

    p += 1; //跳过字符串指针

    for(i = 0; i<4; i++) {
        param[i] = *(p + i);
    }
}

param数组内部就是传入的参数。

3. 验证

查看C语言内部提供的操作可变参数的函数,发现其实原理就是如此。

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

#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap)      ( ap = (va_list)0 )

你可能感兴趣的:(C语言中的可变参数)