可变参数列表,简单的理解一下就是该函数可以接受1个以上的任意多个参数(参数个数不确定)
刚开始我听到这个概念就觉得好神奇啊,从来没听过这么神奇的函数!
果然,打脸总是分分钟就会来的。啥叫没听说过,我朋友告诉我,学习c语言我写的第一个程序就用到了可变参数列表的概念
printf("hello world!");
没错,printf()函数就是一个典型的例子,让我们深度剖析一下。。。
对printf的定义是这样的,他的形参有一个这样的东西不知道大家有没有注意到:...(三个点)--> 这就表示该函数的参数列表是可变的,顾名思义就是传的参数可以是任意多个。
那既然是任意为什么还要有这个参数:const char *format --> 上面已经提到过了“1个以上任意多个参数”,那你要不写这个参数,我要说任意为0呢,这不就没参数了嘛。printf不就疯掉了,这是让我打印什么。。。
好啦,正经一点。。。
我们平时使用printf打印时这样写的
int a = 10;
char b = 'A';
float c = 0.0;
printf("%d %c %f\n", a, b, c);
我写过一篇关于栈帧的博客,我们知道,形参实例化是从右往左进行的,最接近被调用函数的参数(也就是地址最低的参数)其实是最左边的参数 --> 那么printf找到参数*format之后继续寻找上面的参数,可参数的个数是任意的,它怎么知道什么时候就把参数找完了呢?找到参数之后他存放了多大,又应该怎么取它的内容呢??
其实是这样的,printf里面有一些这样的符号%d、%c、%f、%s、%u......这些内容就可以告诉你传进了几个参数,每个参数又存放在多大空间中
类似于上面的例子,*format指向“%d %c %f\n”--> 即后面还有三个参数,第一个是整型它放在format上面的四个字节的空间中、第二个是字符型它放在整型上面的一个字节的空间中、第三个是浮点型它放在了字符型上面的四个字节的空间中(我简要画一下临时变量在栈中的分布)
这样就可以通过第一个参数确定其他所有参数的个数以及类型!!!
因此,可变参数能够被使用,需要确定两个信息:每个传入参数的类型信息;一共传入的参数个数
对上面栈的画法以及形参实例化(所有和栈有关的),有不明白的,戳这里:
https://blog.csdn.net/God_bless_TYY/article/details/80233081
接下来通过一个程序了解一下可变参数列表的概念
实现一个函数可以求任意个参数的平均值
#include <stdio.h>
#include <windows.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);
}
va_end(arg);
return sum / n;
}
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);
system("pause");
return 0;
}
以下是系统提供的宏定义:
1)va_list --> 用来声明一个va_list类型的变量arg,用于访问参数列表的不确定部分
typedef char * va_list;
2)va_start --> 对变量arg进行初始化。第一个参数是需要初始化的对象,第二个参数是省略号前最后一个有名字的参数。执行结束后可以使arg指向可变参数部分的第一个参数
#define va_start(ap, v) (ap = (va_list)&v + _INTSIZEOF(v))
含义是:先取出已知参数v的地址,再将该地址上移至第一个可变参数的位置
不知道大家有没有注意到这个东西:_INTSIZEOF(v),在此可先简单的理解为sizeof(v)。至于具体的区别,我放在下面的扩充内容里研究
3)va_arg --> 取出可变参数的值。第一个参数是变量arg,第二个参数是arg指向参数的类型。执行结束后可以取出该参数,并且将变量arg移至下一个可变参数的位置
#define va_arg(ap, t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZOF(t)))
含义是:先将ap移至需取出参数的下一个可变参数的位置,再取出需取出的参数
4)va_end --> 对变量arg置空
#define va_end(ap) (ap = (va_list)0)
从上面的例子可以看出,对可变参数我们也是有限制的:
1)可变参数必须从头到尾逐个访问。如果你在访问了几个可变参数之后想半途终止,这是可以的。但是,你想一开始就访问参数列表中间的参数,那是不可能的!
2)参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
3)这些宏是无法直接判断实际存在参数的数量。
4)这些宏无法判断每个参数的类型。
5)如果在va_arg中指定了错误的类型,那么其后果是不可预测的。
扩充部分:_INTSIZEOF(n) --> 内存对齐
_INTSIZEOF(n)它的作用就是实现内存对齐问题,它也是系统提供的一个宏定义,是这么写的:
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
注意:cpu每次读取内存是以整型的整数倍进行读取的,也就是说一次读取4字节(或者8字节、或者16字节、......)的内容。这就需要引入内存对齐的概念,当有些数据它所占的空间不足4字节的整数倍时,那就把它放大为4字节的整数倍(向上取整),这样做的目的是为了提高cpu读取内存的效率。
换句话说,当我只有一个字节的数据时,cpu一次还是访问四个字节的空间。因此我在上面画的printf的栈图,应该进行小小的修改
看!虽然临时变量b是个字符型它的值只占了一个字节,但仍然将它放在了四个字节的空间中,因为cpu走一步就是四字节的整数倍
了解了内存对齐这个概念之后,我们再来看看这个宏是怎么实现内存对齐的:
现在有x,y都是正整数,要把x向上取整为y的整数倍,我现将x写成ky+b的形式,其中0<=b<=y-1
这样的话,当b为0时,f(x) = ky,否则f(x) = (k+1)y
求整数倍,当b = 0时比较简单,x/y就可以求得k。但b不为0呢,如何通过x,y求得(k+1)?
直接用x/y得到的总是k,k+1是得不到的
那么现在只能考虑增大分子了:(x+m) / y = (ky+b+m) / y,这个m可以使得当b=0时结果为k,否则结果为k+1
这个问题就等价于,m要小于y,并且要足够大,大到能让最小的余数b=1加上它之后不再小于y
m<y && (m+1)>y --> m=y-1
现在回头来看宏定义的式子,(sizeof(n) + sizeof(int) - 1)这个部分就实现了x+m
~(sizeof(int) - 1)其实就是对3按位取反:1111 1111 1111 1100
这两个部分按位与的结果就实现了将sizeof(n)向上取整为sizeof(int)的整数倍