图解可变参数函数
1. 输入参数在内存中的布局
printf是可变参数函数的典型例子, 其中使用了如下几个宏,
#ifndef va_arg#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif /* _VALIST *//* Storage alignment properties */#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)/* Variable argument list macro definitions */#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (ap = (va_list) NULL)
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))#endif /* va_arg */
看起来也太吓人了. 先不分析, 直接看一个函数调用后, 参数在内存中的布局.
先定义一个可变参数函数
void *p_fmt;
void app_printf ( const char * fmt, ... )
{p_fmt = (void *)&fmt;
}
p_fmt获取了fmt的地址.
在main函数中调用app_printf, 随便输入几个参数
int main(void)
{char a = 0x12;int b = 0xAABB1234;long long c = 0x9988776655443322;app_printf("hello \n", a, b, c);return 0;
}
开启调试, 进入到app_printf中, 将p_fmt加入到memory窗口观察

看到了熟悉的几个数字, 12肯定是传入的a, AABB1234是b, 后面还有一串应该是c被分成两截了, 说明参数就是放在这里的. 最前面的是什么呢, 把它整理成32位的值(0x0041606c)也把它加入到memory观察.

在右侧的ascii码表示中出现了hello, 就是传入的那个字符串. 可以看到传入的参数有规律, 按照顺序和该类型占用的字节数保存在一段连续的内存中.
那么如果能知道每个参数的类型, 就可以知道占内存的大小, 也就可以在这块区域里面逐个找出来.

2. 自定义简易的参数获取
要获取fmt后面的第一个参数, 那就得知道fmt占用了多少字节, 这点可以通过sizeof获取, 万一是char型呢, 前面看到char型参数, 在高位填充了0使它填满32位, 因为这是32位平台. 因此需要考虑参数大小不是4字节的整倍数的情况.
// 对size求"进4"法, 即1~3都算4, 5~7都算8
static inline size_t get_4B_size(size_t size)
{size_t tmp = size % 4;if (tmp > 0){ // 不是4的倍数return (size / 4) * 4 + 4;}else{ // 正好是4的倍数return size;}}// 获取变量A后面的地址赋值给ap
#define my_va_start(ap, A) ( (ap) = ( ((char *) &(A)) + get_4B_size(sizeof(A)) ) )// 获取A的地址 // 偏移到A的下一个
那么通过my_va_start就可以得到"…"中的第一个参数了.
static void * vlist; // 定义一个指针 my_va_start(vlist, fmt); // 指向省略号...中的第一个参数的地址
将vlist加入watch窗口

可以看到vlist在fmt的后面4个字节, 也就是传入参数a的地址.
是不是可以继续用这种方法获取后面的参数呢? 不行了, 因为fmt的类型是知道的, 即char *型, 但是省略号中的参数就不知道了, 必须指定类型.
// 获取参数列表中当前参数的值, 并把列表指针指向下一个参数
#define my_va_arg(ap, T) \
({ \T arg_val; \arg_val = *(T *)ap;/*获取当前参数的值*/ \ap += get_4B_size(sizeof(T)); /*指向下一个参数*/\arg_val;/*返回获取的参数值*/ \
})

调用了my_va_arg之后会返回当前指向的参数的值, 并且会将vlist指向下一个参数.
测试一下
void app_printf ( const char * fmt, ... )
{p_fmt = (void *)&fmt;static void * vlist; // 省略号...中的第一个参数的地址
// my_va_list vlist;my_va_start(vlist, fmt);static char param_a;param_a = my_va_arg(vlist, char); // 第1个传入的参数是char型static int param_b;param_b = my_va_arg(vlist, int); // 第2个传入的参数是int型static long long param_c;param_c = my_va_arg(vlist, long long); // 第3个传入的参数是long long型}
在每个my_va_arg前面打断点, 观察vlist的变化

获取char型变化4字节

获取int型变化4字节

获取long long 型变化8字节

可以看到每次获取参数, 都修改vlist.

成功获取到了fmt之后的三个参数.
但是这种方法不可移植, 在别的平台不一定是这么存储的, 只有编译器知道怎么存储, 只能包含头文件使用其提供的宏.
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
