[转] C中的可变参数研究

初学C,感觉可变参数挺好玩的,转载一发做个mark。本想注明来源,发现来源也是转载的,找不到根源,就不贴链接了。

一. 何谓可变参数

int printf(const char* format, ...);

这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示).   而我们又可以用各种方式来调用printf,如:

printf("%d",value);
printf("%s",str);
printf("the number is %d,string is: %s", value, str);

二. 实现原理

C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:

typedef char *va_list;
/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/
#define va_start(ap,v)(ap = (va_list)&v + _INTSIZEOF(v))
/*va_start的定义为 &v+_INTSIZEOF(v), 这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址*/
#define va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
/*这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
#define va_end(ap) (ap = (va_list)0)
/*x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的.在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型.*/

以下再用图来表示(这什么破图):

在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。

|——————————————————————————|  
|最后一个可变参数   |   ->高内存地址处  
|——————————————————————————|  
……………….  
|——————————————————————————|  
|第N个可变参数   |   ->va\_arg(arg\_ptr,int)后arg_ptr所指的地方,  
|   |   即第N个可变参数的地址。  
|———————————————   |  
………………………….  
|——————————————————————————|  
|第一个可变参数   |   ->va\_start(arg\_ptr,start)后arg_ptr所指的地方  
|   |   即第一个可变参数的地址  
|———————————————   |  
|————————————————————————   ——|  
|   |  
|最后一个固定参数   |   ->   start的起始地址  
|——————————————   —|   ……………..  
|——————————————————————————   |  
|   |  
|———————————————   |->   低内存地址处

三. printf研究

下面是一个简单的printf函数的实现。(作者简直是幽默,所谓printf函数的实现里面居然用到了printf函数。。。)

#include "stdio.h"
#include "stdlib.h"
void myprintf(char* fmt, ...) //一个简单的类似于printf的实现,参数必须都是int类型
{
char* pArg=NULL;   //等价于原来的va_list
char c;

pArg = (char*)&fmt;   //注意不要写成p = fmt, 因为这里要对参数取址,而不是取值
pArg += sizeof(fmt);   //等价于原来的va_start

do
{
c =*fmt;
if (c != '%')
{
putchar(c);   //照原样输出字符
}
else
{
//按格式字符输出数据
switch(*++fmt)
{
case 'd':
printf("%d", *((int*)pArg));
break;
case 'x':
printf("%#x", *((int*)pArg));
break;
default:
break;
}
pArg += sizeof(int);   //等价于原来的va_arg
}
++fmt;
}while(*fmt != '/0');
pArg = NULL;   //等价于va_end
return;
}
int main(int argc, char* argv[])
{
int i = 1234;
int j = 5678;

myprintf("the first test:i=%d",i,j);
myprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);
system("pause");
return 0;
}

在intel+win2k+vc6的机器执行结果如下:

the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;

四. 应用

求最大值(原文代码有问题,重写了一个):

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

int max(int n, ...);

int main()
{
        printf("%d", max(4, 1, 2, 3, 4));
        return 0;
}

int max(int n, ...)    // n记录可变参数的数量
{  
        va_list ap;
        va_start(ap, n);    // 将ap指向n的后一个参数

        int i, tmp, max_num=va_arg(ap, int);
        for(i=1; i<n; i++)   // 计算最大值
        {   
                if(max_num < (tmp=va_arg(ap, int))) 
                        max_num = tmp;
        }

        va_end(ap);    // 清除ap
        return max_num;
}
Creative Commons License