C语言函数可变参数详解

stdarg.h解析

ranpanf

stdarg.hANSI C 的标准头文件。它对可变参数的函数(vararg function)提供了支持。什么是可变参数的函数呢?举个例子:printf scanf就是。

stdarg.hvararg function支持的关键是定义了几个非常有用的宏。注:下面说道的程序运行栈不是很精确,应该是进程的运行栈或进程的执行栈。

1.typedef char *va_list;

注解:本质是一个指向程序运行栈中某一个地址的指针(以后提到的栈都为程序运行栈)

2.#define  __va_size(type) /

(((sizeof(type) + sizeof(long) - 1) / sizeof(long)) */ sizeof(long))

注解:计算某种数据类型的参数在栈中占有的空间。/是连接宏定义中的两行:如

                        #define sum(a,b)((a)+(b))

           等效于:

                        #define sum(a,b)((/

                        a)+(b))

                  

IA32(32位机器程序或汇编程序)的程序中,PUSHPOP指令的操作数是4Bytes(DWORD)。如:char变量为一个字节,但入栈时需要一个DWORD。一个

sizeof5Bytes的结构变量需要2DWORD

3#define     va_start(ap, last) /

((ap) = (va_list)&(last) + __va_size(last))

 

注解:让直指指向第一个可变参数的首地址。

4.  #define    va_arg(ap, type) /

        (*(type *)((ap) += __va_size(type), (ap) -/ __va_size(type)))

注解:这是我见过用逗号操作数最巧妙地例子。

5.  #define    va_end(ap)   ((void)0)

注解:出现一个空值 标准库里将指向可变参数的指针重置为NULL

 

为了详细解析,我先说一下几个要点。

1.     函数参数的传递机制:这里不是指值传递和引用传递。而是以从汇编(机器)语言程序员看参数传递。

我们知道有三种传递方式:寄存器传值,存储器传值,堆栈传值。C支持那种参数传值方式呢?C程序员怎么选择函数参数传递方式呢?在C语言的标准引入了一个概念:调用规则(calling convention),调用规则有:_cdecl,_stdcall,_fastcall,_thiscall(仅C++支持)等,请大家参阅MSDN获得详情。_cdecl 默认C/C++的调用规则,_stdcall win32API的调用规则,_fastcall一般不用,_thiscall C++的类成员函数的调用规则。详细情况见下表:

1:函数调用规则详解

调用规则

清栈

是否支持vararg function

参数传递

_cdecl

调用者(caller)

支持

函数参数从右向左入栈

_stdcall

被调用者(callee)

函数参数从右向左入栈

_thiscall

被调用者(callee)

函数参数从右向左入栈,最后压入默认参数this

_fastcall

被调用者(callee)

前两个参数存入寄存器ECXEDX,剩余参数从右向左入栈

 

 

函数的调用过程:用户函数的调用是由程序运行栈来管理的,

标准库函数和系统调用一般也会用到栈,还有共享代码段(dll),陷入(软中断)等概念,我就不详细说了。现在只要知道用户函数是由程序运行栈管理的即可,栈的功能体现在三点:1.用栈保存函数的返回地址,2.用栈传递函数参数,3.在栈中创建局部变量。这里有个概念叫栈帧,它是指函数在调用时占用的栈中一部分连续的空间。函数的嵌套反映在栈中就是调用函数的栈帧的地址高于被调用者的栈帧的地址并顺序存放(假设栈是从高向低增长的)。函数的返回会伴随着他的栈帧的销毁,这也是为什么局部变量会在作用域之外没法应用。上面的表中有一列为“清栈”,想必你读到此处,它的含义你明白了:调用者清栈,是指被调用的函数参数保存在它调用者的栈帧中;而被调用者清栈,是指被调用的函数参数保存在它自己的栈帧中,通常用RET n指令返回。

 

现在我解析一段小程序:

1.       #include <stdio.h>

2.       #include <stdarg.h>

3.       int  average( int first, ... )

