可变参数列表听着陌生,但其实我们经常在printf和scanf函数中使用。
如下:
其中的… 就是可变参数列表在形参中的表示。
对于可变参数列表的使用关乎到四个宏的使用,其分别是va_list,va_start,va_arg,va_end。
va_list是(char*)重命名的类型,定义可以访问可变参数的变量。
va_start(ap, v) ap是定义的可变参数变量,v是形参中可变参数前第一个参数名,其作用是使ap指向可变参数部分。
va_arg(ap, t) ap是定义的可变参数变量,t是可变参数的类型,根据类型,访问可变参数列表中的数据。
va_end(ap) ap是定义的可变参数变量,使ap变量置空,作为结束使用。
在次我用求解5个数中最大值这一问题,来演示可变参数列表的使用。
代码如下:
//VS2022中使用
int FindMax(int num, ...)
{
va_list arg;
__crt_va_start(arg, num);
int max = __crt_va_arg(arg, int);
for (int i = 0; i < num-1; i++)
{
int cur = __crt_va_arg(arg, int);
if (max < cur)
{
max = cur;
}
}
__crt_va_end(arg);
return max;
}
int main()
{
int max = FindMax(5, 10, 20, 30, 40, 50);
printf("%d\n", max);
return 0;
}
其宏的实现如下:
对于va_list和__crt_va_end而言非常好理解。va_list就是(char*)的重命名,__crt_va_end就是将ap可变参数变量置为0,而对于__crt_va_start_a和__crt_va_arg就相对难理解。
我们都知道,函数形参的实例化,是在栈顶直接进行压栈操作,是从右向左进行的。
如下:
粗线框部分是将要压栈的部分,32h,28h,1Eh…分别是50,40,30的十六进制
也就是说,函数的形参的临时拷贝的相对位置关系是固定的。那么如图,当我知道num的地址,那么我就可以访问可变参数列表中的数据。
0x0113fc10就是esp当前位置,0x0113fc08就是ebp-8的位置,也就是变量arg所在空间。
0x0113fc1c就是ebp+0ch的位置,也就是可变参数列表数据中第一个元素(10)的位置。
此时__crt_va_start(arg, num)未执行。
此时__crt_va_start(arg, num)执行完毕。
0x0113fc08(arg变量)此时存储的地址就是可变参数列表中第一个元素的地址。
示意图:
如图,__crt_va_start_a等价于(char*)(&(num))+_INTSIZEOF(int),就是通过num的地址,加上_INTSIZEOF(v)找到可变参数列表第一个元素(10)的地址,存放到变量arg中。
此时__crt_va_arg未执行。
arg中存放的是可变参数列表中,第一个元素(10)的地址。
此时__crt_va_arg执行完毕。
arg中地址所指向的空间,为可变参数列表第二个元素(20)的地址,并将可变参数列表中第一个元素的内容放在max变量中。
那么是如何进行的?
先将arg中的地址放在eax中,再让eax自增4,再放回arg中,此时arg中地址是可变参数列表第二个元素(20)的地址。
再读取arg中的地址放到ecx中,再寻找ecx-4处的地址,也就是可变参数列表第一个元素的地址,将其放到edx中,再将edx中地址所指向空间的内容放到ebp-14h处,也就是将可变参数列表第一个元素(10)放到max的空间中。
而上图中00D117BB,00D117BE,00D117C1指令就完成了(ap+=_INTSIZEOF(t))【先即为表达式X】的操作,00D117C4,00DC117C7完成的就是X-_INTSIZEOF(t)的操作。
示意图:
到此,对于**__crt_va_start_a** 和 __crt_va_arg就基本理解,那么什么事_INTSIZEOF(n)?
_INTSIZEOF(n)其实就是求4的对齐数。那么为什么要怎么做,直接加上可变参数类型的大小不好吗?
int FindMax(int num, ...)
{
va_list arg;
__crt_va_start(arg, num);
int max = __crt_va_arg(arg, int);
for (int i = 0; i < num - 1; i++)
{
int cur = __crt_va_arg(arg, int);
if (max < cur)
{
max = cur;
}
}
__crt_va_end(arg);
return max;
}
int main()
{
//int max = FindMax(5, 10, 20, 30, 40, 50);
//对于短整型而言,在形参实例化中,会发生整形提升
char e = 'e';
char d = 'd';
char c = 'c';
char b = 'b';
char a = 'a';
int max = FindMax(5, a, b, c, d, e);
printf("%d\n", max);
return 0;
}
注意在上述代码中,我传递的可变参数列表是char类型,而我在使用__crt_va_arg时,使用的是int类型。那么我是否可以得到正确答案?
如下:
101所对应的ASCII值就是e。
其实movsx就是会将短整型形参,发生整形提升。而对于这种情况,我们要是在__crt_va_arg中加减类型的大小就会造成错误,所以才要_INTSIZEOF(n)。