您的位置:首页 > 编程语言 > C语言/C++

C语言的变参技术,va_start,va_arg,va_end这几个函数怎么用?

2013-04-08 22:59 337 查看
一、printf函数的实现原理
在C/C++中,对函数参数的扫描是从后向前的。C/C++的函数参数是通过压入堆栈的方式来给函数传参数的(堆栈是一种先进后出的数据结构),最先压入的参数最后出来,在计算机的内存中,数据有2块,一块是堆,一块是栈(函数参数及局部变量在这里),而栈是从内存的高地址向低地址生长的,控制生长的就是堆栈指针了,最先压入的参数是在最上面,就是说在所有参数的最后面,最后压入的参数在最下面,结构上看起来是第一个,所以最后压入的参数总是能够被函数找到,因为它就在堆栈指针的上方。printf的第一个被找到的参数就是那个字符指针,就是被双引号括起来的那一部分,函数通过判断字符串里控制参数的个数来判断参数个数及数据类型,通过这些就可算出数据需要的堆栈指针的偏移量了,下面给出printf("%d,%d",a,b);(其中a、b都是int型的)的汇编代码.section
.data
string out = "%d,%d"
push b
push a
push $out
call printf
你会看到,参数是最后的先压入栈中,最先的后压入栈中,参数控制的那个字符串常量是最后被压入的,所以这个常量总是能被找到的。
二、栈的内存结构:
栈内分配空间是从高地址向低地址分配
int a = 4;
int b = 0;
printf("%#x\n",&a);
printf("%#x\n",&b);
变量a 先压入栈中,a的地址-
&a    0x002df8ec {4}    int *
&b    0x002df8e0 {0}    int *
可以看到a的地址高于b的地址;
内存中的机构:
0x002DF8D9  cc cc cc cc cc cc cc 00 00 00 00 cc cc cc cc cc cc cc cc
0x002DF8EC  04 00 00 00
x86的机器小端:高高低低 所以 00 00 00 04 为a。

趣味:
你有储蓄账号和支票账号么?如果有,你自己登记每一笔操作么?怎样登记。我发现美国人印的这个登记本子太笨了。我的登记办法是支票账号的操作从前往后登记,储蓄账号的操作从后往前登记。当两个账号的记录在中间相遇时,登记本就用完了,不浪费一点空间。我真是个计算机天才儿童啊。看到什么叫举一反三了吧。岳博士,你也学过计算机,但你能想出这个登记办法么。其实在计算机里就是这样的,程序从低地址往高地址增长,而栈从高地址往低地址增长。当他们在中间相遇,计算机的逻辑空间就耗尽了。嘿嘿。人不傻当不成右派。

三、堆的内存结构:

char* pchTest = new char[10];
for (int i = 0; i< 10; i++)
{
pchTest[i] = 'a'+i;
}

0x004771F8  61 62 63 64 65 66 67 68 69 6a fd fd fd fd ab ab ab ab ab
0x0047720B  ab ab ab ee fe 00 00 00 00 00 00 00 00 70 89 35 66 4e 36

堆是从低地址往到地址生长

三、原理分析

#include <stdarg.h>    // 必须包含的头文件

int Add(int start,...)   // ...是作为占位符
{
va_list arg_ptr;    // 定义变参起始指针
int sum=0;           // 定义变参的和
int nArgValue =start;     //

va_start(arg_ptr,start); // arg_ptr指向第一个变参
do
{
sum+=nArgValue;                         // 求和
nArgValue = va_arg(arg_ptr,int); // arg_ptr指向下一个变参
}
while(nArgValue != 0);                      // 判断结束条件;结束条件是自定义为=0时结束

va_end(arg_ptr);   // 复位指针
return sum;
}

函数的调用方法为Add(1,2,3,0);这样,必须以0结尾,因为变参函数结束的判断条件就是读到0停止。

解释:

所使用到的宏:
void va_start( va_list arg_ptr, prev_param );
type va_arg( va_list arg_ptr, type );
void va_end( va_list 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,int)后arg_ptr所指的地方,
|                                |      即第N个可变参数的地址。
|--------------- |
|--------------------------|
|   第一个可变参数                |      ->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的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: