C语言可变参数

转自http://www.cnblogs.com/wangyonghui/archive/2010/07/12/1776068.html,稍有改动

一、是什么

我们学习C语言时最经常使用printf()函数,但我们很少了解其原型。其实printf()的参数就是可变参数,想想看,我们可以利用它打印出各种类型的数据。下面我们来看看它的原型:

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

它的第一个参数是format,属于固定参数,后面跟的参数的个数和类型都是可变的(用三个点“…”做参数占位符),实际调用时可以有以下的形式:

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

那么它的原型是怎样实现的呢?我今天在看内核代码时碰到了vsprintf,花了大半天时间,终于把它搞的有点明白了。

二、先看两个例子

不必弄懂,先大致了解其用法,继续往下看。

①一个简单的可变参数的C函数

在函数simple_va_fun参数列表中至少有一个整数参数,其后是占位符…表示后面参数的个数不定.。在这个例子里,所有输入参数必须都是整数,函数的功能只是打印所有参数的值。

#include <stdio.h>
#include <stdarg.h>
void simple_va_fun(int start, ...)
{
       va_list arg_ptr;
       int nArgValue =start;
       int nArgCout=0;     //可变参数的数目
       va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
       do
       {
              ++nArgCout;
              printf("the %d th arg: %d\n",nArgCout,nArgValue);     //输出各参数的值
              nArgValue = va_arg(arg_ptr,int);                      //得到下一个可变参数的值
       } while(nArgValue != -1);               
       return;
}
int main(int argc, char* argv[])
{
       simple_va_fun(100,-1);
       simple_va_fun(100,200,-1);
       return 0;
}

②格式化到一个文件流,可用于日志文件

FILE *logfile;
int WriteLog(const char * format, ...)
{
va_list arg_ptr;
va_start(arg_ptr, format);
int nWrittenBytes = vfprintf(logfile, format, arg_ptr);
va_end(arg_ptr);
return nWrittenBytes;
}

稍作解释上面两个例子。

【这部分的引用地址http://www.cppblog.com/lmlf001/archive/2006/04/19/5874.html

从这个函数的实现可以看到,我们使用可变参数应该有以下步骤:

⑴在程序中用到了以下这些宏:

void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list arg_ptr ); 

va在这里是variable-argument(可变参数)的意思.

这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.

⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。

⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数.

⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。

⑸设定结束条件,①是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。②是调用宏va_end。

三、剖析可变参数真相

1.va_*宏定义

我们已经知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的,由于1)硬件平台的

不同 2)编译器的不同,所以定义的宏也有所不同。下面看一下VC++6.0中stdarg.h里的代码

(文件的路径为VC安装目录下的\vc98\include\stdarg.h)

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

再来看看linux中的定义

typedef char *va_list;
#define __va_rounded_size(TYPE) (((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
#define va_start(AP, LASTARG) (AP=((char*)&(LASTARG) + __va_rounded_size (LASTARG))
void va_end (va_list);
#define va_end(AP) (AP= (char *)0)
#define va_arg(AP,TYPE) (AP+=__va_rounded_size(TYPE),*((TYPE *)(AP - __va_rounded_size (TYPE))))

要理解上面这些宏定义的意思,需要首先了解:

①栈的方向②参数的入栈顺序③CPU的对齐方式④内存地址的表达方式。

2.栈——以Intel32位的CPU为分析基础

在IntelCPU中,栈的生长方向是向下的,即栈底在高地址,而栈顶在低地址;从栈底向栈顶看过去,地址是从高地址走向低地址的,因为称它为向下生长,如图。

【图1引用自http://www.yuanma.org/data/2008/0504/article_3027_1.htm,这部分内容,我认为作者讲的很详细,所以引来共享】

从上面压栈前后的两个图可明显看到栈的生长方向,在Intel32位的CPU中,windown或linux都使用了它的保护模式,ss指定栈所有在的段,ebp指向栈基址,esp指向栈顶。显然执行push指令后,esp的值会减4,而pop后,esp值增加4。栈中每个元素存放空间的大小决定push或pop指令后esp值增减和幅度。Intel32位CPU中的栈元素大小为16位或32位,由定义堆栈段时定义。在Window和Linux系统中,内核代码已定义好栈元素的大小为32位,即一个字长(sizeof(int))。因此用户空间程栈元素的大小肯定为32位,这样每个栈元素的地址向4字节对齐。

