写点什么

va_list 可变长参数原理

用户头像
liu_liu
关注
发布于: 2020 年 11 月 16 日



在 c 语言中,我们可以使用可变参数来传入多个参数,比如 printf 函数。可变参数的函数需至少定义一个参数值,其余的用 ... 代表可变参数。



举个栗子,定义如下 sum 函数,求出传入整数的总和。其中 n 为传入整数的个数,它必须有,否则无法知道传入了几个整数。



int sum(unsigned n, ...) {
  va_list ap;
  va_start(ap, n);
  unsigned count = n;
  int sum = 0;
  while (count > 0)
  {
    int value = va_arg(ap, int);
    sum += value;
    count -= 1;
  }
  va_end(ap);
  return sum;
}



从代码中,我们可以看出获取可变参数的步骤如下:



  • 定义 va_list

  • 调用 va_start,并传入第一个参数

  • 调用 va_arg 获取可变参数的值

  • 最后调用 va_end



那么,这些步骤中到底都做了些什么呢?下面我们来一一剖析。



函数调用栈帧



在讲实现原理之前,不得不先介绍函数调用栈帧的布局,因为这是实现可变参数的基础。



函数栈帧就是函数在被调用时,栈上的布局,比如函数参数,函数返回地址,局部变量等等是如何分布的,代表着函数的活动记录。栈增长的方向是`从高往低`。



在栈帧中,有两个重要的寄存器,esp 和 ebp。esp 始终指向栈顶,ebp 指向当前栈帧的栈底,它里面的值是上一个函数栈帧的 ebp,为了在当前函数返回时恢复上个函数的现场。



不同的编译器有着不同的函数调用约定,比如有的参数从右到左进栈,有的从左到右进栈;在参数出栈时有的是调用者清栈,有的是被调用者清栈。下面我们统一以 `cdecl` 为标准,即 c 语言默认的调用约定来讲述。它将从右向左进栈,调用者清栈。



下图是一个函数栈帧的示意图:





当调用一个函数时,会先将参数压栈,然后是返回地址,再就是函数内部的局部变量。有不太清楚的同学可以先去看看栈帧相关的知识。



实现原理



可变参数就是利用了 cdecl 的调用约定。



1. 参数从右向左压栈,那么第一个参数最后进栈。其他可变参数相较于第一个参数来说,只需逐个往高地址方向找即可。

2. 调用者清栈。可变参数只有调用者知道传入了多少个,被调方并不清楚,所以适合调用方来清栈。



假设我们以如下方式调用 sum 函数:



int main(int argc, char *args[])
{
sum(3, 4, 5, 6);  
  return 0;
}



那么 sum 函数调用时,栈帧大体如下所示:



参数 3、4、5、6 依次从右向左进栈,即 6、5、4、3。黄色箭头标记的是第一个参数 3 的地址。



那么 va_xx 之类的宏都做了些什么呢?为什么就能取到可变长参数的值?如果弄懂了栈帧布局,那理解起来也比较简单。



va_list



我们模拟下它的宏定义,它相当于定义了一个 char * 的指针。因为参数的长度是可变的,所以用 char * 类型最合适。



// 定义 char * 指针类型
#define va_list char *



结合栗子来看:



va_list ap;



可转换为如下代码,其实就是定义了 ap 指针:



char *ap;



va_start



主要是做一些准备工作,将 ap 指向传入的第一个可变参数。



从下面代码中可以看到,我们取出了最后一个固定参数的地址,并计算出可变参数的起始地址。这也就是为什么可变参数函数至少需要一个参数的原因,因为需要获取可变参数从哪个地址开始。



// 指向可变参数的第一个
#define va_start(ap, last_arg) (ap = (va_list)&last_arg + sizeof(last_arg))



结合栗子来看:



va_start(ap, n);



可转换为如下代码:



ap = (char *)&n + sizeof(n);

va_arg



这里可能有点迷糊。ap 首先增加了 sizeof(t),然后又减去了 sizeof(t)。主要是为了在一个宏中能让 ap 向上增长,同时又可以获取当前参数的值。



// ap 自增 sizeof(t),然后减去 sizeof(t),顺序获取参数的值
#define va_arg(ap, t) (*(t *)((ap = (ap + sizeof(t))) - sizeof(t)))



结合栗子来看:



int value = va_arg(ap, int);



可转换为如下代码:



// 取值
int value = *(int *)ap;
// 自增
ap += sizeof(t);



### va_end



最后一步,就是将指针清零。



// 指针清零
#define va_end(ap) (ap = ((va_list)0))



结合栗子来看:



va_end(ap);



可转换为如下代码:



ap = (char *)0;



总结



到此,va_xx 的宏作用应该是比较清晰了,总结一下:



  • va_list,定义 char * 类型指针,以便支持任意类型

  • va_start,根据最后一个固定参数地址,定位到第一个可变参数地址

  • va_arg,根据可变参数个数,逐渐向高地址方向取出参数

  • va_end,将指针置空



发布于: 2020 年 11 月 16 日阅读数: 25
用户头像

liu_liu

关注

不要相信自己的记忆力 2017.11.13 加入

还未添加个人简介

评论

发布
暂无评论
va_list 可变长参数原理