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 ) & ~( sizeofint ) – 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 <stdlib.h>
#include <stdio.h>
#include <stdarg.h>

 /**
 * @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< num; i++ ) {
    int t = va_arg (ap, int);
    if ( t > m ) {
    m = t;
    }
  }
  va_end (ap);
  return m;
}

int main int argc, char* argv[] ) {
  int n = max ( 556 ,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函数时的反汇编:

1004010C8 push 5
2004010CA push 8
3004010CC push 3
4004010CE push 6
5004010D0 push 5
6004010D2 push 5
7004010D4 call @ILT+5(max) (0040100a)

 

  • 第一步:将参数从右向左入栈(第1~6行)
  • 第二步:调用call 指令进行跳转(第7行)

这两步包含了深刻的含义,它说明C/C++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。

x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部。

int max ( int num, ...) {
100401020 push ebp
200401021 mov ebp,esp
300401023 sub esp,50h
400401026 push ebx
500401027 push esi
600401028 push edi
700401029 lea edi,[ebp-50h]
8. 0040102C mov ecx,14h
900401031 mov eax,0CCCCCCCCh
1000401036 rep stos dword ptr [edi]
    va_list ap;
    int m = -0x7FFFFFFF; /* 32 系统中最小的整数 */
1100401038 mov dword ptr [ebp-8],80000001h
    va_start ( ap, num );
12. 0040103F lea eax,[ebp+0Ch]
1300401042 mov dword ptr [ebp-4],eax
for ( int i= 0; i< num; i++ )
1400401045 mov dword ptr [ebp-0Ch],0
15. 0040104C jmp max+37h (00401057)
16. 0040104E mov ecx,dword ptr [ebp-0Ch]
1700401051 add ecx,1
1800401054 mov dword ptr [ebp-0Ch],ecx
1900401057 mov edx,dword ptr [ebp-0Ch]
20. 0040105A cmp edx,dword ptr [ebp+8]
210040105D jge max+61h (00401081) {
    int t= va_arg (ap, int);
22. 0040105F mov eax,dword ptr [ebp-4]
2300401062 add eax,4
2400401065 mov dword ptr [ebp-4],eax
2500401068 mov ecx,dword ptr [ebp-4]
26. 0040106B mov edx,dword ptr [ecx-4]
27. 0040106E mov dword ptr [t],edx
    if ( t > m )
2800401071 mov eax,dword ptr [t]
2900401074 cmp eax,dword ptr [ebp-8]
3000401077 jle max+5Fh (0040107f)
    m = t;
3100401079 mov ecx,dword ptr [t]
32. 0040107C mov dword ptr [ebp-8],ecx
    }
33. 0040107F jmp max+2Eh (0040104e)
    va_end (ap);
3400401081 mov dword ptr [ebp-4],0
    return m;
3500401088 mov eax,dword ptr [ebp-8]
    }
36. 0040108B pop edi
37. 0040108C pop esi
380040108D pop ebx
39. 0040108E mov esp,ebp
4000401090 pop ebp
4100401091 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行恢复现场和堆栈地址,执行函数返回操作。
posted @   r_jw  阅读(554)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示