什么是可变参数列表?以及可变参数列表是如何实现的?
1、首先什么是可变参数列表?
对于一般的函数而言,参数列表都是固定的,而且各个参数之间用逗号进行分开。这种函数在调用的时候,必须严格按照参数列表中参数的个数进行传参,否则编译器就会报错。
int add(int a, int b) //该函数定义时,参数有两个,所以在调用时只能传入两个参数
{int c = a + b;return c;
}
int main()
{int sum1 = 0;int sum2 = 0;int sum3 = 0;sum1 = add(1); //报错:error C2198: “add”: 用于调用的参数太少sum2 = add(1, 2); sum3 = add(1, 2, 3); //报错:warning C4020: “add”: 实参太多return 0;
}
我们应该都注意到,库函数 printf(); 的参数并不是固定的,传入的参数个数不同,但该函数仍然可以成功执行。如下:
int a = 20;int b = 30;printf("10\n"); printf("%d\n",a);printf("%d %d\n",a, b);//printf函数的定义如下://int __cdecl printf(_In_z_ _Printf_format_string_ const char * _Format, ...);//可以看出参数列表中的参数并没有完全给出
所以,具有可变参数列表的函数就是:函数定义时,参数列表中的的参数不完全定义;调用该函数时,可以根据实际情况传入多个参数,且可以成功完成其函数功能。而该函数的参数列表就是可变参数列表。
2、那么通过一个简单的函数,详细了解一下具有可变参数列表的函数时如何实现函数功能的
#include<stdio.h>
#include<stdarg.h>int average(int n, ...) //该函数的功能为:求出任意个数参数的平均值
{int i = 0;int sum = 0;va_list arg; //即:char* arg;va_start(arg, n); //即:arg = (char*)&n + 4;//初始化arg为位置参数列表中的第一个参数的地址for(i=0; i<n; i++){sum += va_arg(arg, int); //即: sum += (*(int *)(arg += 4) - 4);//此时已经将arg重新赋值为可变参数列表中第二个参数的地址,//但是此处保留的仍然是上一个参数的地址,然后对保留地址进行 //强制类型转换之后解以用得到内容(参数)}return sum/n;va_end(arg); //即:arg = char* 0; //把arg置为NULL
}
int main()
{int a = 10;int b = 20;int c = 30;int avg1 = average(2, a, b);int avg2 = average(3, a, b,c);printf("avg1 = %d\n",avg1);printf("avg2 = %d\n",avg2);return 0;
}
上述代码的执行结果:。
可以看到函数中定义出了几个之前未见过的符号,我们转到定义看看到底是什么:
typedef char * va_list; //类型重定义:将char* 定义为va_list#define va_start _crt_va_start
#define va_arg _crt_va_arg
#define va_end _crt_va_end //这三个都是#define 定义的符号,不清楚是什么,再次转到定义:#define _crt_va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
#define _crt_va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define _crt_va_end(ap) ( ap = (va_list)0 ) //原来是三个宏,对其中不明白的符号再次转到定义:#define _ADDRESSOF(v) ( &(v) )
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )//仍然是两个宏
哦,这时候先进行替换,方便理解:
va_list arg; 相当于 char* arg; //arg是个字符指针呀
va_start(arg, n); 相当于 _crt_va_start(arg, n);相当于(arg = (va_list)_ADDRESSOF(n) + _INTSIZEOF(n))
相当于 (arg = (char*)&n + ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
其中 ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) ;求出n的字节数,然后向上取整为4的倍数(换句话说: _INTSIZEOF(n)整个做的事情就是将n的长度化为int长度的整数倍)
所以在上述代码中相当于 arg = (char*)&n + 4;
//初始化arg为位置参数列表中的第一个参数的地址(如调用这个函数时用的参数为(int 3, 1,2,6),那么此时arg指向的是1所在的地址)
va_arg(arg, int); 相当于 _crt_va_arg(arg,int)
相当于 ( *(int *)((arg += _INTSIZEOF(int)) - _INTSIZEOF(int)) )
所以在上述代码中相当于 *(int *)(arg += 4) - 4;
//此时已经将arg重新赋值为可变参数列表中第二个参数的地址,但是此处保留的仍然是上一个参数的地址,然后对保留地址进行强制类型转换之后解以用得到内容(参数)
va_end(arg); 相当于 (arg = (va_list)0)
所以在上述代码中相当于 arg = char* 0; //把arg置为NULL
回到原来代码中进行替换。
为了更好的理解,我们把该程序的反汇编拷贝出来解析一下:(仅以int avg1 = average(2, a, b);本次调用为例)
int main() //调用main函数之前已经调用了其他函数
{
01154770 push ebp //将当前ebp的地址压栈到当前的栈顶,每次压栈esp都更新
01154771 mov ebp,esp //将当前的ebp移动到当前esp的位置处
01154773 sub esp,0FCh //将esp向低地址处移动OFCh,为main()开辟空间
01154779 push ebx //将寄存器ebx压入栈顶
0115477A push esi //将寄存器esi压入栈顶
0115477B push edi //将寄存器edi压入栈顶
0115477C lea edi,[ebp-0FCh] //将为main开辟空间时的esp的地址加载进入edi中
01154782 mov ecx,3Fh //ecx中放入3Fh
01154787 mov eax,0CCCCCCCCh //eax中放入0CCCCCCCCh
0115478C rep stos dword ptr es:[edi] //从edi中所存的地址处开始,用0CCCCCCCCh始化3Fh次
0115478E mov ecx,offset _FC008D77_test@c (0115C003h)
01154793 call @__CheckForDebuggerJustMyCode@4 (01151212h) int a = 10;
01154798 mov dword ptr [a],0Ah //创建变量,dword ptr [a]中放入0Ahint b = 20;
0115479F mov dword ptr [b],14h //创建变量,dword ptr [b]中放入14hint c = 30;
011547A6 mov dword ptr [c],1Eh //创建变量,dword ptr [c]中放入1Ehint avg1 = average(2, a, b); //调用函数前先进行传参(参数列表中从右向左)
011547AD mov eax,dword ptr [b] //eax中放入dword ptr [b]的内容
011547B0 push eax //调用average函数前将要用的形参放入寄存器并压栈
011547B1 mov ecx,dword ptr [a] //eax中放入dword ptr [b]的内容
011547B4 push ecx //调用average函数前将要用的形参放入寄存器并压栈
011547B5 push 2 //将参数2也压栈
011547B7 call _average (01151398h) //开始调用average函数(将此地址进行压栈/保护现场)
011547BC add esp,0Ch //esp回到原来的位置,将开辟的栈帧回收
011547BF mov dword ptr [avg1],eax //将eax中存的平均值放入avr1中准备打印int average(int n, ...)
{
01151810 push ebp
01151811 mov ebp,esp
01151813 sub esp,0E4h
01151819 push ebx
0115181A push esi
0115181B push edi
0115181C lea edi,[ebp-0E4h]
01151822 mov ecx,39h
01151827 mov eax,0CCCCCCCCh
0115182C rep stos dword ptr es:[edi] //average()函数的栈帧开辟以及初始化过程,同main()0115182E mov ecx,offset _FC008D77_test@c (0115C003h)
01151833 call @__CheckForDebuggerJustMyCode@4 (01151212h) int i = 0;
01151838 mov dword ptr [i],0 //创建局部变量i并初始化为0;int sum = 0;
0115183F mov dword ptr [sum],0 //创建局部变量sum并初始化为0;va_list arg; va_start(arg, n);
01151846 lea eax,[ebp+0Ch] //将当前的ebp+0Ch处的地址存放到eax中
01151849 mov dword ptr [arg],eax //将eax中的内容赋值给arg for (i = 0; i < n; i++)
0115184C mov dword ptr [i],0 //进入循环,先把i用0赋值
01151853 jmp average+4Eh (0115185Eh) //跳转到0115185Eh处,执行eax,dword ptr [i]
01151855 mov eax,dword ptr [i] //由0115187Bh跳转过来
01151858 add eax,1 //执行++
0115185B mov dword ptr [i],eax //并重新赋值给i,继续进行循环
0115185E mov eax,dword ptr [i] //由01151853h跳转过来,将此时的i加载到eax中
01151861 cmp eax,dword ptr [n] //eax的值与变量n的值进行比较
01151864 jge average+6Dh (0115187Dh) {sum += va_arg(arg, int); //即: sum += (*(int *)(arg += 4) - 4);
01151866 mov eax,dword ptr [arg]
01151869 add eax,4
0115186C mov dword ptr [arg],eax //此时arg存放的是下一个参数(第3个参数)的地址
0115186F mov ecx,dword ptr [arg]
01151872 mov edx,dword ptr [sum]
01151875 add edx,dword ptr [ecx-4] //sum的值加上一次arg所指地址处的内容(第2个参数)
01151878 mov dword ptr [sum],edx //将求的和继续放在sum中 }
0115187B jmp average+45h (01151855h) //跳转到0115185Eh处,执行准备执行i++return sum / n;
0115187D mov eax,dword ptr [sum] //将for循环结束之后的sum放入eax中
01151880 cdq
01151881 idiv eax,dword ptr [n] //用eax中的数值除以变量n的内容,得到平均值
01151884 jmp average+7Dh (0115188Dh) va_end(arg); //即:arg = char* 0; //把arg置为NULL
01151886 mov dword ptr [arg],0
}
0115188D pop edi
0115188E pop esi
0115188F pop ebx //弹出栈顶的各种寄存器
01151890 add esp,0E4h //回收为average开辟的栈帧
01151896 cmp ebp,esp
01151898 call __RTC_CheckEsp (0115121Ch)
0115189D mov esp,ebp
0115189F pop ebp
011518A0 ret //最终将栈顶的元素取出作为一个地址跳转到该地址处回到main()函数)
再以一个简单的图表示一下:
int avg1 = average(2, a, b); 该语句调用average() 函数时,其参数列表中有三个参数,而且是从右向左依次压栈的。
因为参数列表中最左侧的参数表示的就是该参数之后参数的个数,所以压栈顺序它在函数栈帧的最上方。当调用函数时,读取这个参数就能在此基础上明确参数的总个数,在利用for循环和arg指针就能准确的完成该函数的功能。
所以average(); 函数不管有几个参数都可以按照上面的函数栈帧的运行规律正确读取参数个数并成功完成功能。
3、总结一下:
3.1 实现参数列表可变的函数的前提是:函数栈帧的压栈规律(参数列表中的参数是从右向左以此压栈的,同时函数栈帧是先使用高地址再使用低地址的)。
3.2 具有可变参数列表的函数在定义时,参数列表中的参数是不完全给定的,只给出第1个参数,后面用“...”表示,而且第1个参数必须是int类型,用于调用该函数时确定本次调用该函数所传的参数的个数(若传递n个参数,则第1个参数为n-1)。
3.3 一般都要用到 va_list arg; va_start(arg, n); va_arg(arg, int); va_end(arg)
3.4 有一点必须注意:va_arg(arg, int) 每使用一次这个函数,arg指针都会指向下一个参数,所以必须合理使用
4、可变参数的限制:
4.1 可变参数必须从头到尾依次访问,在依次访问中可以在中间某一参数处停止,但不能直接访问参数列表中间的参数。
4.2 参数列表中至善有一个命名参数,否则无法使用va_start。
4.3 上述用到的宏是无法直接判断实际存在的参数的数量。
4.4 如果在va_arg中执行了错误的类型,那么其后果是不可预测的。
什么是可变参数列表?以及可变参数列表是如何实现的?相关推荐
- Scala可变参数列表,命名参数和参数缺省
重复参数 Scala在定义函数时允许指定最后一个参数可以重复(变长参数),从而允许函数调用者使用变长参数列表来调用该函数,Scala中使用"*"来指明该参数为重复参数.例如: 1 ...
- python函数用法详解2(变量的作用域(全局变量、局部变量)、共享全局变量、函数返回值、函数的参数(位置参数、关键字参数、默认参数、不定长参数)、拆包、交换变量值、引用、可变和不可变类型)
1. 变量作⽤域 变量作⽤域指的是变量⽣效的范围,主要分为两类:局部变量和全局变量. 局部变量 定义在函数体内部的变量,即只在函数体内部⽣效. def testA(): ...
- python def函数报错详解_【python】详解python函数定义 def()与参数args、可变参数*args、关键参数**args使用实例...
Python内置了很多函数,可以直接调用.Python内置的函数可以通过官方文档查看.也可以通过help()查看帮助信息.函数名是指向函数对象的引用,把函数名赋给变量,相当于给函数起了别名. 1. 定 ...
- python 可变参数 关键字参数_Python之 可变参数和关键字参数
原标题:Python之 可变参数和关键字参数 刚开始接触 python 的时候,对 python 中的 *wargs (可变参数) 和 **kwargs (关键字参数)的理解不是很透彻,看了一下 &l ...
- python可变类型与不可变类型作为函数参数区别_不要用可变类型对象做函数默认参数...
不要用可变类型对象做函数默认参数 1. 可变对象做默认参数 内置数据类型int,float,bool,str,tuple 是不可变对象, 字典,集合,列表是可变对象. 在定义python函数时,千万不 ...
- mysql不定参数函数_可变参数函数(一)
一个函数可以接受不定数的参数个数,这就是可变参数函数,比较常见的比如printf(),scanf(): printf(const char*format,-); printf("%d&quo ...
- python3函数参数(必选参数、默认参数、关键字参数、可变参数)
python3函数参数 形参是参数在函数定义过程中的状态,这个过程中没有赋予实际的数值,实参是参数在函数调用过程中的状态,当参数被赋予实际的数值后,它会由形参转为实参. 必选参数在前,默认参数在后,默 ...
- python函数参数之必选参数,默认参数,可变参数,关键字参数
Python的函数定义非常简单,但灵活度却非常大.除了正常定义的必选参数外,还可以使用默认参数.可变参数和关键字参数, 1 默认参数 定义形式:def calc(para1,para2=None):其 ...
- python 默认参数后接可变参数_Python可变参数会自动填充前面的默认同名参数实例...
Python可变参数会自动填充前面的默认同名参数实例 最近在学习Python的时候遇到一个知识点,在此记录下来 可变参数会自动填充前面的同名默认参数 比如下面这个函数 def add_student( ...
- c# 方法参数 传值or传引用?(ref,out,可变参数params,可选参数,命名参数)
目录 一.方法参数的类型----值类型和引用类型 二.一些特殊的方法参数 1.引用参数---ref 2.输出参数---out 注意:ref和out的区别 3.可变参数/参数数组-----params ...
最新文章
- 数据结构Queue:poll、offer、element、peek
- Linux内核BPF学习1
- Oralce的图形化界面----plsql developer涉及到的知识点总结
- 五、Hive数据类型和简单使用
- mysql 架构优化_Mysql 架构及优化之-查询性能优化
- 解决虚拟机时间引起的奇怪问题
- 关于table的用法(二)
- Django生命周期,FBV,CBV
- MPEG4 笔记3(TRAK,TKHD, MVHD)
- linux iptables命令
- Java自学网站推荐(整理好发给大家)
- netty银行账目管理系统_基于Java的银行帐目管理系统.doc
- 基金收益率计算5:金额加权收益率(MWRR)和时间加权收益率(TWRR)
- Mac电脑怎样添加打印机?
- 软件插件安装激活教程以及密钥
- Notepad++ 一键格式化php代码
- 腾讯QQ2004II Beta3火爆下载 可自定义头像
- C简单动态规划——爬数塔
- 百度网盘如何生成目录树结构?
- 8大经典数据挖掘算法
热门文章
- 【深度学习】利用深度学习监控女朋友的微信聊天?
- Clearing Floats清除浮动--clearfix的不同方法的使用概述
- PHP 如何安装ionCube扩展
- 【网址】收藏一下会死啊!
- C#、TypeScript 之父 Anders Hejlsberg:“会用 Excel 的,都是程序员 ”
- 电机专题:控制电机介绍
- 谈IBM的转型与人工智能开发
- R语言——(五)、探索性数据分析
- 第六届全国计算机学科博士后论坛,首届全国计算机学科博士后论坛在清华大学圆满举行...
- 居家学习:新冠肺炎疫情下中国高校基于直播的远程教育体验的混合方法分析