C语言的函数重载与可变参数函数--variable argument in function

来源:互联网 发布:世界黑客编程大赛题目 编辑:程序博客网 时间:2024/06/05 01:11

    最近在写基于C语言的TRDP相关的程序,由于要对UDP数据进行封包操作难免需要用到可变参数。

    读懂和使用可变参数需要明确几个关键点。

    Function(char A, char B, char C, ...)

    { 

    va_list ap;
    int ret;

    va_start(ap, C);
    ret = subfunction(protocol, command, handle, ap);
    va_end(ap);

     }


subfunction(char protocol,char  command, char handle, ap)

{

    int arg;

    while ((arg = va_arg(ap, int)) != RPC_END)

    {

      arg_array[arg_size++] = (int)arg;

     }

}


典型代码如上图所示,需要读懂几个关键名词。

...     :参数占位符。 使用此表示显然表示要使用可变参数

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


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宏初始化va_list类型变量中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,也就是最后一个固定参数。

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


⑸设定结束条件: 程序员必须自己在代码中指明结束条件。一般选用特殊值或者变量值。

宏实现:
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 )

代码的含义:

1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的

2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。

3、va_start的定义为&v+_INTSIZEOF(v),而&v是最后一个固定参数的起始地址,再加上其大小后,就得到了第一个 可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。

注意:
⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
(2)在VC等绝大多数C编译器中,参数进栈的顺序是由右向左的,因此,
参数进栈以后的内存模型如下图所示:最后一个固定参数的地址正好位于第一个可变参数之下,并且是连续存储的。
|—— —————————————|
| 最后一个固定参数 | ->高内存地址处
|— ——————————————|
........................
|-------------------------------|
| 第N个可变参数 | ->va_arg(arg_ptr,datatype)后arg_ptr所指的地方
|-------------------------------|
...................
|——— ————————————|
| 第一个可变参数 | ->va_start(arg_ptr,start)后arg_ptr所指的地方
| | 即第一个可变参数的地址
|——————————————— |
|————————————— ——|
| |
| 最后一个固定参数 | -> start的起始地址
|—————————————— —|
...............
|——————————————- |
| |
|——————————————— | -> 低内存地址处

(4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
因此,现在再来看va_arg()的实现就显而易见了,读取数值,并将指针移到下一个参数:
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
这个宏做了两个事情,

①用用户输入的类型对参数地址进行强制类型转换,得到用户所需要的值

②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。

(5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不 会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所 以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

四、可变参数在编程中要注意的问题
因为va_start, va_arg, va_end等定义成宏, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型.

五、小结:
1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。

2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
⑴在固定参数中设标志—— printf函数就是用这个办法。后面也有例子。

⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法——当可变参数的值为-1时,即认为得到参数列表的结尾。

无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。这是一个不太方便

3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
①函数栈的生长方向
②参数的入栈顺序
③CPU的对齐方式
④内存地址的表达方式
结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的 实现,最后va_end的存在则是良好编程风格的体现—将不再使用的指针设为NULL,这样可以防止以后的误操作。

4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。


0 0
原创粉丝点击