刚开始学C语言的时候,看到用scanf输入浮点数据的对应字符串如下:
float : %f
double : %lf
而printf输出的时候却都是统一的:
float / double : %f
你也许曾经跟我一样,用%lf输出过double,
结果是正常的,因为%lf直接被当作了%f了。
但是这时候问题就来了,
问题1.C语言也算是强类型的语言,两种不同类型怎么能统一到同一种输出上呢?
问题2.既然输出都可以统一,为什么输入必须用两种不同的格式串呢?
实验一:确定以上结论
int main(){
double d;
scanf("%f",&d);
printf("%f\n",d);
scanf("%lf",&d);
printf("%f\n",d);
return 0 ;
}
两次都输入1,
我电脑上的输出为:
-92559604281615349000000000000000000000000000000000000000000000.000000
1.000000
可见用%f输入double型是不正确的,还有很多类似实验,就不一一列举了。
输入两种格式*输出两种格式*两种类型变量 = 8 组 。
实验二:规范的输入输出,以及汇编分析
int main(){
float f;
double d;
scanf("%f",&f);
scanf("%lf",&d);
printf("%f\n",f);
printf("%f\n",d);
return 0 ;
}
这个实验的写法是符合标准的,两次输入1,能够输出正确的结果。
现在要解决的就是提出的两个问题。
我把汇编代码放在最后。这里挑关键说明一下。
截取部分就是四个函数的调用,四块大致上是长这样:
push ...
push ...
call ...
add esp , ?
(关于C调用约定,查一下就清楚了)
第二个push的是格式字符串的地址,
所以我们关心的在于第一个push,那里进去的是我们的变量
第一个:
lea eax, dword ptr [ebp-4]
push eax
先获取变量的有效地址,然后直接push
第二个:
lea ecx, dword ptr [ebp-C]
push ecx
除了地址外,其他和第一个一样。
既然传给scanf 的都是变量的地址,我们当然就要用不同的格式字符串来区分了。
第四个:先看第四个因为它更简单
mov edx, dword ptr [ebp-8]
push edx
mov eax, dword ptr [ebp-C]
push eax
ebp-C就是我们d变量的地址,
这里先让高双字[ebp - 8]入栈,
然后再是低双字[ebp - C]。
这些是由平台的字节顺序规定的,我的机器是小尾顺序,
而esp的增长方向是向下的,所以这样 push进去的东西依然是小尾的。
第三个:很猫腻的就在这里了
fld dword ptr [ebp-4]
sub esp, 8
fstp qword ptr [esp]
[ebp - 4]是我们的变量f。
第一条指令,f入浮点栈
第二条指令,把esp减掉8,相当于在栈顶“腾"出了8字节的空间
第三条指令,从浮点栈弹出一个数到指定内存。
后面的esp由 qword修饰 , 即8字节。
简要的说 ,就是利用机器本身的浮点指令完成了
从float到double型的转换
而我们传给printf的始终是一个double型。
既然我们传给函数的都是同一种类型,当然没必要区分格式字符串了!
至此,前面提出的两个问题算是有了一个说法。
问题3:
也许你也跟我一样,想到了既然传给scanf的都是一个地址,
那我们给了一个double的地址,用了%f输入,
但是机器不知道那是double的地址啊!一定会正确的输入一个浮点数的!
就像下面这样
double d;
scanf("%f",&d);
那么输出呢?
实验三:
int main(){
double d;
scanf("%f",&d);
printf("%f\n",d);
printf("%f\n",float(d));
printf("%f\n",*(float*)&d);
return 0 ;
}
在我电脑上的输出为:
-92559604281615349000000000000000000000000000000000000000000000.000000
-92559604281615349000000000000000000000000000000000000000000000.000000
1.000000
第一个只是说明这样输出是错的。
也许有人会说,既然正确输入了一个float,那么我来个强制转换吧。
这就是第二个结果,其实也是错的。
因为float(d)将产生double -> float 的类型转换。
是按值转换,所以还是和上面一样的一个东西。
那第三个呢?从结果看,显然是对的。
这句话比较绕口,从右向左看:
先取了d的地址,这里是double*的类型,
然后指针类型转换,这里就是float*的类型了,
最后脱指针引用,得到的就是一个float类型。
呵呵,这个玩得稍微大了一点。
只是想说明,虽然用scanf 和printf的格式串有细微的规定,
但并不是一种八股,只要知道原理怎么来都行。
有兴趣可以试下用%lf输入float,再输出也是可以的。
不过注意,假设你输入输出用的是变量f,要这样:
int main(){
float fspace;
float f;
。。。
试试把fspace去掉,即把float f放在main的第一行会出现什么!
有意思吧。。。
实验二的汇编代码:
========================
scanf("%f",&f);
00412BA8 8D45 FC lea eax, dword ptr [ebp-4]
00412BAB 50 push eax
00412BAC 68 2C614200 push 0042612C ; ASCII "%f"
00412BB1 E8 7AFFFFFF call scanf
00412BB6 83C4 08 add esp, 8
scanf("%lf",&d);
00412BB9 8D4D F4 lea ecx, dword ptr [ebp-C]
00412BBC 51 push ecx
00412BBD 68 1C604200 push 0042601C ; ASCII "%lf"
00412BC2 E8 69FFFFFF call scanf
00412BC7 83C4 08 add esp, 8
printf("%f\n",f);
00412BCA D945 FC fld dword ptr [ebp-4]
00412BCD 83EC 08 sub esp, 8
00412BD0 DD1C24 fstp qword ptr [esp]
00412BD3 68 08704200 push 00427008 ; ASCII "%f",LF
00412BD8 E8 93E4FEFF call printf
00412BDD 83C4 0C add esp, 0C
printf("%f\n",d);
00412BE0 8B55 F8 mov edx, dword ptr [ebp-8]
00412BE3 52 push edx
00412BE4 8B45 F4 mov eax, dword ptr [ebp-C]
00412BE7 50 push eax
00412BE8 68 08704200 push 00427008 ; ASCII "%f",LF
00412BED E8 7EE4FEFF call printf
00412BF2 83C4 0C add esp, 0C
===============================