格式化输入输出浮点数据的细微问题(C标准:printf,scanf)

来源:互联网 发布:知乎qq登陆 编辑:程序博客网 时间:2024/06/18 04:12

刚开始学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

===============================

0 0
原创粉丝点击