在 Linux 编程中经常用到 printf 函数,其函数定义如下:
/* Write formatted output to stdout.
This function is a possible cancellation point and therefore not
marked with __THROW. */
extern int printf (__const char *__restrict __format, ...);
这是最典型的C标准 —— 未知个数参数传递的函数模型,"printf" 传递的参数个数可以自由控制:
int sum = 100;
char p[] = "Test";
printf("%s", p);
printf("%s %d", p, sum);
原理解析
1、函数参数的内存存放格式。
函数参数是以数据结构的方式:栈的形式存取,从右往左进栈,即最后一个参数先进栈,如:
int func(int m, char n, double k);
参数 "k" 先进栈,然后是 "n" -> "m",即从栈底往上次序是 k->n->m ,所以,栈底是高地址,栈顶是低地址。也符合栈的后进先出的概念。
2、调用。
调用参数时从栈顶开始一直寻址至栈底,即次序为 m->n->k,因此,理论上说,只要知道 m 的地址,顺手牵羊,哦不,顺藤摸瓜,即可寻到其他函数参数。
stdarg.h 中宏定义的原型
#define va_start(v,l) __builtin_va_start(v,l)
#define va_end(v) __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)
#define va_copy(d,s) __builtin_va_copy(d,s)
等同于
va_start(va_list p, fmt);
va_end(va_list p);
type va_arg(va_list p, type);
va_copy(va_list dest, va_list source);
假如定义一个函数
int printb(char *fmt, ...);
"va_list" 定义的是一个指向参数列表的指针(类型可能是"char *"或者"void *"),"fmt" 指向输入参数的第一个参数,此参数应必须存在,否则无法获取后面的参数。"type"是数据类型,可取值 "int"、"char *"类型(其他类型尚需实测)。
下面是一个简单的例子
#include <stdio.h>
#include <stdarg.h>
static int MAX_ARGS=4;
int printb(char *fmt, ...)
{
char buf[1024];
char *bb=NULL;
va_list ap;
int argno = 0;
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
while(fmt!=0 && argno<MAX_ARGS)
{
bb = va_arg(ap, char*);
printf("bb=%s ap=%p \n",bb, ap);
argno ++;
}
va_end(ap);
printf("\nThere are your enter strings:%s\n", buf);
return 0;
}
int main()
{
printb("Bingwu %s %s %s %s", "WHAT", __FILE__, __FUNCTION__, "Oh no");
return 0;
}
特别注意,在使用 "va_arg" 宏时一定要留心输入参数的类型,如上述例子中把main函数改成
int main()
{
printb("Bingwu %s %s %d %s", "WHAT", __FILE__, __LINE__, "Oh no");
return 0;
}
那么整个程序的运行将会产生段错误!因为 "va_arg" 指定了 "char *" 类型,那么将无法容忍其他类型的输入!所以,除非是刚性需求,一般使用只用如下模型即可
va_start(ap, fmt);
vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
va_start 与 va_end 应总是成对出现,方能符合 初始化->其他调用->去初始化 的安全性原则。