一、问题抛出
先看一个例子:
1、若运行时从键盘上输入9876543210l,则该程序在gcc编译器下的输出结果是()
int main(){
int a;float b,c;
scanf("%2d%3f%4f",&a, &b, &c);
printf("\na=%d,b=%d,c=%f\n",a,b,c);
}
A、a=98,b=765,c=4321.000000
B、a=98,b=0,c=0.000000
C、a=98,b=765.000000,c=4321.000000
D、a=98,b=765.0,c=4321.0
正确答案:B
解析:scanf语句执行完后,a=98,b=765,c=4321,这个语句没有问题。
这是问题的解释:答案解析
原文:
printf函数执行的时候,会先把这三个数字压入栈里,然后再执行打印。压入栈的时候按照数据本身的长度来,首先把c和b压入,并且每一个都是8个字节(printf自动转化为double)。然后再压入a是4个字节。然后再执行打印。打印的时候按照用户指定的格式来出栈。首先打印a,a打印正常。然后又打印4个字节长度的b,在栈里面由于b长度是八个字节,并且b目前是64位的表示方式,数据的后面全是0.(float 变double),电脑是小端存储方式,0存储在距离a近的地方。打印b的时候,打印的4个字节都是0。然后再打印c,c用正常的方式打印,会一下子读取8个字节,正好,读出来的八个字节前面四个字节全是0,自己可以算一下,实在太小了,因此为0。(c低4字节为765,高四字节为765左边的0000)
栈底 栈顶
高字节 低字节
4321 0000 765 0000 98
4字节 4字节 4字节 4字节 4字节
打印c 打印b 打印a
二、printf如何打印的
printf打印时需要解决这几个问题:要打印数据的起始地址是多少、取多少数据、以什么类型解读数据、以什么形式打印。
示例1 |
#include <stdio.h>
int main(int argc, const char *argv[])
{
char a = 'a';
char *p = &a;
printf("p=%p\n", p);
printf("p=%#x\n", p);
printf("&a=%p\n", &a);
printf("&a=%#x\n", &a);
return 0;
}
运行结果为:
p=0xbf887f8b
p=0xbf887f8b
&a=0xbf887f8b
&a=0xbf887f8b
函数定义的变量是占用栈空间,printf函数被调用时,首先将形参的内容复制给printf函数中的变量,输出控制符的作用是取多少数据、按什么类型来解读该数据(有无符号、整型或浮点型)、以什么样的形式打印到屏幕(打印多少,如%2.1f)。该例子中,%p的输出形式等于%#x。
示例2 |
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 0x444FFFFF, b = 0x433FFFFF;
printf("a=%f\n", a, b);
return 0;
}
运行结果为:
a=9007196105867263
从该例中说明,printf中%f是从栈中取八个字节进行输出打印,输出控制符%f用于打印单精度和双精度数值,这也说明为什么单精度实参输入会以双精度进行保存。
示例3 |
/* a.c */
#include <stdio.h>
int main(int argc, const char *argv[])
{
char a = 'A'; // 对应的十进制是 65
char b = 0x11;
char c = 0x12;
char d = 0x13;
printf("a=%d, b=%d\n", a, b, c, d);
return 0;
}
运行结果为:
a=65, b=17
gcc a.c -o a.out # 将 a.c 编译成 a.out 可执行目标文件
objdump -d a.out > a.dump # 生成反汇编文件 a.dump
a.dump:(部分)
(%d的读取数据为4字节,这两点是难以从上述代码中确定的,从源码上看确实是的,结论就是字符类型是以4字节入栈。)
示例4 |
#include <stdio.h>
int main(int argc, const char *argv[])
{
int a = 98;
float c = 1.0;
double d = 9.007;
printf("a=%d, b=%d, c=%x\n", a, d, c);
return 0;
}
运行结果为:
a=98, b=-2130303779, c=40220395
十进制的-2130303779(负数)转换成十六进制为0x810624DD,同c打印出的0x40220395结合起来,正好就是浮点数d存储在内存中的值。
从输出结果可知,以第一个形参变量的起始地址为基址,按照输出控制符进行偏移。例如:
假设a变量内容保存在栈空间中的起始地址为0,再取4字节打印第一个%d;
第二个%d的起始地址:a内容在栈空间的起始地址+%d(4个字节)= 4,再取4字节打印第二个%d。
最后%x的起始地址:a内容在栈空间的起始地址+第一个%d(4个字节)+ 第二个%d(4个字节)= 8,再取4字节打印%x。
小结 |
1、输出控制符的作用是取多少数据、以什么类型解读数据、以什么形式打印到屏幕(标准输出)。
2、char类型的实参,以4字节为单位入栈;float类型的实参,将扩展到double类型(8字节)的数据,然后以8字节为单位入栈。
3、printf中%f是从栈中取八个字节进行输出打印。
4、printf双引号中的%d、%u、%f等,从输出控制符的基址开始取若干字节进行输出打印,这里输出控制符的基址并不是各形参的起始地址,而是从左到右第一个形参为基址,按输出控制符需要的字节数进行偏移,从而确定后续输出控制符的基址。
三、回到问题
栈中的地址从栈顶到栈底依次为:
a=0x00000062
b=0x4087E80000000000
c=0x40B0E10000000000
打印第一个%d为:0x00000062,即为98。
打印第二个%d为:0x00000000(蓝色),即为0。
打印%f为:0x000000004087E800(这里高4字节部分为c变量的低4字节部分),对应的浮点数越等于2.09E-319,近似于0。
四、参考资料
[1]: printf以"%d"输出浮点数
[2]: printf以%d形式输出浮点数的问题
[3]: 浮点数:不同的内存存储格式,当整型变量赋值给浮点型变量会发生什么?