主函数在调用函数时,会进行传参,当实参的数量发生变化但被调用函数的功能没有发生改变,这时候被调用函数的参数数量也要发生改变。C语言中有一个比较有意思的实现------可变参数列表
可变参数列表,通过将函数实现为可变参数的形式,可以使得函数可以接受1个以上的任意多个参数(不固定)。
举个例子:
实现一个函数可以求任意个参数的平均值
#include <stdio.h>
#include <stdarg.h>
int average(int n, ...)
{
va_list arg;
int i = 0;
int sum = 0;
va_start(arg, n);
for (i = 0; i < n; i++)
{
sum += va_arg(arg, int);
}
return sum / n;
va_end(arg);
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
int avg1 = average(2, a, c);
int avg2 = average(3, a, b, c);
printf("avg1=%d\n", avg1);
printf("avg2=%d\n", avg2);
return 0;
}
结果如下:
源码解析
上面的代码,对于没学过可变参数列表的人来说是一脸懵逼,上面代码中的va_x都是什么意思呢,接下来 ,就逐一了解。
首先要了解的是:
函数在传参的时候,首先在栈区push的是c接下来依次从右向左push参数,最后是要计算的参数的个数。
在调用后面的average函数时把要计算的参数的个数传给了n,后面的参数到底是怎么传过去的目前还不知道。
前面的都了解了,接下来就了解这些陌生的va_x。
va_list
在代码区选中va_list右击选择转到定义,看到下图所示的代码:
可以说va_list就是一个char*
va_start
在代码区选中va_start右击选择转到定义,看到如下图所示代码:
了解定义后,做相应的替换,得到如下结果:
va_start(arg, n);这段代码的意思是初始化arg为未知参数列表的第一个参数的地址。
va_arg
了解定义后,做相应的替换,得到如下结果:
va_arg(arg, int);向sum上加数字,直到for循环结束。
va_end
在代码区选中va_end右击选择转到定义,看到如下图所示代码:
了解定义后,做相应的替换,得到如下结果:
代码替换
把不了解的va_x替换成所认识的东西。
#include <stdio.h>
#include <stdarg.h>
int average(int n, ...)
{
//va_list arg;
char* arg;
int i = 0;
int sum = 0;
//va_start(arg, n);
//__crt_va_start_a(ap, v) ((void)(ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)))
(arg = (char*)&(n)+4);//初始化arg为未知参数列表的第一个参数的地址
for (i = 0; i < n; i++)
{
//sum += va_arg(arg, int);
//__crt_va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))
sum +=(*(int*)((arg += 4) - 4));
//此时arg指向的是c的地址,但表达式留下的是a的地址
//为什么指向c的地址?下一个处理的是c
}
return sum / n;
//va_end(arg);
//__crt_va_end(ap) ((void)(ap = (va_list)0))
arg = (char *)0;//arg赋值成空指针
}
int main()
{
int a = 1;
int b = 2;
int c = 3;
int avg1 = average(2, a, c);
int avg2 = average(3, a, b, c);
printf("avg1=%d\n", avg1);
printf("avg2=%d\n", avg2);
return 0;
}
运行结果:
- 声明一个va_list类型的变量arg,它用于访问参数列表的未确定部分。
- 这个量是调用va_start来初始化的。他的第一个参数是va_list的变量名,第二个参数是省略号前最后一个有名字的参数。初始化过程把arg变量设置为指向可变参数部分的第一个参数。
- 为了访问参数,需要使用va_arg,这个宏接受两个参数:va_list变量和参数列表中下一个参数的类型。在这个例子中所有可变参数都是整型。va_arg返回这个参数的值,并使用va_arg指向下一个可变参数。
- 最后,当访问完毕,最后一个可变参数之后,需要调用va_end
可变参数的限制
注意:
- 可变参数必须从头到尾逐个访问。如果在访问了几个可变参数之后想半途中止,这是可以的,但是,如果想一开始就访问参数列表中间的参数,那是不行的。
- 参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
- 这些宏是无法直接判断实际存在参数的数量。
- 如果va_arg中指定了错误的类型,那么其后果是不可预测的。
相关例题
1.使用可变参数,实现函数,求函数参数的最大值。
#include <stdio.h>
#include <stdarg.h>
int Max(int n, ...)
{
int max = 0;
int i = 0;
va_list arg;
va_start(arg, n);
max = va_arg(arg, int);
for (i = 1;i < n; i++)
{
int tmp = va_arg(arg,int );
if (tmp > max)
{
max = tmp;
}
}
va_end(arg);
return max;
}
int main()
{
int max1 = Max(2, 1, 4);
int max2 = Max(3, 3, -1, -2);
printf("%d\n", max1);
printf("%d\n", max2);
return 0;
}
2.模拟实现printf函数,可完成下面的功能
//能完成下面函数的调用。
//函数原型:
//print(char *format, …)
//
void show(int n)
{
if (n>9)
{
show(n / 10);
}
putchar(n % 10 + '0');
}
void print(const char *format, ...)
{
va_list arg;
va_start(arg, format);
while (*format)
{
switch (*format)
{
case 's':
{
char *ret = va_arg(arg, char*);
while (*ret)
{
putchar(*ret);
ret++;
}
}
break;
case 'd':
{
int ret = va_arg(arg, int);
show(ret);
}
break;
case 'c':
{
int ch = va_arg(arg, char);
putchar(ch);
}
break;
default:
putchar(*format);
break;
}
format++;
}
}
int main()
{
print("d s c\n", 100, "hehe", 'w');
return 0;
}
总结
可变参数列表的第一个参数是不一定要传的,可变参数列表,在调用函数时,简化了传参的数量,使函数的适应能力更强。可变参数的实现过程就是使用宏的封装。只要完成替换,就可以自行分析了。
如有不足之处,欢迎指正!!