函数参数的传递原理
函数参数在内存中是以栈的形式存取,从右至左入栈。
参数在内存中存放格式:
在进程中,堆栈地址是从高到低分配的。当执行一个函数的时候,将参数列表入栈,压入堆栈的高地址部分,然后入栈函数的返回地址,接着入栈函数的执行代码,这个入栈过程,堆栈地址不断递减。
总之,函数在堆栈中的分布情况是,地址从高到低,依次是:函数参数列表,函数返回地址,函数执行代码段。堆栈中,各个函数的分布情况是倒叙的。即最后一个参数在列表中地址最高部分,第一个参数在列表地址的最低部分。参数在堆栈中的
分布情况如下:最后一个参数->倒数第二个参数->...->第一个参数->函数返回地址->函数代码段.
宏定义:
typedef char* va_list
#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)
va_list 是一个字符指针,可以理解为指向当前参数的一个指针,取参必须通过这个指针进行。调用步骤如下:
1.在调用参数表之前,定义一个 va_list 类型的变量,(假设va_list 类型变量被定义为ap);
2.然后应该对ap进行初始化,让它指向可变参数表里面的第一个参数,这是通过 va_start 来实现的,第一个参数是 ap 本身,第二个参数是在变参表前面紧挨着的一个变量,即“...”之前的那个参数;
3.然后是获取参数,调用va_arg,它的第一个参数是ap,第二个参数是要获取的参数的指定类型,然后返回这个指定类型的值,并且把 ap 的位置指向变参表的下一个变量位置;
4.获取所有的参数之后,我们有必要将这个 ap 指针关掉,以免发生危险,方法是调用 va_end,他是输入的参数 ap 置为 NULL,应该养成获取完参数表之后关闭指针的习惯。说白了,就是让我们的程序具有健壮性。通常va_start和va_end是成对出现
各个宏的功能:
va_list 用于声明一个变量,我们知道函数的可变参数列表其实就是一个字符串,所以va_list才被声明为字符型指针,这个类型用于声明一个指向参数列表的字符型指针变量,例如:va_list ap;//ap:arguement pointer
va_start(ap,v) 它的第一个参数是指向可变参数字符串的变量,第二个参数是可变参数函数的第一个参数,通常用于指定可变参数列表中参数的个数。
va_arg(ap,t) 它的第一个参数指向可变参数字符串的变量,第二个参数是可变参数的类型。
va_end(ap) 用于将存放可变参数字符串的变量清空(赋值为NULL).
va_list的用法:
(1)首先在函数里定义一具va_list型的变量,这个变量是指向参数的指针
(2)然后用va_start宏初始化变量刚定义的va_list变量,这个宏的第二个参数是第一个可变参数的前一个参数,是一个固定的参数。
(3)然后用va_arg返回可变的参数,va_arg的第二个参数是你要返回的参数的类型。
(4)最后用va_end宏结束可变参数的获取。然后你就可以在函数里使用第二个参数了。如果函数有多个可变参数的,依次调用va_arg获取各个参数。
va_list在编译器中的处理:
(1)在运行va_start(ap, v)以后,ap指向第一个可变参数在堆栈的地址。
(2)va_arg()取得类型t的可变参数值,在这步操作中首先apt = sizeof(t类型),让ap指向下一个参数的地址。然后返回ap-sizeof(t类型)的t类型*指针,这正是第一个可变参数在堆栈里的地址。然后用*取得这个地址的内容。
(3)va_end(),x86平台定义为ap = ((char*)0),使ap不再指向堆栈,而是跟null一样,有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如 gcc在linux的x86平台就是这样定义的。
例如:
1. #include <stdio.h> #include <string.h> #include <stdarg.h> //ANSI标准形式的声明方式,括号内的省略号表示可选参数 int demo(char *msg, ...) { //定义保存函数参数的结构 va_list argp; int argno = 0; char *para; //argp指向传入的第一个可选参数,msg是最后一个确定的参数 va_start(argp, msg); while (1) { para = va_arg(argp, int);#类型不能为char、signed char、unsigned char、short、unsigned short、signed short、short int、signed short int、unsigned short int、float if (strcmp(para, "") == 0 ) break; printf("Parameter #%d is: %s\n", argno, para); argno++; } va_end( argp ); //将argp置为NULL return 0; } int main() { demo("DEMO", "This", "is", "a", "demo!", ""); return 0; } 2. #include <stdio.h> #include <stdarg.h> #include <stdlib.h> //第一个参数指定了参数的个数 int sum(int number,...) { va_list vaptr; int i; int sum = 0; va_start(vaptr,number); for(i=0; i<number;i++) { sum += va_arg(vaptr,int); #类型不能为char、signed char、unsigned char、short、unsigned short、signed short、short int、signed short int、unsigned short int、float } va_end(vaptr); return sum; } int main() { printf("%d\n",sum(4,4,3,2,1)); return 0; }
注意:
1.因为va_start, va_arg, va_end等定义成宏,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型,也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现。
2.va_arg(ap,type)取出一个参数的时候,type绝对不能为以下类型:char、signed char、unsigned char、short、unsigned short、signed short、short int、signed short int、unsigned short int、float。在C语言中,调用一个不带原型声明的函数时:调用者会对每个参数执行“默认实际参数提升(default argument promotions)”。同时,对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。提升工作如下:a.float类型的实际参数将提升到double;b.char、short和相应的signed、unsigned类型的实际参数提升到int;c.如果int不能存储原值,则提升到unsigned
int。然后,调用者将提升后的参数传递给被调用者。所以,my_printf是绝对无法接收到上述类型的实际参数的。