可变参数列表解析

来源:互联网 发布:神木法院淘宝网下载 编辑:程序博客网 时间:2024/06/07 06:39

可变”参数列表解析

函数其参数在大多数情况下都是确定的,但是在某些时候,其参数却是可变的。今天我就来简单做一下可变参数列表的解析。
什么是可变参数?如果留心的话可以发现,就库函数里面有些大家常用的均可接受1个以上的任意多个参数。
比如printf函数:

下来先看一个简单的例子,使用可变参数,实现函数求未知参数部分n个数的平均值:
#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(){printf("%d\n", average(3, 5, 6, 7));printf("%d\n", average(5, 1, 2, 3, 4, 5));system("pause");return 0;}
上面的函数参数即为可变参数,其个数为不确定个。
此程序运行结果为:

从average函数内部开始分析,在VS的原码处可以看到:

1.  va_list :  其本身为类型定义: 
typedef  char *  va_list 其意义为:typedef for pointer to list of arguments defined in STDIO.Htypedef用于指向STDIO.H中定义的参数列表的指针)

2.  va_start(arg, n) :  其本身为宏,在函数预处理阶段会进行替换:
#define _INTSIZEOF(n)  ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
宏 _INTSIZEOF(n) ,目的在于把sizeof(n)的结果变成至少是sizeof(int)的整倍数,用来在结构中实现按int的倍数对齐。

#define va_start(arg, n)  (arg = (va_list)&n + _INTSIZEOF(n))
宏 va_start(ap,v) ,其意义在于初始化arg,并找到参数n的地址,并由此地址让arg指向未知参数部分的第一个参数即指向参数n其下一个地址。

3.  va_arg(arg, n) :  其本身为宏,在函数预处理阶段会进行替换:
#define va_arg(arg, t)  ( *(t *)((arg += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
宏 va_arg(arg, t),接受两个参数:va_list变量和参数列表中下一个参数的类型。其操作意义在于:
在栈帧空间中,先让arg+=_INTSIZEOF(t),即让arg下移,令arg指向未知参数部分第二个参数的地址;
而后再给(arg+=_INTSIZEOF(t))-4,此时这个表达式即又指向了未知参数部分的第一个参数;
再给其(t *),强制转换为t型指针;
然后解引用,即得到了未知参数部分第一个参数的值。
再进下一次循环运算时,arg此时指向的已是未知参数部分第二个参数。(我上一篇博客《浅谈函数栈帧》中有分析函数传参过程及参数在栈帧中的分布,看过的话可以很容易理解此处代码的意义)

4. va_end(arg) : 其本身为宏,在函数预处理阶段会进行替换:
#define va_end(arg) ( arg = (va_list)0 )
宏 va_end(arg) 其意义在于在arg完成调用后,令arg为空指针,保证了程序的安全性。

那么,对上面的例子,用宏进行替换后,其运行又是如何?
来看下面代码:
//#define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))//#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )//#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )//#define va_end(ap) ( ap = (va_list)0 )
#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 = (char *)&n + 4;for (i = 0; i < n; i++){//sum += va_arg(arg, int);sum += (*(int *)((arg += 4) - 4));}
//va_end(arg); arg = (char*)0;
return sum / n;}int main(){printf("%d\n", average(3, 5, 6, 7));printf("%d\n", average(5, 1, 2, 3, 4, 5));system("pause");return 0;}
其运行及如果如下:

 针对上面代码再进行分析既有:


利用可变参数模拟实现printf函数:
#include <stdio.h>#include<stdarg.h>void dispaly(int num){if (num > 9){dispaly(num / 9);}putchar(num % 10 + '0');}void my_printf(const char* format, ...){va_list arg;va_start(arg, format);while (*format != '\0'){switch (*format){case 'd':{int ret = va_arg(arg, int);dispaly(ret);}break;case 's':{char* str = va_arg(arg, char*);while (*str){putchar(*str);str++;}}break;case  'c':{ char word = va_arg(arg, char); putchar(word);}break;default:putchar(*format);break;}format++;}va_end(arg);}int main(){int a = 10;char arr[] = "abcdef";char c = '1';my_printf("s d c", arr, a, c);system("pause");return 0;}
可变参数也存在限制,如下:
1. 可变参数必须从头到尾逐个访问。不可能从中间开始访问。
2. 为了确定其参数地址,可变参数列表中至少有一个命名参数。如果连一个命名参数都没有,就无法使用va_start。
3. 这些宏是无法直接判断实际存在参数的数量和每个参数的是类型。
4. 如果在va_arg中指定了错误的类型,那么其后果是不可预测的。
5. 如果参数列表中有多个命名参数,则在使用va_start时,取的应该是最后边的命名参数的地址。

以上即为个人对可变参数列表的解析,如有错误,敬请指正!