可变参数源码剖析

前言

可变参数,顾名思义即参数类型不确定,参数个数不确定(只是表面上个数不确定,实际上还是需要直接或者间接的将参数个数传入)。可变参数的应用场景非常多,例如:求n个数字之和,如果写成普通函数,那么将永远也实现不了这个函数的功能,如果写成可变参数的话就会变得非常简单,不至于出现代码冗余。我们最常用的printf()就是一个实现了可变参数的函数,这种函数基本上不可能写成常规函数,可变参数便成最佳选择!printf函数是个经典的可变参数的例子!

示例

虽然我们对像printf()这样的库函数是如何实现可变参数原理不是很了解,但是我们可以从最简单的示例入手,接下来这个例子演示了如何利用可变参数求n个数字之和:

 1#define _CRT_SECURE_NO_WARNINGS
 2#include<stdio.h>
 3#include<stdlib.h>
 4#include<stdarg.h>
 5 
 6int get_add(int num, ...){
 7	va_list arg;
 8	va_start(arg, num);
 9	int ret = 0;
10	int i = 0;
11	for (i = 0; i < num; i++){
12		ret += va_arg(arg, int);
13	}
14	return ret;
15}
16 
17int main(void){
18	int ret = get_add(3, 10, 10, 10);  //第一个参数为后面的参数个数
19	printf("ret = %d\n",ret);
20	system("pause");
21	return 0;
22}

分析

va_list

1typedef char *  va_list;

先看看 va_list 的类型,从源码中我们看到 va_list 其实是一个 char * 类型 ,这就像是可变参数列表的表头一样

va_start

接下来看看 va_start(arg,num) ,这里把上面得到的字符指针,后移动4个字节,就是跳过num的内存地址

1#define va_start _crt_va_start
2#define _crt_va_start(ap,v)  ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
3#define _ADDRESSOF(v)   ( &(v) )

通过 va_arg(arg, int) 是怎么拿到后面的参数的呢?ap表示可变参数指针,而t表示数据类型。使用 ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1)) ,传入char,float,double等小于4字节的类型,都会返回4。相应的传入如果类型大小是,5,6,7的话,则返回8。即返回当前一组数中靠近4的倍数的值。 ( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 该表达式先让指针ap加上4字节的大小,再把减去4字节大小处所对应的值返回。用t强制类型转换,再解引用,注意此处 t是传入参数的数据类型

va_end

1#define va_end _crt_va_end
2#define _crt_va_end(ap)      ( ap = (va_list)0 )

该语句把整型0强制转换为字符0,然后传给指针ap,由此可知将ap指向空!

从第一个参数就可以访问到后面的参数,这比较依赖于函数栈帧,后面的参数先压栈,我们只需要得到最后一个压栈的参数得出参数的总体个数和类型,这样就可以逐个访问到后面的参数了,这就是可变参数的实现原理!

结语

可变参数让函数的灵活性大大增加,避免了冗长的函数,也实现了普通函数所不能达到的功能,需要注意的是:有时候第一个参数显示的告诉了后面的参数个数和类型,有时候只是隐式的告诉参数个数和类型(就比如printf()函数一样),在使用可变参数构造自己的函数的时候务必想清楚是否能根据第一个参数正确访问到后面的参数!