“可变”参数列表解析
函数其参数在大多数情况下都是确定的,但是在某些时候,其参数却是可变的。今天我就来简单做一下可变参数列表的解析。
什么是可变参数?如果留心的话可以发现,就库函数里面有些大家常用的均可接受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.H
(
typedef用于指向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));
}
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;} 其运行及如果如下://va_end(arg); arg = (char*)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时,取的应该是最后边的命名参数的地址。
以上即为个人对可变参数列表的解析,如有错误,敬请指正!