深入解析printf/spintf/snprintf中的类型转换

  问题的提出

源码1:

  1. #include   
  2. f1()  
  3.  
  4.         double -5.5625;  
  5.         printf("%d\n",x);                //输出为0,为什么?  
  6.  
  7. main()  
  8.  
  9.         f1();  
  10.  
源码2
  1. #include   
  2. f1()  
  3.  
  4.         int y=1;  
  5.         printf("%f\n",y);          //输出的值是随机的, 为什么?  
  6.  
  7. main()  
  8.  
  9.         f1();  
  10.  
分析这个问题之前,我们要首先了解浮点数在计算机中是如何存储的。

 

 

浮点数在计算机中的存储

c语言中,浮点数有两种数据类型floatdoublefloat占用32bitdouble占用64bitfloat的存储格式遵从IEEE R32.24,参见图1;而double的存储格式遵从R64.53,参见图2

 

1.1 float

float x= -5.5625;

接下来,详细解析x在内存中的存储格式。-5.5625 的二进制表示为-101.1001,用二进制的科学计数法表示为-1.011001*22

●符号位

由于是负数,所以为1

●指数位

为2。由于指数部分采用8bit存储,取值范围为[-127,128],指数部分采用加127存储,即在指数部分存储的数据为2+127=129,即10000001。

●尾数部分为011001。所以x在内存中存储格式见图3。对应的16进制整数为0xc0b20000。 

1.2 double

double y = -5.5625;

 -5.5625 的二进制表示为-101.1001,用二进制的科学计数法表示为-1.011001*22

 ●符号位

由于是负数,所以为1

●指数位

为2。由于指数部分采用11bit存储,取值范围为[-1023,1024],所以指数部分采用加1023存储,即在指数部分存储的数据为2+1023=1025,即100 0000 0001。

●尾数部分

为011001。

 所以y在内存中存储格式见图4。对应16进制整数为0xc01640000000 00 00。 

分析问题

以下是利用gdb跟踪调试源码1的过程。发现,printf("%d\n",x);根本就没有把x由double类型转换为int类型,只是截取了x的低4个字节,并输出。

  1. (gdb) main  
  2. (gdb)  
  3. Breakpoint 1, main () at 1.c:9  
  4.               f1();  
  5. (gdb) display /i $pc  
  6. call   0x8048354   
  7. (gdb) si  
  8. push   �p                  ;保存上层函数的栈的上下文  
  9. (gdb) si  
  10. mov    %esp,�p       ;保存上层函数的栈的上下文  
  11. (gdb) si  
  12. sub    $0x28,%esp        ;为函数f1分配的栈,大小为28字节  
  13. (gdb) si  
  14. double -5.5625;  
  15. fldl   0x8048480             ;把0x8048480存储的双精度浮点数置入浮点寄存器%st(0)  
  16. (gdb) p/x (char[8])*0x8048480  
  17. $1 {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}  ;证明了内存0x8048480处存储常量-5.5625  
  18. (gdb) si  
  19. double -5.5625;  
  20. fstpl  0xfffffff8(�p)          ;把浮点寄存器%st(0)的值置入内存(�p-8)处  
  21. (gdb) info all-registers  
  22. st0            -5.5625  (raw 0xc001b200000000000000)  ;证明了%st(0)存储的浮点数为-5.5625  
  23. (gdb) si  
  24. printf("%x\n",x);  
  25. fldl   0xfffffff8(�p)    ;把内存(�p-8)处的双精度浮点数置入%st(0),即-5.526  
  26. (gdb)   
  27. printf("%x\n",x);  
  28. fstpl  0x4(%esp)                        ;把%st(0)中的值置入内存(%esp+4),即把printf的第二参数压栈  
  29. (gdb) esp  
  30. esp            0xbfb00320       0xbfb00320  
  31. (gdb) p/x (char[8])*0xbfb00324  
  32. $2 {0x6c, 0x95, 0x4, 0x8, 0x38, 0x3, 0xb0, 0xbf}  
  33. (gdb) si  
  34. printf("%x\n",x);  
  35. movl   $0x8048478,(%esp)   ;把函数printf的第一个参数压入栈中,用栈来传递参数  
  36. (gdb) p/x (char[8])*0xbfb00324  
  37. ;显示printf的第二个参数的值。printf的格式串中”%d”在指明第二参数是int类型,即使实际传递的;是double类型,也没有进行类型转换,即没有把x由double类型转换为int类型,printf在取值是  
  38. ;直接读取前4个字节00 00 00 00,所以printf输出为0  
  39. $3 {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}    
  40. (gdb) si  
  41. printf("%x\n",x);  
  42. call   0x8048298  ;调用printf函数  
  43. (gdb) p/x (char[8])*0xbfb00324  
  44. $4 {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}  

由此引申开来发现如下规律:

● %d/%x/%u --> float/double

利用%d/%x/%u输出float/double类型变量时,会得到意想不到的结果,因为不会进行类型转换,而是把变量截断为4个字节并输出。原因在前面已经给出。

● %f --> int

利用%f输出int变量,输出的值是随机的。

  1. f1()  
  2.  
  3.         int 1;  
  4.         printf("%f\n",x);  ;输出的值是随机的  
  5.  

对应的汇编代码:

  1. movl    $1, -4(�p)  
  2. movl    -4(�p), �x  
  3. movl    �x, 4(%esp)  ;没有把x转换为float类型  
  4. movl    $.LC0, (%esp)  
  5. ;printf会读取内存4(%esp)除的8个字节,由于后4个字节的值是随机的,所以输出的值是随机的  
  6. call    printf      

●%d/%x/%u  --> char/short

利用%d/%x/%u输出char/short类型变量时,会对char/short类型进行符号位扩展,扩展为4个字节。

  1. f1()  
  2.  
  3.          char 0x80;  
  4.         printf("%x\n",x);  
  5.   

对应的汇编代码:

  1. movb    $1, -1(�p)  
  2. movsbl  -1(�p),�x  ;把x符号扩展为4个字节  
  3. movl    �x, 4(%esp)  
  4. movl    $.LC0, (%esp)  
  5. call    printf  


结论

由于float/double存储机制迥异于char/short/int/long long的存储机制,所以利用%f来输出char/short/int/long long变量,或者是利用%d输出float/double变量,得到的结果会令你大吃一惊。所以我们在使用printf/sprintf/snprintf函数时,应该严格的按照参数的类型指定格式串。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值