【C语言】深度剖析可变参数列表源码

   C语言中可变参数是一个比较有意思的实现,通过将函数实现为可变参数的形式,可以使得函数接收1个以上的任意多个参数。我们先来举个栗子:

实现一个函数可以求任意个参数的平均值。

#define _CRT_SECURE_NO_WARNINGS 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);//2
	int avg2 = average(3, a, b, c);//2
	printf("%d\n", avg1);
	printf("%d\n", avg2);
	system("pause");
	return 0;
}

运行结果如下,没有问题。 

那么在代码里,我们会看到很多不认识的东西,接下来我来一 一解析。

首先是 va_list ,我们想知道它是什么意思,选中它,右击转到定义便可以查看它的定义。

 

很明显我们可以看到  va_list  在定义里是 char* 的重命名,那么我们就可以说va_list 替换的是char* ,即原代码可以替换成char* arg  。这就是说我们在这里创建了一个char*类型的指针变量arg 。va_list  arg-----> char*  arg

所以说va_list 其实是一个类型,为了表示可变参数,与其语法统一,所以写成这样。它的本质就是char*

 

扫描二维码关注公众号,回复: 2747681 查看本文章

接下来我们定义了一个va_start,同样右击转到定义。

我们可以看到在定义里,va_start是define定义的一个宏。在这里,_INTSIZEOF转到定义后我们发现他也是一个宏,表示如果括号里的n 所占字节大小是1~4字节,那么宏返回值是4,如果n所占字节是5~8字节,那么他返回8,如果n所占字节是9~12字节,那么他返回12,实际上就是向上取整。那么我们可以将我们的代码再次替换va_start(arg, n)----> arg = (char*)&n + _INTSIZEOF(n)------> arg = (char*)&n + 4

再来看这里的n 。n是一个地址,强制转换成一个char*类型的指针变量,又+4,这句代码到底什么意思呢?画个图看一下,之前写过的博客里讲过函数的调用栈帧,栈的使用时压栈,压栈从高地址往低地址走,但是往出拿的时候却是从低地址往高地址走。也就是说,先进后出,后进先出。那么n就是这个栗子里的3,也就是说 &n 取出来的就是3 的地址。那么在代码arg = (char*)&n + 4里就可以解释清了,这里的n被强制转化为char*,指向3 的地址,并且+4就跳过3,指向后边的1的地址。那么我们可以说arg现在指向了未知参数列表的第一个参数。只要找到了第一个参数,那么后边的也就能找到了。

所以我们说va_start的作用是初始化va_start(arg, n) 指的是初始化arg为未知参数列表的第一个参数的地址,n表示未知参数列表的参数个数。

那么再往下走继续看:看到这句sum += va_arg(arg, int),后边又返回了sum/n。我们猜想这句代码是每调用一次va_arg(arg, int)取出一个参数,加在sum上,最后返回sum/n计算的平均值。同样的转到定义来验证下我们的猜想。

 

va_arg(arg, int)转到定义我们可以发现,它也是define定义的一个宏,那我们也对代码进行替换,变成   sum += va_arg(arg, int)      ------>      sum += (*(int *)((arg+= _INTSIZEOF(int)) - _INTSIZEOF(int)))       ----- >     sum += (*(int *)((arg+= 4) - 4)

我们来分析下sum += (*(int *)((arg+= 4) - 4)的意思。还是用同样的图,刚刚经过上一步的分析,我们知道arg此时指向的是未知参数的第一个参数1的地址。那么arg+=4,就是跳过1的地址,拿到2的地址。那么arg就变成了指向2的地址。但是代码((arg+=4)-  4  )里又-4,又跳回1的地址,但是留给arg的仍是2的地址。那么给sum赋值的是强制转化为int的1的地址再解引用,也就是数字1。等到循环再次过来,arg+=4,这次arg指向3的地址,但是表达式赋值给sum的有事数字2。所以这个表达式十分的巧妙,一句代码完成两步操作。

所以这个va_arg是一个宏,接受两个参数,va_list变量和参数列表中下一个参数的类型,返回第一个参数并指向第二个参数。

sum += (*(int *)((arg+= 4) - 4)的整体意思就是将参数一个个拿出强制类型转换并加在sum上。

接下来我们再来看va_end ( arg )同样转到定义如下,我们再次替换得到va_end(arg)    ------>    arg = (va_list)0    ------>   arg= (char*)0

 

其实在这里就是把0强制类型转换赋给了arg,因为arg始终维护的是未知参数列表的参数的位置,现在循环结束,已经没用了,就把它还原成空指针。

所以说va_end()就是用来把一个值赋值成空指针的。

到现在为止,上边的求平均数代码已经全部解释清楚。那来总结一下可变参数列表的知识:

小结

  • 声明一个va_list类型的变量arg,它用于访问参数列表的未确定部分。
  • 这个变量是调用va_start来初始化的。它的第一个参数是va_list的变量名,第二个参数是省略号前最后一个有名字的参数。初始化过程把arg变量设置为指向可变参数部分的第一个参数。
  • 为了访问参数,需要使用va_arg,这个宏接受两个参数:va_list变量和参数列表中下一个参数的类型。在这个例子中,所有的可变参数都是整形。va_arg返回这个参数的值,并使va_arg指向下一个可变参数。 
  • 最后,访问完毕最后一个参数之后需要调用va_end。 

可变参数的限制

  • 可变参数必须兴头到尾逐个访问。如果你在访问了几个可变参数列表之后想半途终止,这是可以的,但是,如果一开始就要访问中间的参数,这是不行的。
  • 参数列表中至少要有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
  • 这些宏无法判断实际存在参数的数量。
  • 这些宏无法判断每个参数的类型。
  • 如果在va_arg中指定了错误的类型,那么其后果是不可预测的。

 最后再上一次代码,里面有注释和替换部分,以供参考。

#define _CRT_SECURE_NO_WARNINGS 1
#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);//初始化arg为未知参数列表的第一个参数的地址
	//arg = (char*)&n + 4;//替换之后的结果
    //#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
	for (i = 0; i < n; i++)
	{
		sum += va_arg(arg, int);
		//sum += (*(int *)((arg+= 4) - 4);//替换后的结果
		//sum += (*(int *)((arg+= _INTSIZEOF(int)) - _INTSIZEOF(int)));
		//#define _crt_va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )

	}
	return sum / n;
	va_end(arg);
	// arg= (char*)0 
    //#define _crt_va_end(ap)      ( ap = (va_list)0 )
}

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("%d\n", avg1);
	printf("%d\n", avg2);
	system("pause");
	return 0;
}

猜你喜欢

转载自blog.csdn.net/Miss_Monster/article/details/81214075