可变参数列表实现机制与printf()函数源码分析
包含有可变参数列表的函数(printf函数族、scanf函数族等),可以说是我们编程中接触最多的。但是关于可变参数列表的实现机制,却鲜有初学者去了解,即使是使用C/C++好几年的人也不全都了解。在秋招时,被问到printf()可变参数列表的实现机制,虽然之前有听说过有关va_list,但是没有做深入的了解,因此第一道题就被问死了。今天查找有关源码和相关资料,将该知识点进行整理,也算是亡羊补牢为时未晚。
一、glibc-2.21与VC6.0中printf()的源码:
在glibc-2.21中,printf()的实现如下:
int printf (const char *format, ...)
{
va_list arg;
int done;
va_start (arg, format);
done = vfprintf (stdout, format, arg);
va_end (arg);
return done;
}
而在VC6.0源码中,实现如下:
//C语言默认的调用约定是_cdecl而不是_stdcall。
//多数情况下,二者均可以使用,但此处只能使用_cdecl,不能用_stdcall
//_stdcall是由被调用函数清理堆栈(内平栈),而在不知道参数数量的时候,被调用者无法清理。
//_cdecl则是调用者清理堆栈(外平栈),调用者可以清楚地知道参数个数,因此函数返回后可以由调用者清理堆栈。
//换句话说,_stdcall不支持可变数量的参数,而_cdecl支持可变量参数。
int __cdecl printf (const char *format, ...)
/*
* stdout 'PRINT', 'F'ormatted
*/
{
//VC6.0中实现看似复杂,实际上:
//_lock_str2()、_stbuf()、_ftbuf()、_unlock_str2()是为了线程安全做的处理,可以忽略
va_list arglist;//va_list即char *
int buffing;
int retval;
va_start(arglist, format);
_ASSERTE(format != NULL);//判空,如果为空则出错,与assert()无异
_lock_str2(1, stdout);
buffing = _stbuf(stdout);
retval = _output(stdout,format,arglist);
_ftbuf(buffing, stdout);
_unlock_str2(1, stdout);
return(retval);
}
在以上两种不同平台的实现版本中(具体代码可参考文后“参考资料”),我们都看到va_list、va_start()等相同的部分。va_list、va-start()、va_arg()、va_end()是一个宏定义三个宏函数,这四个宏的存在,才使得可变参数列表能够实现。正如这四个宏所在的头文件所述:This file defines ANSI-style macros for accessing arguments of functions which take a variable number of arguments.(该文件定义了ANSI风格的宏,用于访问需要可变数量参数的函数的参数。)关于va_start()等的分布我们可以在整个VC6.0的SRC中搜索,以下是一小部分搜索结果(可见该宏在C标准库的实现中起到至关重要的作用):
那么除了这四个宏以外,vfprintf()函数和_output()函数则分别实现了glibc-2.21和VC6.0两种版本里对于可变参数列表的处理(函数内部是一个大的while()循环)。vfprintf()和_output()是printf()函数对于参数细节处理实现的核心,包含了printf函数族所有函数实现的代码。虽然是核心部分,但是由于代码量过大(仅仅一个函数实现近千行,且对于平台、版本等的预处理条件过多),且_outpur()和vfprintf()是对于printf整个函数族而言,所以我们的重点只放在va_list、va_start()、va_arg()、va_end()上来分析总结。以下列出_output()的变量定义(读者可根据定义的变量信息来大概猜测_output()的内容,也可以自己去看output.c源文件):
int hexadd; /* offset to add to number to get 'a'..'f' */
TCHAR ch; /* character just read */
int flags; /* flag word -- see #defines above for flag values */
enum STATE state; /* current state */
enum CHARTYPE chclass; /* class of current character */
int radix; /* current conversion radix */
int charsout; /* characters currently written so far, -1 = IO error */
int fldwidth; /* selected field width -- 0 means default */
int precision; /* selected precision -- -1 means default */
TCHAR prefix[2]; /* numeric prefix -- up to two characters */
int prefixlen; /* length of prefix -- 0 means no prefix */
int capexp; /* non-zero = 'E' exponent signifient, zero = 'e' */
int no_output; /* non-zero = prodcue no output for this specifier */
union {
char *sz; /* pointer text to be printed, not zero terminated */
wchar_t *wz;
} text;
int textlen; /* length of the text in bytes/wchars to be printed.
textlen is in multibyte or wide chars if _UNICODE */
union {
char sz[BUFFERSIZE];
#ifdef _UNICODE
wchar_t wz[BUFFERSIZE];
#endif /* _UNICODE */
} buffer;
wchar_t wchar; /* temp wchar_t */
int bufferiswide; /* non-zero = buffer contains wide chars already */
char *heapbuf; /* non-zero = test.sz using heap buffer to be freed */
textlen = 0; /* no text yet */
charsout = 0; /* no characters written yet */
state = ST_NORMAL; /* starting state */
heapbuf = NULL; /* not using heap-allocated buffer */
二、可变参数列表具体分析:
1、va_list、va_start、va_arg、va_end:
//stdarg.h
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
//我们要考虑内存对齐,所以不能简单的用sizeof(v)
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
在printf()实现的代码中:
va_start(arglist, format);
retval = _output(stdout,format,arglist);//或done = vfprintf (stdout, format, arg);
va_end(arglist);
三行是核心部分。
va_list是char *类型,即arglist指向参数列表首字节,在实际调取参数时要根据参数类型强制转换。
va_start()是初始化指向变参列表(即第二个参数)的指针arglist。为获取可变数目参数的函数的参数提供一种便捷手段。设置arglist为指向传给函数参数列表中的第一个可选参数的指针,且该参数必须是va_list类型。format是在参数列表中第一个可选参数前的必选参数。
va_arg()是用来确定后面每个参数的位置。因此va_arg()会在_output()之中多次使用。返回由arglist所指向的参数的值,且自增指向下一个参数的地址。t(type)为当前参数的类型,用来计算该参数的长度,确定下一个参数的起始位置。它可以在函数中应用多次,直到得到函数的所有参数为止,但必须在宏va_start()后面调用。
va_end()则是在获取所有的参数后,设置指针arglist为null。
关于所有参数所在栈中的位置以及va_start()和va_arg()的作用如下图:
注意:va_arg()执行完毕后的返回值和arglist不是同一个地址。返回值为当前参数地址(旧的arglist(ap)),arglist为下一个参数地址(新的arglist(ap))。
2、关于_output()函数其大体简化功能如下所示:
int __cdecl _output(FILE * stdout,const char * format,va_list ap)
{
char ch;
int charsout = 0;//目前写入的字符数
//循环读取format字符串的每一个字符,当读取到'\0'时,即字符串末尾则循环结束
//_T是一个宏,作用是让程序支持Unicode编码,因为Windows使用两种字符集:ANSI和UNICODE
while((ch = *format++) != _T('\0') && charsout >= 0){
if(ch!=‘%’) {
将ch写入到文件stdout中;
continue;
}
switch(*format):
case ’c’:{//%c
将ap所指向的内容以字符形式写入文件stdout;
ap指向下一个参数;
break;
}
case ’d’:{//%d
……;
format++;break;
}
case ‘f’:{//%f
……;
format++;break;
}
…………//%x、%p...
}
return charsout;/* return value = number of characters written */
}
3、自己的可变参数列表的函数:
只要遵循:
①包含stdarg.h头文件(或者自己写va_list、va_start()、va_arg()、va_end())
②va_list定义指针;
③va_start()初始化指针;
④va_arg()通过指针调用参数;
⑤va_end()指针置NULL,内存释放;
的使用顺序,我们自己也可以制作自己的可变参数列表函数。
如下是一个能够处理单个字符和字符串输出的简易的printf()函数:
#include <stdio.h>
//step ①
#include <stdarg.h>
void myprintf(const char *format, ...){
//step ②
va_list ap;
char ch;
//step ③
va_start(ap, format);
while(ch = *format++){
if(ch!='%'){
putchar(ch);
continue;
}
switch(*format){
case 'c':{
//step ④
char ch1 = va_arg(ap, char);
putchar(ch1);
format++;
break;
}
case 's':{
//step ④
char *p = va_arg(ap, char *);
fputs(p,stdout);
format++;
break;
}
}
}
//step ⑤
va_end(ap);
}
int main(void)
{
myprintf("%c\t%s\t%c\n",'A',"hello",'B');
return 0;
}
关于程序执行过程中,可变参数列表的栈的变化,我们分别用不同个数参数来测试:
四个参数(栈大小为24Byte,可以看到回收栈资源属于调用者回收,即_cdecl的外平栈):
对于这个入栈过程,如下所示:
三个参数(栈大小为12Byte):
参考资料:
glibc-2.21、VC6.0SRC
printf内部实现
_INTSIZEOF(n)
上一篇: 算法设计与分析_递归与分治
下一篇: SpringBoot 文件上传