4.       {

5.       int count = 0, sum = 0, i = first;

6.       va_list marker;

 

7.       va_start( marker, first );     /* Initialize variable arguments. */

8.       while( i != -1 )

9.       {

10.  count++;

11.  sum += i;

12.  i = va_arg( marker, int);

 

 

13.  }

14.  va_end( marker );              /* Reset variable arguments.     

15.  return( sum ? (sum / count) : 0 );

 

16.  }

 

17.  int main(){

 

18.  int a=20

19.  printf("Average(%d,%d)=%d/n",a,400,average(a,400,-1));

20.  return printf("Average(%d,%d,%d)=%d/n",100,20,400,average(100,20,400,-1));

21.  }

 

 

执行结果:

 

 

 

 

 

 

 

其中的average函数就是vararg function

当执行到18行是:程序运行栈状态如图1所示:

                                 

 

 

 

 

 

 

 

地址(address)

内容(value)

0xFFFF 0000

00

0xFFFE FFFF

00

0xFFFE FFFE

00

0xFFFE FFFD

         03

 

 

 

 

 

 

 

 

 

栈底

main()的栈帧

ESP

当前main栈帧中保存了局部变量a的值(小端编码)3

1.执行到18行时的栈状态

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

当执行到19行时,程序运行栈的状态如图2.所示:

 

 

2.执行到19行是的栈状态

地址(address)

内容(value)

0xFFFF 0000

00

0xFFFE FFFF

00

0xFFFE FFFE

00

0xFFFF FFFD

14

0xFFFF FFFC

FF

0xFFFF FFFB

FF

0xFFFF FFFA

FF

0xFFFF FFF9

FF

0xFFFF FFF8

00

0xFFFF FFF7

00

0xFFFF FFF6

01

0xFFFF FFF5

90

0xFFFF FFF3

00

0xFFFF FFF2

00

0xFFFF FFF2

00

0xFFFF FFF1

02

... ...

... ...

0xFFFF FFED

... ...

... ...

... ...

0xFFFF FFE9

00

... ...

... ...

0xFFFF FFE5

00

0xFFFF FFE4

00

0xFFFF FFE3

00

0xFFFF FFE2

00

0xFFFF FFE1

14

 

 

栈底

变量a(20)

average的参数-1

average的参数400

 

average的参数a20

 

average返回地址

局部变量count(0)

局部变量sum(0)

局部变量i(20)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0xFFFF 00000xFFFE FFF1 main函数的调用栈帧。在main函数的

栈帧中,被调用函数average的参数从右到左入栈(0xFFFE FFFC 0xFFFE FFF1).

既然被掉函数的参数位于调用函数的栈帧中,因此有main函数来清栈。参数从右向左依次入栈,由着两点可知:调用规则是_cdecl.

6步“va_list marker;”:定义了一个指向一个字节的指针maker

7步“va_start(maker,first);” :使得指针maker指向第一个可变参数(400,即栈中的0xFFFE FFF5存储单元)这是怎么做到的呢?其实很简单!只要知道紧接着average第一个变参数之前的非可变参数(也就是最后一个非可变参数)在栈中的地址,再加上减去该参数在栈中的存储大小就可以获得第一可变个参数的首地址.在该列中average的最后一个非可变参数a的地址为0xFFFE FFF1,a在栈中的大小为4Bytes,显然0xFFFE FFF1+4=0xFFFE FFF5.第一个非可变参数的地址可由取地址运算符&取得,在它在栈中的大小可使用sizeof取得。试看两宏:

                 #define     __va_size(type) /

(((sizeof(type) + sizeof(long) - 1) / sizeof(long)) */ sizeof(long))

该宏计算type在栈中的存储大小, 因为可以用sizeof得到type的存储大小,所以type可以是变量也可以是类型。请注意数据类型的存储大小与在栈中的存储大小的区别,如char 的存储大小为Byte ,而它的栈中存储大小为4Bytes。这也是为什么不直接用sizeof的原因。

#define     va_start(ap, last) /

((ap) = (va_list)&(last) + __va_size(last))

该宏让ap指针指向函数的第一个可变参数的首地址。&(last)为函数的最后一个非可变参数的地址,__va_size(last)是它在栈中的存储大小,两者相加便得第一个可变参数的首地址。

12步“i = va_arg( marker, int);”:取函数当前可变参数,并且让指针指向下一个

 

可变参数,该宏的定义如下:

                 #define     va_arg(ap, type) /

                 (*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))

                 这里用到了逗号运算符“,”。逗号运算符的定义是:expr1expr2

                 先计算expr1,在计算expr2,整个表达式的结果为expr2的值。

                 显然逗号运算符的左侧操作数:(ap) += __va_size(type)是将修改指针移向下一个可变参数,但不会作为整个表达式的结果;而右侧操作数(ap) - __va_size(type)的不会修该指针的值,但会将恢复到指针被修改之前结果并将其作为整个表达式的结果。该结果为char型的变量的地址值,经过强制类型转化和取指针指向的值,终于得到了type类型的可变参数。

 

几点需要注意

1.     有可变参数的函数必须拥有非可变参数,并且非可变参数必须位于可变参数的前面。

2.     程序员必须自己控制可变参数的类型以及可变参数的数目。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值