C语言的函数调用约定对编写可变参数函数是非常重要的,只有清楚了,才更欲心所欲地控制程序。在高级程序设计语言中,函数调用约定有如下几种,stdcall,cdecl,fastcall,thiscal,nakedcall。cdel是C语言中的标准调用约定,如果在定义函数中不指明调用约定(在函数名前加上约定名称即可),那编译器认为是cdel约定,从上面的几种约定来看,只有cdel约定才可以定义可变参数函数。下面是cdel约定的重要特征:如果函数A调用函数B,那么称函数A为调用者(caller),函数B称为被调用者(callee)。caller把向callee传递的参数存放在栈中,并且压栈顺序按参数列表中从右向左的顺序;callee不负责清理栈,而是由caller清理。我们用一个简单的例子来说明问题,并采用Nasm的汇编格式写相应的汇编代码,程序段如下:

void callee(int a, int b)
{
int c = 0;
c = a +b;
}
void caller()
{
callee(1,2);
} 

来分析一下在调用过程发生了什么事情。程序执行点来到caller时,那将要执行调用callee函数,在跳到callee函数前,它先要把传递的参数压到栈上,并按右到左的顺序,即翻译成汇编指令就是push2;push1;

图2

函数栈如图中(a)所示。接着跳到callee函数,即指令callcalle。CPU在执行call时,先把当前的EIP寄存器的值压到栈中,然后把EIP值设为callee(地址),这样,栈的图变为如图2(b)。程序执行点跳到了callee函数的第一条指令。C语言在函数调用时,每个函数占用的栈段称为stackframe。用ebp来记住函数stackframe的起始地址。故在执行callee时,最前的两条指令为:

push ebp
mov ebp, esp

经过这两条语句后,callee函数的stackframe就建好了,栈的最新情况如图2(c)所示。函数callee定义了一个局部变量intc,该变量的储存空间分配在callee函数占用的栈中,大小为4字节(insizeofint)。那么callee会在如下指令:

sub esp, 4
mov [ebp-4], 0

这样栈的情况又发生了变化,最新情况如图2(d)所示。注意esp总是指向栈顶,而ebp作为函数的stackframe基址起到很大的作用。ebp地址向下的空间用于存放局部变量,而它向上的空间存放的是caller传递过来的参数,当然编译器会记住变量c相对ebp的地址偏移量,在这里为-4。跟着执行c=
a + b语句,那么指令代码应该类似于:

mov eax , [ebp +  8] ;这里用eax存放第一个传递进来的参数,记住第一个参数与ebp的偏移量肯定为8
add eax,  [ebp + 12] ;第二个参数与ebp的偏移量为12,故计算eax = a+b
mov [ebp -4], eax  ;执行 c = eax, 即c = a+b

栈又有了新了变化,如图2(e)。至此,函数callee的计算指令执行完毕,但还要做一些事情:释放局部变量占用的栈空间,销除函数的stack-frame过程会生成如下指令:

movesp, ebp;把局部变量占用的空间全部略过,即不再使用,ebp以下的空间全部用于局部变量

popebp;弹出caller函数的stack-frame基址

在IntelCPU里上面两条指令可以用指令leave来代替,功能是一样。这样栈的内容如图2(f)所示。最后,要返回到caller函数,因此callee的最后一条指令是

ret

ret指令用于把栈上的保存的断点弹出到EIP寄存器,新的栈内容如图2(g)所示。函数callee的调用与返回全部结束,跟着下来是执行callcallee的下一条语句。

从caller函数调用callee前,把传递的参数压到栈中,并且按从右到左的顺序;函数返回时,callee并不清理栈,而是由caller清楚传递参数所占用的栈(如上图,函数返回时,1和2还放在栈中,让caller清理)。栈元素的大小为4个字节,每个参数占用栈空间大小为4字节的倍数,并且任何两个参数都不能共用同一个栈元素。

下面是使用gcc  -S 生成的AT&T格式汇编代码

	.file	"test.c"
	.text
	.globl	callee
	.type	callee, @function
callee:
.LFB0:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$16, %esp
	movl	$0, -4(%ebp)
	movl	12(%ebp), %eax
	movl	8(%ebp), %edx
	addl	%edx, %eax
	movl	%eax, -4(%ebp)
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE0:
	.size	callee, .-callee
	.globl	caller
	.type	caller, @function
caller:
.LFB1:
	.cfi_startproc
	pushl	%ebp
	.cfi_def_cfa_offset 8
	.cfi_offset 5, -8
	movl	%esp, %ebp
	.cfi_def_cfa_register 5
	subl	$8, %esp
	movl	$2, 4(%esp)
	movl	$1, (%esp)
	call	callee
	leave
	.cfi_restore 5
	.cfi_def_cfa 4, 4
	ret
	.cfi_endproc
.LFE1:
	.size	caller, .-caller
	.ident	"GCC: (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3"
	.section	.note.GNU-stack,"",@progbits

其中callee中

subl	$16, %esp

可见AT&T是预留一段空间栈

从C语言的函数调用约定可知,参数列表从右向左依次压栈,故可变参数压在栈的地址比最后一个命名参数还大,如下图3所示:

