可变参数函数printf调用过程的分析
2014年3月17日 16:17 作者:张春玲 潍坊科技学院 山东寿光 2627张春玲 潍坊科技学院 山东寿光 262700
【文章摘要】
printf 是C 中功能比较强大的一个可变参数函数,本文通过分析printf函数调用过程中的参数入栈,参数访问,参数出栈的实现,以帮助正确使用printf 函数。
【关键词】
C ;printf 函数调用;可变参数函数1 可变参数函数在程序设计中,可变参数函数是指函数拥有不定参数。C 语言中常见的printf函数就是比较典型的可变参数函数。它的函数原型:int printf(char *fmt,...) ;函数参数中,除了有一个参数fmt 固定以外, 后面参数的个数和类型是可变的(...)。
2 栈栈就是一个具有先进后出属性的动态内存区域。栈的增长方向是向下增长,即由高地址向低地址方向。栈的这种先进后出特性有广泛运用。函数调用是间接使用栈的最好例子。
3 printf 调用过程的分析printf 函数的功能是按照用户指定的格式, 将数据格式化然后向系统默认的输
出设备输出若干个任意类型的数据。函数调用时,实现参数之间的传递,必须先将参数读取到堆栈中,然后再调用函数。C语言函数参数采用自右向左的入栈顺序,即函数的最后一个参数先入栈,第一个参数最后入栈;对于printf 函数,通过指针找到的第一个参数就是固定参数fmt。
3.1 printf 函数参数的入栈
C 调用协议下,为了遵循“对齐“原则,对Intel80x86 机器来说就是要求每个变量的地址都是sizeof(int) 的倍数,因此参数入栈都是整数字节。那为什么要对齐?因为在对齐方式下,CPU 的运行效率要快得多。同时在C 语言中,调用一个不带原型声明的函数时,调用者会对每个参数执行“默认实际参数提升”。提升工作如下:1)float 类型的实际参数将提升到double(分配8 个字节)。2)char、short 和相应的signed、unsigned 类型的实际参数提升到in(t 4 个字节),如果int 不能存储原值,则提升到unsigned int。然后,调用者将提升后的参数传递给被调用者。
因此printf 的参数入栈时根据参数类型以及类型提升规则来分配相应大小的栈空间。printf() 参数入栈过程如实例:float f1 ; double f2 ; int n1 ; longn2 ; char c1 ;…printf("%f,%f,%d,%ld,%c",f1,f2,n1,n2,c1) ;printf 调用告诉计算机,要把参数f1,f2,n1,n2,c1 的值交给计算机,它把这些参数值依次入栈。入栈时根据参数定义时的类型以及提升规则而不是转换说明符,将参数自右向左入栈 ,入栈为c1(char 提升为int)分4 个字节, n2 分4 个字节,n1放了4 个字节,f2 放8 个字节 f1 放8 个字节(float 提升为double),最后格式字符串入栈。
3.2 访问printf 参数
printf 参数入栈后,调用函数,然后访问参数。对于可变参数函数,当传递的参数个数大于1 时,是无法判断后面参数的类型,不知道类型就不知道后面参数在栈中占用多大空间,那也就无法通过移动指针读取栈中相应大小的参数值,那函数也就无法正确使用参数值。那对于printf 函数,它又如何正确输出各个实参的值?
3.3 printf 通过格式字符串来读取后面参数
对于编译器来说,printf 函数的第一个固定参数就是一个普通字符串,如何通过这个字符串识别参数类型,在printf 函数实现中,是通过循环判断字符串中的每一个字符,如果是普通字符,直接输出,与栈中参数值无关,对于‘%’后面的每个字符则借助于switch 语句判断,如遇到d,f,c等c 语言中声明的格式字符时,分别将这些特殊字符转换到相应的类型。如:d代表int,f 代表double,c 代表int ;转换类型的原则与上述参数入栈时使用的“默认参数提升“规则一致。在匹配后面可变的各个参数时,需要使用三个宏(定义在stdarg.h);三个宏如下:void va_start(va_list ptr,prev_param);type va_arg(va_list ptr,type );void va_end(va_list ptr );函数里首先定义一个va_list 型的指__针变量ptr,这个变量是存储参数地址的指针。因为得到参数地址之后,再结合参数类型,才能得到参数值。va_start 宏初始化ptr,将其指向第一个可变参数。很明显它先得到第一个固定参数内存地址,然后又加上这个固定参数prev_param 的内存大小,就是第一个可变参数的内存地址了。va_arg 宏有两个作用,首先返回ptr所指向的参数的值,然后自增指向下一个可变参数的地址。要得到参数值,必须借助type 当前参数的类型(格式字符转换过来的类型),用来计算该参数的长度(指针移动的步长),确定下一个参数的起始位置。它可以在函数中应用多次,直到得到函数的所有参数为止,但必须在宏va_start后面调用。在使用va_arg 时,要注意type所用类型名应与传递到堆栈的参数字节数对应,以保证能对不同类型的可变参数进行正确地寻址。va_end 宏在获取所有的参数后,设置指针ptr 为NULL。printf("%f,%f,%d,%ld,%c",f1,f2,n1,n2,c1); 参数访问过程如下:指针先指向第一格式字符串,首先遇到%f 指定printf 应读8 个字节(va_arg(va_list ptr,type ) 中type=double),因此printf读入栈中的8 个字节,作为它的第一个值; 当指针指向第2 个%f 时同样读入栈中的8 个字节,作为第二个值,依次指向%d,%ld,%c,分别读入栈中的4 个字节,4 个字节,4 个字节,作为第3 个值,第4个值,第5 个值。这时每次读入的字节数正好与参数入栈时的字节数对应,保证参数入栈时的值和读取时的值一致,这也解释了在使用printf 时固定参数中的格式字符必须与后面可变参数的类型一致的问题。
4 printf 参数出栈
C 调用约定在返回前, 要作一次堆栈平衡, 也就是参数入栈了多少字节, 就要弹出来( 出栈) 多少字节。
5 总结
printf 是C 语言中应用比较频繁的可变参数函数,本文重点介绍了参数入栈的规则,读取参数值时函数中固定格式字符串与可变参数之间的对应关系,这对使用printf 函数有一定的指导意义。
【参考文献】
[1] 王瑞庆. 格式输出函数printf 的执行流程分析[J]. 信息技术,2007.(5).[2] 谭浩强.C 语言程序设计[M]. 北京. 清华大学出版社,2011.