参数可变函数又称VA函数,例如printf,scanf,exec。
1.举例:
//fun:打印n后面参数的值
void fun(int n, ...);
int main()
{
int part1 = 128;
int part2 = 256;
int part3 = 512;
fun(part1, part2, part3);
return 0;
}
void fun(int part1, ...)
{
int *p = &part1; //获取part1的地址
printf("%d\n", *++p);//打印part2的值
printf("%d\n", *++p);//打印part3的值
}
C默认的函数调用规范是_cdecl,即所有参数从右到左依次入栈,严格的fun声明应该是:
void _cdecl fun(int n, ...);
在main中,调用fun函数前先将参数入栈,入栈的顺序是:
push part3
push part2
push part1
然后调用fun,执行函数体代码。
先压栈的参数会放在高地址,因为栈是由高地址往低地址生长的,所以part1,part2,part3在内存中的顺序将会是:
0xFE6C part3
0xFE70 part2
0xFE74 part1
这样可以通过取得第一个参数的地址&part1和++操作分别访问到后面的参数,但这必须是_cdecl函数调用规范,例如printf系列的库函数(sprintf,fprintf)都是可以接受可变参数的函数。假设有下面一条语句:
printf("%d %d %d\n", m, n, k);
可以看出,我们可以通过第一个参数得到参数的个数(格式符的数目)和类型(例如%d),这就是为什么printf("%d %d\n", m, n, k)可以成功执行,而printf("%d %d %d\n", m, n)会失败的原因了。传递的参数如果多于格式符的个数可以忽略掉(在Linux下能正常执行),但是少于就会出现访问越界(Linux下会报警告,但是仍然正常执行,越界的参数会是个随机值)。(执行自己程序的时候记得加./,例如./a.out)
2.采用varargs宏来编写支持可变参数列表的函数,在ASCI C标准里,这些宏包含在stdarg.h。
例如以下代码:
#include <stdio.h>
#include <stdarg.h>
void _cdecl fun(int n, ...);//可以不加_cdecl
int main(int argc, char *argv[])
{
int part1 = 128;
int part2 = 256;
int part3 = 512;
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%d\n", va_arg(ap, int));
printf("%d\n", va_arg(ap, int));
va_end(ap);
}
在Microsoft为VC提供的实现中,可以看到这样的定义:
#define _ADDRESSOF(v) (&(v))
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1))
typedef char* va_list;
#define va_start(ap,v) ( ap = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )//ap指向v下一个参数的地址
#define va_arg(ap,t) (*(t*)( (ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) //ap指向下一个参数的地址,但是返回的还 //是这个参数的值
#define va_end(ap) ( ap = (va_list)0 )
va_list 一个char型指针,每次单个字节寻址。
va_start 通过_INTSIZEOF计算参数类型大小,并让ap获得v后面参数对象的地址
va_arg ap指向参数列表中ap下一个参数对象,并返回ap之前指向的t类型参数对象
其中_INTSIZEOF(n)就是将n的长度化为int长度的整数倍。如果n是char类型,那么sizeof(n)=1,化为int长度的整数倍就应该是4,假设sizeof(n)=4m+k(m>=0,k=0,1,2,3),sizeof(n)+4-1=4m+(k+3),~(sizeof(int)-1)=~(4-1)=~(00000011b)=11111100b,这样任何数&~(sizeof(int)-1)后,后两位肯定是0,那就是4的倍数了,(4m+(k+3))&~(sizeof(int)-1)可以保证能存放4m+k。
将这些宏还原重写fun函数:
void fun(int part1, ...)
{
int part2, part3;
char *ap;//va_list ap;
ap = (char *)&part1+4;//va_start(ap, part1), ap指向part2
part2 = *(int *)(ap+=4 - 4);//返回part2,ap指向part3,part2=va_arg(ap,int)
part3 = *(int *)(ap+=4 - 4);//返回part2,ap指向part3,part3=va_arg(ap,int)
ap=(char *)0;//ap指向空,var_end(ap)
}
若part2为char,shor则会自动转化成int型,float则会自动转化成double型。
#include <stdio.h>
#include <stdarg.h>
void fun(int n, ...);
int main(int argc, char *argv[])
{
int part1 = 128;
float part2 = 256.0;//float
float part3 = 512.0;
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%f\n", va_arg(ap, double));//double正确,float错误,系统自动存储为double型
printf("%f\n", va_arg(ap, double));
va_end(ap);
}
#include <stdio.h>
#include <stdarg.h>
void fun(int n, ...);
int main(int argc, char *argv[])
{
int part1 = 128;
char part2 = 'c';//char
short part3 = 8;//short
fun(part1, part2, part3);
return 0;
}
void fun(int n, ...)
{
va_list ap;
va_start(ap, n);
printf("%c\n", va_arg(ap, int));//char自动存储为int,不能用char,即使这样,仍然可用%c输出字符
printf("%d\n", va_arg(ap, int));//short自动存储为int,不能用short
va_end(ap);
}
以上这些问题都是内存对齐造成的。
3.vprintf,vfprintf,vsprintf,vsnprintf,vasprintf格式化输出,有一个va_list参数
#include <stdio.h>
#include <stdarg.h>
int vprintf(const char *format, va_list ap);//格式化输出到标准输出,对应到printf
int vfprintf(FILE *stream, const char *format, va_list ap);//格式化输出到文件流,对应到fprintf
int vsprintf(char *s, const char *format, va_list ap);//格式化输出到字符串,对应到sprintf
int vsnprintf(char *s, size_t n, const char *format, va_list ap);//格式化输出固定长度到字符串,对应到snprintf
int vasprintf(char **ret, const char *format, va_list ap);//对应到asprintf
举例,用vprintf实现error:
#include <stdio.h>
#include <stdarg.h>
void error(char *function_name, char *format, ...)
{
va_list ap;
va_start(ap, format);
/*print out name of function causing error*/
(void)fprintf(stderr, "ERR in %s:", function_name);
/*print out remainder of message*/
(void)vfprintf(stderr, format, ap);
va_end(ap);
(void)abort();
}