引言:语言是在练习和思考中精进的,Hello World的出场让你见识到printf的魅力所在,那么你是否知道它隐藏在背后更深层次的美(坑)呢?
文章向导
- 原型出发,悉之结构
- 令人困扰的问题
- printf的实现细节
- 细枝末节
一、原型出发,悉之结构
作为C语言标准库函数的一员,它的函数原型为int printf( const char * format, … )。其中,const char * format为文字字符参数,而…代表可变数量的参数。为了更好地认识这种其参数结构,不妨看下面的图1-1。
上图清晰地指出了填写参数时需要关注的细节,值得注意的是变量列表中的每一个项目都应对应着一个转换说明,即不应该写出下面这种语句:
printf("%d, %d", a);
~~~~ ~~~ 这里没有使用第二个%d,错误语句的结果取决于试验者的系统。
二、令人困扰的问题
~~~~ ~~~ 对printf函数的原型有了较为清晰的认知后,现在不妨来看一个有趣的问题。
#include <stdio.h>
int main(void)
{
float f = 2;
int* p1 = (int*)&f;
float* p2 = &f;
printf("%p, %p, %p\n" , p1, p2, &f);
printf("%d, %f, %f\n", *p1, *p2, f);
printf("%f, %f, %f\n", *p1, *p2, f);
return 0;
}
如上图所示,本程序是在32位VC++6.0平台下编译并运行的,但其运行结果不免让人有些摸不着头脑,尤其是最后一个打印竟然值全0。对比最后两个printf语句可知,似乎是由于使用了不正确的转换说明而导致的,即*p1,%d,%f这三个书写。具体的原因是甚,则得从printf的实现细节开始说起。
三、printf的实现细节
~~~~ ~~~ 要解释上面这个问题,首先得了解两个知识点:转换说明的意义;printf参数传递的实现机制。 下面将一一进行论述。
1.转换说明的意义
~~~~
~~~
%d,%x,%f等都是常用的转换说明,它们把存储在计算机中的二进制格式的数值转换成一系列字符(或一个字符串)以便于显示。比如,十进制数值76对应的二进制存储形式为01001100,使用%d转换说明符则将之转换成7和6,并显示为76。%x则把相同的二进制值(01001100)转换成十六进制4c。
另外,值得一提的是“转换”一词可能会引起人们的误解,它更倾向于翻译的意味,而不是用转换后的值代替原值。就如%d应理解为“将给定的值翻译成十进制整数文本表示,并打印出来”。
显然,转换说明符应与要打印的值的类型相匹配,否则就会出现上面那般困扰。
2.参数传递的机制
~~~~ ~~~ 接下来是重点部分,但我想以一个更为简单的例子来向读者解析第二节的问题。
#include <stdio.h>
int main(void)
{
float n1 = 3.0;
double n2 = 3.0;
long n3 = 2000000000;
long n4 = 1234567890;
printf("%.le, %.le, %ld, %ld\n", n1, n2, n3, n4);
printf("%ld, %ld, %ld, %ld\n", n1, n2, n3, n4);
return 0;
}
~~~~
~~~
下图是该程序的运行结果,可见第二个prinft语句由于使用了不正确的转换说明符,使得后续即使为正确的说明符也会产生虚假的结果。细细思考后应能够猜想到:问题或许出在C语言将参数信息传递给函数的方式中。
参数传递的机制随实现不同而有所不同,但在本文中它的工作原理如下:
printf("%ld, %ld, %ld, %ld\n", n1, n2, n3, n4);
~~~~
~~~
通过该调用,计算机首先把它们(n1,n2,n3,n4)放置到栈的一块内存区域中来实现。计算机是根据变量的类型而非转换说明符,把这些值放到栈中。所以,n1在栈中占用8个字节(使用printf时所有float型参数会被转换成double型)。
同理,n2占用8个字节,而n3和n4则分别占用4个字节。接着,控制转移到printf()函数,函数从栈中把值读出来,但这种读取是根据转换说明符来进行的。%ld表示,printf()应该读取4个字节,所以printf()在栈中读取前4个字节作为它的第一个值,即n1的前半部分将被解释为长整型long。下一个%ld说明符再读取4个字节,即n1的后半部分,它被解释为第二个long型数据。同样,n2的前半部分和后半部分被依次读出作为第三个和第四个值。
所以,虽然n3和n4的说明符都正确,但是printf()却读取了错误的字节。为便于形象的理解上述说明,可参考下面的示意图。
由上述的分析,再回过头来看第二部分的问题,必然会豁然开朗!
四、细枝末节
1. printf的返回值
~~~~ ~~~ 由函数原型可知printf()返回一个int值,其值为打印字符数。这里的字符包括转换说明符,空格和不可见的换行字符(转义字符不计算在内)。可见下面这个简单例子:
#include <stdio.h>
int main(void)
{
int i = 2;
int rv;
rv = printf("%d F is \n", i);
printf("%d\n", rv); //8
return 0;
}
2.打印较长的字符串
printf("Hello, young people, wherever you are.");
printf("Hello, young" "people" ", wherever you are.");
printf("Hello, young people"
", wherever you are. ");
~~~~ ~~~ 上述三种形式是等同的,其中后面两个打印语句采用的是字符串连接方法,它是ANSI C的新方法。如果在一个用双引号引起来的字符串后面跟有另一个用双引号引起来的字符串,而且两者之间仅用空白字符分隔,那么C将会把该组合当作一个字符串来处理。
参阅资料
C Primer Plus
狄泰软件学院-C语言进阶剖析教程