printf的实现原理
要了解变参函数的实现,首先我们的弄清楚几个问题:
1: 该函数有几个参数。
2: 该函数怎样去访问这些参数。
3: 在访问完成后,如何从堆栈中释放这些参数。
函数变参
对于c语言,它的调用规则遵循_cdedl调用规则。 在_cdedl规则中:
1. 参数从右到左依次入栈
2. 调用者负责清理堆栈
3. 参数的数量类型不会导致编译阶段的错误
printf的声明: int _cdecl printf(const char* format, …); // _cdecl是C和C++程序的缺省调用方式
栈由高地址向低地址生长,又参数从右到左依次入栈,所以栈的高地址是printf最右边的参数。以
printf("%d %f %c %s\n", 3, 5.40, A, "hello world");为例,其栈的结构如下:
在被调用的函数内部我怎么知道变参的类型是什么呢?对于printf函数来说,调用者通过第一个fmt参数中的%+格式字符的方式通知了被调用者(printf的实现者)。
格式解析
扫描format参数里的字符,如果是普通字符就打印输出,如果是%,就说明后面有可能是格式字符,需要进行检测,然后从栈低(其实是第一个参数的位置)弹出指定类型的数据,按照指定格式(十进制、十六进制、指定宽度、指定精度等等)进行输出。基本上是一个字符串解析的过程。
typedef char *va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) /* 1 */
#define va_start(va_list ap, format) ( ap = (va_list)&format+ _INTSIZEOF(format) ) /* 2 */
#define va_arg(va_list ap,type)
( *(type*)((ap += _INTSIZEOF(type)) -_INTSIZEOF(type)) ) /* 3 */
#define va_end(va_list ap) ( ap = (va_list)0 ) /* 4 */
1: 将sizeof(n)按sizeof(int)对齐
2: 初始化参数指针ap,将format右边第一个参数地址赋值给ap
3: type用来指名当前参数类型, 获得ap指向参数的值,同时使ap指向下一个参数
4: 在有些简单的实现中不起任何作用,在有些实现中可能会把ap改成无效值,这里把ap指针指向了 NULL
c标准要求在同一个函数中va_start 和va_end 要配对的出现。那么到现在,处理多参数函数的步骤就是
1:首先是要保证该函数至少有一个参数,同时用...参数申明函数是变参函数。
2:在函数内部以va_start(ap,format)宏初始化参数指针。
3:用va_arg(ap,type)从左到右逐个取参数值。
printf()格式转换的一般形式如下:
%[flags][width][.prec][type]
prec有一下几种情况:
正整数的最小位数
在浮点数中表示的小数位数
%g格式表示有效为的最大值
%s格式表示字符串的最大长度
若为*符号表示下个参数值为最大长度
width:为输出的最小长度,如果这个输出参数并非数值,而是*符号,则表示以下一个参数当做输出长度。
另附一些比较有用的链接: