你真的很了解printf函数吗?
对C语言中经常使用的printf这个库函数,你是否真的吃透了呢?
系统化的学习C语言程序设计,是不是看过一两本C语言方面的经典著作就足够了呢?答案是显而易见的:不够。通过这种典型的入门级的学习方式,是不可能真正吃透C语言的。如果C语言这么容易就被吃透了,那么C语言就不可能成为程序设计语言之母,也不可能被各种操作系统用来作为系统调用的API接口语言。学习任何一门技术,如果只是会使用,而不知道其背后的原理,那么就只是知其然不知其所以然。本文试图通过回答一个跟printf有关的“为什么”的问题,来讨论一下简单用法背后的C语言标准中的一个与这个问题相关的技术概念。
为什么printf能够使用%f这个格式化字符串来输出float和double类型的参数?
先看代码例子。
printf("%f %f", 1.234f, 1.234);
乍一看,很多人会觉得这个代码简直天经地义,C语言入门者就已经能够正确的写出这种“如此简单”的代码。这又有什么值得大惊小怪的呢?
现在的问题是为什么上面这个语句能够正常运作?你是否真的深入思考过这个问题呢?
在回答这个为什么的问题之前,先来看另外一个为什么的问题:为什么要思考这个问题呢?
请注意,参数中对1.234f和1.234的输出使用的格式化字符串都是%f。
printf函数的格式化字符串有两个作用:
作用1:指示输出格式。这个是printf格式化字符串不可或缺的作用。
作用2:指示后面的参数值的数据类型。printf函数内部必须能够精确的知道后面传入的参数的类型,否则无法通过va_arg取得实参的值。这个作用是至关重要的。
上面这个代码显然是没有任何问题的。至此,不由得思考一个问题,printf如何能够取得后面两个参数的数据类型呢?printf不可能根据同样一个%f,一会得出后面的参数是float类型的推测,一会得出后面的参数是double的推测。printf能利用的已知信息就只有%f这个格式化字符串。有人说,后面写1.234f就是float,写1.234就是double啊。这个信息仅仅对函数调用方有效,对于被调用函数printf是不可见的。即printf确实无法分辨出后面两个参数的数据类型是什么。
那么printf究竟怎么就知道了两个参数的数据类型的呢?printf肯定是已经知道了数据类型的,否则无法用va_arg取得实参的值。这其实不是printf自身的特异功能,完全是沾了C语言标准的光。这就是神奇的默认参数类型提升。正是有了默认参数类型提升这个概念,编译器在编译上面这个代码时,首先将1.234f这个float类型的参数转化为了double类型的参数。那么printf在处理%f这个格式化字符串时,直接认为对应的参数是double类型即可完成工作,根本不需要区分实际参数是float还是double的问题。
既然是C语言标准提出的这个概念,那么很多C语言编译器就支持了这个特性。包括gcc和vs2019。而且,我们自己写一个变长参数的函数时,也必须考虑这个特性,否则所写函数无法正常工作。当然,当使用float作为参数调用va_arg时,编译器会编译错误的,因此也不必过于担心这个问题带来的困扰。问题在于当我们不了解这个神奇的概念时,在出现类似编译错误时,我们就无法理解代码到底为什么会产生编译错误。
实际上对于printf的格式化字符串,还有整数的格式化输出的问题。比如:
printf("%d %d %d", (short)1, (char)2, 3);
这又是一个C语言入门者就已经“熟练掌握”的代码。这其中同样存在前面关于%f类似的问题。printf怎么知道后面的3个参数的实际类型呢?要回答这个问题,同样需要理解前面提到的默认参数类型提升的概念。默认参数类型提升不仅对于float类型有效,对于char和short类型,同样会提升,不同在于前者执行的是浮点数类型提升,后者执行的是整数类型提升。因此,char和short类型都会先转化为int类型,再传递给printf函数。这样printf只需要把%d这个格式化字符串对应的实参理解为是int类型即可,这就解决了所有的实参类型的识别的问题。
一个简单的printf函数,背后竟然藏着这么一个神奇的技术概念,而且这个概念还是在C语言标准中明确描述的。现在,你了解了上面那个printf语句为什么能够正常运作了吗?
现在,你还认为printf这个库函数很简单吗?你还自信满满的认为自己精通C语言吗?