由图3可知,最后一个命名参数a上面都放着可变参数,每个参数占用栈的大小必为4的倍数。因此:可变参数1的地址=参数a的地址+a占用栈的大小,可变参数2的地址=可变参数1的地址+可变参数1占用栈的大小,可变参数3的地址=可变参数2的地址+可变参数2占用栈的大小,依此类推。如何计算每个参数占用栈的大小呢?

3.数据对齐问题

对于两个正整数 x,n总存在整数
q,r使得

x= nq + r, 其中  0<=r <n                 //最小非负剩余

q,r是唯一确定的。q= [x/n], r = x - n[x/n].这个是带余除法的一个简单形式。在c语言中,q,r容易计算出来:
q= x/n, r = x % n.

所谓把 x按n对齐指的是:若r=0,取qn,若r>0,取(q+1)n.这也相当于把x表示为:

x= nq + r‘,其中
-n< r‘ <=0               //最大非正剩余

nq是我们所求。关键是如何用c语言计算它。由于我们能处理标准的带余除法,所以可以把这个式子转换成一个标准的带余除法,然后加以处理:

x+n= qn + (n+r‘),其中0<n+r‘<=n           //最大正剩余

x+n-1= qn + (n+r‘-1),其中
0<=n+r‘-1 <n    //最小非负剩余

所以 qn= [(x+n-1)/n]n.用
c语言计算就是:

((x+n-1)/n)*n

若 n是2的方幂,比如2^m,则除为右移m位,乘为左移m位。所以把x+n-1的最低m个二进制位清0就可以了。得到:

(x+n-1)& (~(n-1))

【来自CSDN博客:http://blog.csdn.net/swell624/archive/2008/11/03/3210779.aspx

根据这些推导,相信已经了解#define__va_rounded_size(TYPE)  (((sizeof (TYPE) + sizeof (int) - 1) /sizeof (int)) * sizeof (int))的涵义。

4.再看va_*宏定义

va_start(va_listap, last)

last为最后一个命名参数,va_start宏使ap记录下第一个可变参数的地址,原理与“可变参数1的地址=参数a的地址+a占用栈的大小”相同。从ap记录的内存地址开始,认为参数的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址+=occupy_stack(type)

va_arg(va_litap, type)

这里是获得可变参数的值,具体工作是:从ap所指向的栈内存中读取类型为type的参数,并让ap根据type的大小记录它的下一个可变参数地址,便于再次使用va_arg宏。从ap记录的内存地址开始,认为存的数据类型为type并把它的值读出来;把ap记录的地址指向下一个参数,即ap记录的地址+=occupy_stack(type)

va_end(va_listap)

用于“释放”ap变量,它与va_start对称使用。在同一个函数内有va_start必须有va_end。

5.可变参数函数问题

考虑了参数大小和数据对齐问题,使得可变参数的类型不但可以是基本类型,同样适用于用户定义类型。值的注意的是,如果是用户定义类型,最好用typedef定义的名字作为类型名,这样就会减少在va_arg进行宏展开时出错的机率。

在可变参数函数中,由va_list变量来记录(或获得)可变参数部分,但是va_list中并没有记录下它们的名字,事实上也是不可能的。要想把可变参数部分传递给下一个函数,唯有通过va_list变量去传递,而原来定义的函数用"..."来表示可变参数部分,而不是用va_list来表示。为了方便程序的标准化,ANSIC在标准库代码中就作出了很好的榜样:在任何形如:type
fun( type arg1, type arg2,...)的函数,都同时定义一个与它功能完全一样的函数,但用va_list类型来替换"...",即

typefun(type arg1, type arg2, va_list ap)。以printf函数为例:
intprintf(const char *format, ...);
intvprintf(const char *format, va_list ap);

第一个函数用"..."表示可变参数,第二个用va_list类型表示可变参数,目的是用于被其它可变参数调用,两者在功能功能上是完全上一样。只是在函数名字相差一个‘"v"字母。

四、可变参数函数的应用

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
/* minprintf: minimal printf with variable argument list */
void minprintf(char *fmt, ...)
{
       va_list ap; /* points to each unnamed arg in turn */
       char *p, *sval;
       int ival;
       double dval;
       va_start(ap, fmt); /* make ap point to 1st unnamed arg */
       for (p = fmt; *p; p++) {
              if (*p != '%') {
                     putchar(*p);
                     continue;
              }
              switch (*++p) {
                 case 'd':
                        ival = va_arg(ap, int);
                        printf("%d", ival);
                        break;
                 case 'x':
                        ival=va_arg(ap,int);
                        printf("%#x",ival);
                        break;
                 case 'f':
                        dval = va_arg(ap, double);
                        printf("%f", dval);
                        break;
                 case 's':
                        for (sval = va_arg(ap, char *); *sval; sval++)
                               putchar(*sval);
                        break;
                 default:
                        putchar(*p);
                        break;
              }
       }
       va_end(ap); /* clean up when done */
}
 
