printf 函数可能是学C语言遇到的第一个函数,这必然是可以接收变长参数的函数,才可以被用来 printf(“Hello World!\n”), 被用来 printf(“%d\n”, n) …… 在可变长参数的函数实现中,有几个宏是很重要的, va_start, va_arg, va_end.
在 redis 的代码中,看到了一段用来写日志的可接受变长参数的函数的实现,代码还是比较简单的,实现的重点在代码的 15 ~ 17 这三行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | void redisLog(int level, const char *fmt, ...) { const int syslogLevelMap[] = { LOG_DEBUG, LOG_INFO, LOG_NOTICE, LOG_WARNING }; const char *c = ".-*#"; time_t now = time(NULL); va_list ap; FILE *fp; char buf[64]; char msg[REDIS_MAX_LOGMSG_LEN]; if (level < server.verbosity) return; fp = (server.logfile == NULL) ? stdout : fopen(server.logfile,"a"); if (!fp) return; va_start(ap, fmt); vsnprintf(msg, sizeof(msg), fmt, ap); va_end(ap); strftime(buf,sizeof(buf),"%d %b %H:%M:%S",localtime(&now)); fprintf(fp,"[%d] %s %c %s\n",(int)getpid(),buf,c[level],msg); fflush(fp); if (server.logfile) fclose(fp); if (server.syslog_enabled) syslog(syslogLevelMap[level], "%s", msg); } |
在上面 redis 的代码中,用到了 va_start, vsnprintf, va_end, 一般的实现应该是 va_start 和 va_end 是宏,vsnprintf 是一个函数。
在描述这几个宏或者函数的作用之前,有必要知道一下函数的参数传递的方式。调用一个函数时,传递若干参数,在正常情况下,按照代码中参数的顺序从右到左依次入栈。
要实现可以接收变长的参数,问题就是怎样确定这些参数的地址。像上面 redis 的代码中,在栈顶的参数也就是第一个参数 level 的地址是可以确定的,第二个参数 fmt 的地址同样可以确定,在 fmt 之后,是不确定的参数,如果跳过 fmt 变量占用的空间大小,就知道了第三个参数,再跳过第三个参数……下面开始说下真正的实现的方法原理。
va_start 宏的实现如下
#define va_start(ap,v) (ap = (va_list)&v + _INTSIZEOF(v))va_start(ap, fmt) 这行代码就是把 fmt 作为可变长参数的“头”,可变长参数的列表通过 ap 来获取,va_list 实际只是一个普通的指针,获取 fmt 之后的参数,就是要开始“跳”。
vsnprintf 函数类似于 snprintf, 把指定最多 sizeof(msg) 大小的内容输出到 msg 的那块空间里去,内容的格式由 fmt 控制,具体内容是由 ap 变量决定。在这个函数里需要实现这样的功能:1.知道 fmt 后面有多少参数,2.知道这些参数的类型是什么。这样才能知道每次跳多少才能得到正确的参数,并且要知道跳到什么时候是个头,千万不要越界。
一开始学C语言的时候,被告知 printf 的格式里, %d 表示整型, %s 表示字符串,%c 表示单个字符…… 稍微动下脑子应该知道为啥要有这么些个“规矩”了,有了这样一个协议,就可以通过分析 fmt 这个字符串中的 ‘d’, ‘s’, ‘c’, ‘f’ 等等字符(或者更复杂的 %.3f 这种格式)去很好的实现前面说到的两个功能。
根据得到的参数个数,然后是一个循环,每次循环就是取到一个参数,跳过这个参数,对取到的参数进行处理。在 C语言的实现中, va_arg 这个宏是专门来取这个参数并跳过这个参数的,它的实现(之一)是:
#define va_arg(ap,t) (*(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)))ap就是参数列表的地址, t 是参数的类型(int, double等)。上面的宏定义看上去怪怪的,乍看上去好像加了又减同一个值,稍微仔细看下就明白了。当前参数的地址其实就是 ap 当前指的地址,同时这个宏把 ap 的地址加上了 t 类型的大小,也就是跳过了当前参数到了下一个参数。
就这样,printf 函数的实现,或者上面 redis 这一部分代码的实现,应该了然了。
最后,还有 va_end 这个宏,上面已经把工作全做完了,va_end 是干嘛用的?实际上在一些 C语言实现中,这个宏就是几乎什么都不做的
#define va_end(ap) (ap = (va_list)0)《C缺陷与陷阱》里有一段
在大多数C实现上,调用va_end与否并无区别。但是,某些版本的va_start宏为了方便对va_list的遍历,就给参数列表动态分配内存。
这样一种C实现很可能利用va_end宏来释放此前动态分配的内存。如果忘记调用宏va_end,最后得到的程序可能在某些机型上没有问题,而在另一些机型上则发生“内存泄露”。
最后一点,上面碰到的INISIZEOF这个宏的定义:
#define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) – 1) & ~(sizeof(int) – 1))
这个宏让“跳”的幅度都是4的倍数(或者8,不同平台结果不一样),至于原因,涉及到参数传递时类型的自动提升,“栈帧”的大小,可以看这里http://hi.baidu.com/xyk34/blog/item/b4fd61351e63b50391ef3915.html
参考内容:
也谈C语言变长参数
可变长参数列表误区与陷阱——va_arg不可接受的类型
可变长参数列表误区与陷阱——va_end是必须的吗?
C语言中变长参数中的一个有趣的现象。关于默认参数提升