printf函数
函数原型:int printf(const char *format, ...)
调用格式为:printf("<格式化字符串>", <参量表>);
功能:发送格式化输出到标准输出 stdout
变长参数实现思路
C语言支持变长参数函数(Variable Argument Functions),即参数的个数可以是不定个,在函数定义的时候用...
表示。采用这种形式定义的变长参数函数,至少需要一个普通的形参,且...
需要放在最后一个参数,比如printf函数中的*format
后面的...
是函数原型的一部分。
变长参数的实现得益于C语言默认的cdecl调用惯例,其参数是从右向左压入栈的,第一个参数位于栈顶。这样printf函数实现的时候,就无需关心调用他的函数会传递几个参数过来,而只要关心自己用到几个,将全部参数压入栈,函数处理时从栈中取即可。
自己实现一个变长参数的函数
C已经有现成可用的一些东西来帮我们实现变长参数,它主要通过stdarg.h
头文件定义的一个变量类型(va_list)和三个宏(va_start、va_arg、va_end)来实现。
实现一个可变长参数的sum函数,第一个参数num
传递变长参数中有参数的数量,紧接着后面会传递num
个整型变量,具体实现如下:
1 | int sum(int num, ...) |
变长参数实现原理
上面的sum函数也可以不使用va_list等宏,通过其他方法实现。
当我们调用:int n = sum(3, 16, 38, 53);
参数在栈上会形成如下布局:
在函数内部,函数可以使用变量num
来访问数字3,但无法使用任何名称访问其他的几个不定参数。但此时由于栈上其他的几个参数实际恰好依序排列在参数num
的高地址方向,因此可以很简单地通过num
的地址计算出其他参数的地址,sum函数的另一种实现如下:
1 | int sum(int num, ...) |
printf的不定参数比sum要复杂得多,因为printf的参数不仅数量不定,而且类型也不定。所以printf需要在格式字符串中注明参数的类型,例如用%d
表明是一个整数。printf里的格式字符串如果将类型描述错误,因为不同参数的大小不同,不仅可能导致这个参数的输出错误,还有可能导致其后的一系列参数错误。[摘自《程序员的自我修养——链接、封装、库》P338]
printf("%lf\t%d\t%c\n", 1, 666, 'a');
在这行函数里,printf的第一个输出参数是一个int(4 字节),而我们告诉printf它是一个double(8字节),因此printf的输出会错误,由于printf在读取double的时候实际造成了越界,因此后面几个参数的输出也会失败。该程序的实际输出为:0.000000 97
(根据实际编译器和环境可能不同)
va_list等宏如何实现
va_list 实际是一个指针,用来指向各个不定参数。由于类型不明,因此这个 va_list 以 void* 或 char* 为最佳选择。
va_start 将 va_list 定义的指针指向函数的最后一个参数后面的位置,这个位置就是第一个不定参数。
va_arg 获取当前不定参数的值,并根据当前不定参数的大小将指针移向下一个参数。
va_end 将指针清 0。
按照以上思路,va_list等宏的一个最简单的实现就可以得到了,如下所示:
1 |
注意:实际代码中还套了很多宏,不同编译器,不同架构都有可能使用不同的代码实现,但具体实现思想一致,有些x64条件编译时va_list会是一个结构体,里面会记录可变参数开始地址、结束地址、参数数量等信息。