int main(int argc, char* argv[])
{
       int i = 1234;
       int j = 5678;
       char *s="nihao";
       double f=;
 
       minprintf("the first test:i=%d\n",i,j);
       minprintf("the secend test:i=%d; %x;j=%d;",i,0xabcd,j);
       minprintf("the 3rd test:s=%s\n",s);
       minprintf("the 4th test:f=%f\n",f);
       minprintf("the 5th test:s=%s,f=%f\n",s,f);
       system("pause");
       return 0;
}
 
//不使用va_*宏定义的实现:
void minprintf(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;
}

时间: 2024-10-28 16:03:24

C语言可变参数的相关文章

C语言可变参数函数实现原理

一.可变参数函数实现原理 C函数调用的栈结构: 可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈. 本文地址:http://www.cnblogs.com/archimedes/p/variable-parameter.html,转载请注明源地址. 例如,对于函数: void fun(int a, int b, int c) { int d; ... } 其栈结构为 0x1ffc-->d 0x200

转:C语言 可变参数

C语言 可变参数 堆栈一般是怎么压栈处理的 /* * stack space: * *        参数3   |    up *        参数2   | *        参数1   v    down */ 因为参数是按照从右向左依次压入的,所以后面参数的地址依次根据“%”处理即可.文章中start = (int*) &buffer即获取了最后入栈的参数地址(如果最后一个入栈参数为int类型,自己可以推算出倒数第二个参数的地址.) [ 声明:版权所有,欢迎转载,请勿用于商业用途.  

C语言——可变参数

http://blog.chinaunix.net/space.php?uid=25304914&do=blog&id=3066441 一.是什么 我们学习C语言时最经常使用printf()函数,但我们很少了解其原型.其实printf()的参数就是可变参数,想想看,我们可以利用它打印出各种类型的数据.下面我们来看看它的原型: int printf( const char* format, ...); 它的第一个参数是format,属于固定参数,后面跟的参数的个数和类型都是可变的(用三个点&

C语言可变参数在宏定义中的应用

在C语言的标准库中,printf.scanf.sscanf.sprintf.sscanf这些标准库的输入输出函数,参数都是可变的.在调试程序时,我们可能希望定义一个参数可变的输出函数来记录日志,那么用可变参数的宏是一个不错的选择. 在C99中规定宏也可以像函数一样带可变的参数,如: #define LOG(format, ...) fprintf(stdout, format, __VA_ARGS__) 其中,...表示可变参数列表,__VA_ARGS__在预处理中,会被实际的参数集(实参列表)

c语言可变参数函数

c语言支持可变参数函数.这里的可变指,函数的参数个数可变. 其原理是,一般情况下,函数参数传递时,其压栈顺序是从右向左,栈在虚拟内存中的增长方向是从上往下.所以,对于一个函数调用 func(int a, int b, int c); 如果知道了参数a的地址,那么,可以推导出b,c的地址 #include <stdio.h> void test(int a, int b, int c) { printf("%p, %p, %p\n", &a, &b, &

Go语言 可变参数(变参函数)

合适地使用可变参数,可以让代码简单易用,尤其是输入输出类函数,比如日志函数等. 本文主要从4个实例介绍可变参数用法. 程序代码: package main import ( "bytes" "fmt" ) /****************** 1 **************** 定义可参变数函数 形如...type格式的类型只能作为函数的参数类型存在, 并且必须是最后一个参数,它是一个语法糖(syntactic sugar), 即这种语法对语言的功能并没有影响,

c语言 可变参数的使用

主要涉及函数 va_list ap     声明一个指针 va_start(ap,arg)     将ap指向到最后一个确定的参数arg va_arg(ap,size)       将ap储存的地址加size,指向下一个参数 va_end(ap)          将ap赋值为NULL #include<stdarg.h> #include<stdio.h> int sum(int, ...); int main(void) { printf("Sum of 10, 20

C语言可变参数函数详解示例

先看代码 printf("hello,world!");其参数个数为1个. printf("a=%d,b=%s,c=%c",a,b,c);其参数个数为4个. 如何编写可变参数函数呢?我们首先来看看printf函数原型是如何定义的.在linux下,输入man 3 printf,可以看到prinf函数原型如下: SYNOPSIS #include <stdio.h> int printf(const char *format, ...); 后面的三个点...

c语言可变参数列表处理

函数参数的传递原理 函数参数在内存中是以栈的形式存取,从右至左入栈. 参数在内存中存放格式: 在进程中,堆栈地址是从高到低分配的.当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减. 总之,函数在堆栈中的分布情况是,地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段.堆栈中,各个函数的分布情况是倒叙的.即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分.参数在堆栈中的 分布情况如