转:C语言可变参数函数的原理

来源:互联网 发布:mysql创建数据库语句 编辑:程序博客网 时间:2024/05/16 16:05

原文地址:http://www.cppblog.com/ichenq/archive/2009/06/21/88245.html

 

一,开始

当我们的程序调用函数的时候,系统会首先将函数的参数按照从右自左的顺序压入函数的栈中。如果底层的C语言实现让函数参数在内存中连续存储,那么我们只需要知道当前参数的地址,就可以依次访问参数列表中的其他参数。

这里为了举例方便,假设传递的3个都是int类型的参数。代码:

//fun: 打印n后面参数的值
void fun(int n, ...);

int main()
{
    int par1 = 128;
    int par2 = 256;
    int par3 = 512;
    fun(par1, par2, par3 );
    return 0;
}

void fun(int par1, ...)
{
    int* p = &par1;             //获取par1的地址   
    printf("%d /n", *++p);      //打印par2的值
    printf("%d /n", *++p);      //打印par3的值
}


C默认的函数调用规范是__cdecl,也就是所有参数从右到左依次入栈,严格的fun声明应该是:

void __cdecl fun(int n, ...);


由于入栈顺序是从右向左,所以main给传递的par1,par2和par3的入栈顺序是:
push    par3
push    par2
push    par1

当然汇编代码(wintel)更可能是:
mov         eax,dword ptr [par3]
push        eax
mov         ecx,dword ptr [par2]
push        ecx
mov         edx,dword ptr [par1]
push        edx

最后再call fun,执行函数体的代码。

先压栈的参数会放在高地址,因为栈是由上往下生长的,所以par1,par2,par3在内存中的顺序将会是:
0xFE6C        par3
0xFE70         par2
0xFE74         par1

地址是假设,但距离应该是sizeof(int)的值(4个字节)。




另外,标准库里的printf一族的库函数(sprintf, fprintf)那样接受可变参数个数的函数也只有用cdecl才能够实现。假设有下面一行语句:

printf("%d %d %d /n", m, n, k);

可以看到它是通过把参数的个数和类型保存在第一个参数来实现正确寻址的。格式符%d指定了要读取的类型,而格式符的数目指定了传递参数的个数。这就是为什么printf("%d %d /n", m, n, k)可以成功执行,而printf("%d %d %d /n", m, n)会失败的原因。因为被调用函数无需要求调用者传递多少参数,调用者传递过多或者不同的参数不会产生编译阶段的错误。



二,原理

我们通常会使用C语言的varargs宏来编写支持可变参数列表的函数,在ANSI C标准里,这些宏包含在<stdarg.h>头文件里。

下面再分析一下varargs宏,增加varargs宏后的代码:

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

void __cdecl fun(int n, ...);

int main()
{
    int par1 = 128;
    int par2 = 256;
    int par3 = 512;
    fun(par1, par2, par3 );
    return 0;
}

void fun(int n, ...)
{
    va_list ap;
    va_start(ap, n);
    printf("%d /n", va_arg(ap, int));
    printf("%d /n", va_arg(ap, int));
    va_end(ap);
}

在Microsoft为VC提供的实现中,可以看到这样的定义:

#define _ADDRESSOF(v)   ( &(v) )
#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
typedef char* va_list;
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )

va_list        一个char型指针,每次单个字节寻址。
va_start    通过_INTSIZEOF计算类型的大小,并让ap获得v后面参数对象的地址       
va_arg       ap指向参数列表中ap下一个参数对象,并返回ap之前指向的t类型参数对象


将va_list等宏还原后会更明白一点:

void fun(int par1, ...)
{
    int par2, par3;
    char*   ap;                         //va_list ap;
    ap = (char*)&par1 + 4;      //va_start(ap, par1);
    par2 = *(int*)(ap+=4 - 4); //m = va_arg(ap, int);
    par3 = *(int*)(ap+=4 - 4); //m = va_arg(ap, int);
    ap = (char*)0;                   //va_end(ap);
}

根据这个实现,va_arg的第二个参数若为char, short则会被转换为int类型,若为float则会转换为doule类型,这是因为_INTSIZEOF宏的返回值(4或8)的原因,毕竟宏用C来做泛型还是不如C++的模板来的好。



三,实践

根据标准库提供的va_list我们可以实现自己的能接受可变参数的列表的函数,下面以Win32API中的MessageBox作为试验。
在windows程序设计中有时候也需要输出一些调试信息,但是这个时候如果根据在控制台下的编程习惯使用printf()函数来实现将信息输出到窗口的话是比较麻烦的,这样可以选择一个最简单的Windows窗口函数MessageBox。但是MessageBox()的参数类型是字符串,所以我们需要对它做一些格式化。这就需要用到标准库的vsprintf函数(详细)。

需要包括的头文件:
#include <stdio.h>           // vsprintf()
#include <stdarg.h>        // va_list, va_start(), va_end()
#include <windows.h>     // MessageBoxA()
s
代码如下:

void my_messagebox(const char* format, ...)
{
    char        buffer[MAX_PATH] = {0};//260个字节的缓冲
    va_list     ap;   
    va_start(ap, format);
    vsprintf(buffer, format, ap);
    va_end(ap);
    ::MessageBoxA(NULL, buffer, "", MB_OK);//ANSCII版本的MessageBox()
}

 

我的疑问:#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )为什么不直接用sizeof(n)?

这个宏的作用不就是计算类型的大小吗?

 

 

原创粉丝点击