A_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。
va_list 用法示例:
#include
#include
#include
int add(int arg,...);
int main(int arg,char* args[])
{
int rel = add(4,10,10,10,10);
printf("结果为 %d \r\n",rel);
return 0;
}
//操作系统只给我们定义了几个宏,包含:va_list va_start() va_arg() va_end()
//注意这里我们要做的操作就是判断参数的个数,然后进行不同的处理,这是用户需要做的
int add(int arg,...) //以前还有这样的变参函数:void test(const char * format, ...);由此可以看出第一个固定参数的形式不一定是
//int类型,由此又可以看出第一变参的作用是要么是变参按什么格式来输出,要么是代表程序员要输入的变参的个
//数,将来循环输出时作为一个限制条件譬如: while(arg > 0)
{
int ret = 0;
va_list list;
va_start(list,arg);
while(arg > 0)
{
ret += va_arg(list,int); //va_arg()的返回值是整形int的数,所以可以赋值给整形变量ret进行累加。
arg--;
}
va_end(list);
return ret;
}
VA_LIST的用法:
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针;
(2)然后用VA_START宏初始化变量刚定义的VA_LIST变量;
(3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数);
(4)最后用VA_END宏结束可变参数的获取。
上面是va_list的具体用法,下面讲解一下va_list各个语句含义(如上示例黑体部分)和va_list的实现。
//可变参数是由宏实现的,但是由于硬件平台的不同,编译器的不同,宏的定义也不相同,下面是VC6.0中x86平台的定义 :
typedef char * va_list; /* TC中定义为void* */
#define _INTSIZEOF(n) ((sizeof(n)+sizeof(int)-1)&~(sizeof(int) - 1) ) /*为了满足需要内存对齐的系统*/
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) ) /*ap指向第一个变参的位置,即将第一个变参的地址赋予ap*/
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) /*获取变参的具体内容,t为变参的类型,如有多个参数,则通过移动ap的指针来获得变参的地址,从而获得内容*/
#define va_end(ap) ( ap = (va_list)0 ) /*清空va_list,即结束变参的获取*/
va_list ap ; 定义一个va_list变量ap
va_start(ap,v) ;执行ap = (va_list)&v + _INTSIZEOF(v),ap指向参数v之后的那个参数的地址,即 ap指向第一个可变参数在堆栈的地址(由此说明参数v是最靠近变参列表中的参数的一个固定参数,也就是...符号前面的一个固定参数)。
va_arg(ap,t) , ( (t )((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )取出当前ap指针所指的值,并使ap指向下一个参数。 ap+= sizeof(t类型),让ap指向下一个参数的地址。(INTSIZEOF(t)-INTSIZEOF(t)不是抵消了吗?从数学的含义来理解确实是抵消了,但是c 语言设计时不是这样的,而是要将来从参数t的地址处跳转到下一个参数的地址处,就要将执行参数t的指针ap加上参数占用的内存空间字节数,参数t的占用内存的空间字节数如何带到的呢?就是通过一加一减的过程,譬如原来ap= 2000,如加n后等于200n,再减去n,即2005-n= 2000(2000的地址值是运算后的值),虽然ap地址值还是2000,但是如果将运算后的地址值2000转换成指针的话即(t*)2000,那么这个指针t的占用的字节空间的大小就是参数t占用的内存字节数,所以说只要知道了某个参数譬如k(这个参数是程序员编程时(即调用函数时)就确定好的,不用担心它不是看不见的,而是看得见的参数,这样的参数可以是若干个,可以是10 ,20, 50个等等,调用函数之前是不确定的,是未知的,不知道将来调用此函数的程序员使用几个这样的参数,也就是使用几个这样的变参数,所以在定义此函数时使用了符号三个点...来表示不确定的参数数量。但是在调用此函数时这三个点...就换成了确定的参数个数和参数名字譬如变参列表:(int r, char w, double b, float c,int a, char m, float s),当然变参前面还要有至少两个的固定参数。为什么要有固定参数?我暂时还没有搞清楚。下来再研究........:作用是将来输出变参时按照什么格式(按"%s_%d"输出两个变参的内容)或者输出的参数的个数(int arg ,此处的变量arg 就是将来调用函数时输入的整形数字,它代表程序员一定会在调用函数时输入arg个变参。),这是程序员在调用函数时自己确定的一项内容。.。),就可以通过这种运算创建一个指针,通过移动这个指针就可以指向下一个参数了)然后返回ap-sizeof(t类型)的t类型指针,这正是第一个可变参数在堆栈里的地址。然后 用取得这个地址的内容。
va_end(ap) ; 清空va_list ap。
使用VA_LIST应该注意的问题:
(1)因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢,可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能地识别不同参数的个数和类型. 也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的.
(2)另外有一个问题,因为编译器对可变参数的函数的原型检查不够严格,对编程查错不利.不利于我们写出高质量的代码。
(3)由于参数的地址用于VA_START宏,所以参数不能声明为寄存器变量,或作为函数或数组类型。
好了,是时候表演真正的技术的,咱们去看看大神是如何使用可变参函数的(源代码参考libevent)
void
event_errx(int eval, const char *fmt, ...) //这里变参前面使用了两个固定参数:eval fmt.前面学习时是一个固定参数,
//这又是咋回事?难道固定参数的个数不一定吗?
{
va_list ap;
va_start(ap, fmt);
_warn_helper(_EVENT_LOG_ERR, NULL, fmt, ap);
va_end(ap);
event_exit(eval);//?????
}
static void
_warn_helper(int severity, const char *errstr, const char *fmt, va_list ap)
{
char buf[1024];
size_t len;
//如果不为null则进行字符串的拼接操作
if (fmt != NULL)
//字符串的拼接函数 参数分别为内存首地址,内存块的大小,要拼接的格式,和其后跟的参数
evutil_vsnprintf(buf, sizeof(buf), fmt, ap);
else
buf[0] = '\0';
if (errstr) { //errstr是一个变量,它是存储报错信息的 ,在哪里定义的?在前面定义的,它是如何存储
//内容的,上面没有代码啊?
len = strlen(buf);
//这里为什么减3呢,那是因为我们的格式上面有 :+ 空格 + \0
//其实这一段我们可以不用做,因为怎么输出也不会超的
if (len < sizeof(buf) - 3) {
evutil_snprintf(buf + len, sizeof(buf) - len, ": %s", errstr);
}
}
event_log(severity, buf); //这个函数又在哪里定义的?
}
OK!