C语言可变参数的使用详解

一、可变参数表介绍
c/c++语言具备一个不同于其他编程语言的的特性,即支持可变参数。
例如c库中的printf,scanf等函数,都支持输入数量不定的参数。例如:
printf(hello world);  ////< 1个参数prinf(%d, a);         ////< 2个参数printf(%d, %d, a, b); ////< 3个参数  
printf函数原型为 int printf(const char *format, …);
从printf的原型来看,其除了接受一个固定参数format以外,后面的参数使用…来表示。
在c/c++语言中,…表示可以接受不定数量的参数。
二、可变参数表用法
在标准c/c++中,头文件中定义了如下三个宏:
void va_start ( va_list arg_ptr, prev_param ); /* ansi version */type va_arg ( va_list arg_ptr, type );void va_end ( va_list arg_ptr );  
va 就是variable argument(可变参数)的意思
arg_ptr 是指向可变参数表的指针
prev_param 则指可变参数表的前一个固定参数
type 为可变参数的类型
va_list 也是一个宏
其定义为typedef char * va_list 实质上是一char 型指针。
char 型指针的特点是++、--操作对其作用的结果是增1 和减1(因为sizeof(char)为1)。
与之不同的是int 等其它类型指针的++、--操作对其作用的结果是增sizeof(type)或减sizeof(type),而且sizeof(type)大于1。
通过使用va_start宏我们可以取得可变参数表的首指针,这个宏的定义为:
#define va_start ( ap, v ) ( ap = (va_list)&v + _intsizeof(v) )  
其作用为将最后那个固定参数的地址加上可变参数对其的偏移后赋值给ap,这样ap就是可变参数表的首地址。
_intsizeof 宏定义为:
#define _intsizeof(n) ((sizeof ( n ) + sizeof ( int ) – 1 ) & ~( sizeof( int ) – 1 ) )  
宏定义va_arg原型为:
#define va_arg(list, mode) ((mode *)(list =(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]  
其作用为指取出当前arg_ptr 所指的可变参数并将ap 指针指向下一可变参数。
va_end宏定义用来结束可变参数的获取,定义为:
#define va_end ( list )  
va_end ( list )实际上被定义为空,没有任何真实对应的代码,用于代码对称,与va_start对应;
可能发挥代码的“自注释”作用。所谓代码的“自注释”,指的是代码能自己注释自己。
三、可变参数表的简单使用
#include #include #include  /** * @brief        求n个数中的最大值 * @details * @param[in]     num 整数个数 * @param[out]    ... 整数 * @retval        最大整数 * @par  */int max ( int num, ... ) {  int m = -0x7fffffff; /* 32 系统中最小的整数 */  va_list ap;  va_start ( ap, num );  for ( int i= 0; i m ) {    m = t;    }  }  va_end (ap);  return m;}int main ( int argc, char* argv[] ) {  int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5 个整数中的最大值 */  cout << n;  return 0;}  
max(int num, …)中首先定义了可变参数表指针ap,而后通过va_start ( ap, num )取得了参数表首地址(赋给了ap),其后的for 循环则用来遍历可变参数表。
max函数相比于printf简单了许多,其原因如下:
max函数可变参数表的长度是已知的,通过num参数传入;
max函数可变参数表中参数的类型是已知的,都为int型;
printf 函数可变参数的个数不能轻易的得到,而可变参数的类 型也不是固定的,需由格式字符串进行识别(由%f、%d、%s 等确定)。
四、运行机制
反汇编是研究语法深层特性的终极良策,首先查看main函数中调用max函数时的反汇编:
1. 004010c8 push 52. 004010ca push 83. 004010cc push 34. 004010ce push 65. 004010d0 push 56. 004010d2 push 57. 004010d4 call @ilt+5(max) (0040100a)  
第一步:将参数从右向左入栈(第1~6行)
第二步:调用call 指令进行跳转(第7行)
这两步包含了深刻的含义,它说明c/c++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。
x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部。
int max ( int num, ...) {1. 00401020 push ebp2. 00401021 mov ebp,esp3. 00401023 sub esp,50h4. 00401026 push ebx5. 00401027 push esi6. 00401028 push edi7. 00401029 lea edi,[ebp-50h]8. 0040102c mov ecx,14h9. 00401031 mov eax,0cccccccch10. 00401036 rep stos dword ptr [edi]    va_list ap;    int m = -0x7fffffff; /* 32 系统中最小的整数 */11. 00401038 mov dword ptr [ebp-8],80000001h    va_start ( ap, num );12. 0040103f lea eax,[ebp+0ch]13. 00401042 mov dword ptr [ebp-4],eaxfor ( int i= 0; i m )28. 00401071 mov eax,dword ptr [t]29. 00401074 cmp eax,dword ptr [ebp-8]30. 00401077 jle max+5fh (0040107f)    m = t;31. 00401079 mov ecx,dword ptr [t]32. 0040107c mov dword ptr [ebp-8],ecx    }33. 0040107f jmp max+2eh (0040104e)    va_end (ap);34. 00401081 mov dword ptr [ebp-4],0    return m;35. 00401088 mov eax,dword ptr [ebp-8]    }36. 0040108b pop edi37. 0040108c pop esi38. 0040108d pop ebx39. 0040108e mov esp,ebp40. 00401090 pop ebp41. 00401091 ret  
第1~10行进行执行函数内代码的准备工作,保存现场;
第2行对堆栈进行移动;
第3行则意味着max函数为其内部局部变量准备的堆栈空间为50h字节;
第11行表示把变量n 的内存空间安排在了函数内部局部栈底减8的位置(占用4个字节);
第12~13行非常关键,对应着va_start ( ap, num),这两行将第一个可变参数的地址赋值给了指针ap;
从第12行可以看出num 的地址为ebp+0ch;
从第13行可以看出ap 被分配在函数内部局部栈底减4 的位置上(占用4 个字节);
第22~27行最为关键,对应着va_arg (ap, int);
第22~24行的作用为将ap 指向下一可变参数(可变参数的地址间隔为4 个字节,从add eax,4 可以看出);
第25~27行则取当前可变参数的值赋给变量t。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给t(从mov edx,dword ptr [ecx-4]语句可以看出);
第36~41行恢复现场和堆栈地址,执行函数返回操作。


气动调节阀的结构_气动调节阀的工作原理
电容器放在潮湿的地方会有什么影响吗
多个大牛分析2021年RFID市场的趋势
深度测评宝马3系GT 335i
45nm工艺直跃2nm工艺,日本芯片工艺凭什么?
C语言可变参数的使用详解
HT4832丨33mW免输出电容立体声耳机放大器
Altium Designer原理图中BUS的使用
成功构建文本分析工作流的四个步骤
docker 搜索镜像,docker查看镜像详细信息(docker下载镜像命令)
GEO半导体有意收购美信的视频IC部门
FCBGA先进封装基板兴力量
电话机器人的工作效率高吗,它的优势是什么
英特尔推出了基于新华三UIS超融合一体机精选开源云解决方案
电子工程师必备的元器件整理大法
接近传感器的特性有哪些
可同时测试多个手机主板BTB连接器的弹片微针模组
镖神汽车自适应远近光灯智能控制系统自动切换远近光灯
风电场SCADA系统的数据传输技术
区块链对仪器行业的发展会有什么影响