c 语言可变参数函数的原理与实现详解
上代码,注意此代码运行在 32 位系统上,所以下文的数据类型大小按32位计算。
#include <stdio.h>
#include <stdarg.h>
// 模仿 printf 函数可变参数的样式
void f(const char *fmt, ...) {
// 模仿获取可变参数的获取过程
char *ap;
ap = (char *)&fmt; // ap 为第一个参数的地址
int v1 = *(int *)(ap + 4); // 因为上一个参数 char * 占4个字节,所以第二个参数(即第一个可变参数)的地址是 ap + 4
int v2 = *(int *)(ap + 8);
char *v3 = *(char **)(ap + 12);
long long v4 = *(long long *)(ap + 16);
int v5 = *(char *)(ap + 24); //上一个参数 long long 占 8 个字节
printf("%d %d %s %lld %c\n", v1, v2, v3, v4, v5);
// c 语言中获取可变参数
va_list a;
va_start(a, fmt); // 这里 a 实际上是第二个参数(第一个可变参数)的地址
int z1 = va_arg(a, int);
int z2 = va_arg(a, int);
char *z3 = va_arg(a, char *);
long long z4 = va_arg(a, long long);
char z5 = va_arg(a, int);
printf("%d %d %s %lld %c\n", z1, z2, z3, z4, v5);
va_end(a);
}
int main() {
long long x = 293738475;
char y = 'r';
f("call %d %d %s %lld", 20, 301, "hello", x, y);
return 0;
}
运行,指定 32 位系统
gcc -m32 -g -O0 f.c
./a.out
运行结果
p1: 20 301 hello 293738475 r
p2: 20 301 hello 293738475 r
为什么可以获取到可变参数
1、函数传递参数时,参数从右到左依次压栈。
2、栈是向下增长的,栈顶内存地址比栈底小。
所以只要知道第一个参数的内存地址(最后压栈参数),和参数类型(所占的内存大小),就可以算出各参数在栈中的位置,从而获取到各参数。
反汇编查看汇编代码实现
objdump -S a.out
此时各参数在内存栈中的分布
赋上获取动态参数的宏定义
#ifndef _STDARG_H
#define _STDARG_H
typedef char *va_list;
/* Amount of space required in an argument list for an arg of type TYPE.
TYPE may alternatively be an expression whose type is used. */
#define __va_rounded_size(TYPE) \
(((sizeof (TYPE) + sizeof (int) - 1) / sizeof (int)) * sizeof (int))
#ifndef __sparc__
#define va_start(AP, LASTARG) \
(AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#else
#define va_start(AP, LASTARG) \
(__builtin_saveregs (), \
AP = ((char *) &(LASTARG) + __va_rounded_size (LASTARG)))
#endif
void va_end (va_list); /* Defined in gnulib */
#define va_end(AP)
#define va_arg(AP, TYPE) \
(AP += __va_rounded_size (TYPE), \
*((TYPE *) (AP - __va_rounded_size (TYPE))))
#endif /* _STDARG_H */
__va_rounded_size(TYPE) : 是为了字节对齐,32 位系统内存栈4个字节对齐,比如 TYPE为3时计算得4,TYPE为7时计算得8。
va_start(AP, LASTARG):AP 指向第二个参数(第一个可变参数)的内存地址。
va_arg(AP, TYPE):每次获取后AP值增加TYPE所占大小,指向下一个可变参数地址,为获取下一个参数准备。
va_end(AP):最后调用,将AP指向无效。