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

来源:互联网 发布:职业网络打假 编辑:程序博客网 时间:2024/04/30 17:27

本文通过两个问题出发,跟踪汇编代码,阐明了函数 printf/spintf/snprintf中的类型转换的规律。

1 问题的提出

源码1:

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

2 浮点数在计算机中的存储

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。 

3 分析问题

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

(gdb) b main(gdb) rBreakpoint 1, main () at 1.c:99               f1();(gdb) display /i $pccall   0x8048354 <f1>(gdb) sipush   %ebp                  ;保存上层函数的栈的上下文(gdb) simov    %esp,%ebp       ;保存上层函数的栈的上下文(gdb) sisub    $0x28,%esp        ;为函数f1分配的栈,大小为28字节(gdb) sidouble x = -5.5625;fldl   0x8048480             ;把0x8048480存储的双精度浮点数置入浮点寄存器%st(0)(gdb) p/x (char[8])*0x8048480$1 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}  ;证明了内存0x8048480处存储常量-5.5625(gdb) sidouble x = -5.5625;fstpl  0xfffffff8(%ebp)          ;把浮点寄存器%st(0)的值置入内存(%ebp-8)处(gdb) info all-registersst0            -5.5625  (raw 0xc001b200000000000000)  ;证明了%st(0)存储的浮点数为-5.5625(gdb) siprintf("%x\n",x);fldl   0xfffffff8(%ebp)    ;把内存(%ebp-8)处的双精度浮点数置入%st(0),即-5.526(gdb) printf("%x\n",x);fstpl  0x4(%esp)                        ;把%st(0)中的值置入内存(%esp+4),即把printf的第二参数压栈(gdb) i r espesp            0xbfb00320       0xbfb00320(gdb) p/x (char[8])*0xbfb00324$2 = {0x6c, 0x95, 0x4, 0x8, 0x38, 0x3, 0xb0, 0xbf}(gdb) siprintf("%x\n",x);movl   $0x8048478,(%esp)   ;把函数printf的第一个参数压入栈中,用栈来传递参数(gdb) p/x (char[8])*0xbfb00324;显示printf的第二个参数的值。printf的格式串中”%d”在指明第二参数是int类型,即使实际传递的;是double类型,也没有进行类型转换,即没有把x由double类型转换为int类型,printf在取值是;直接读取前4个字节00 00 00 00,所以printf输出为0$3 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0} ;  (gdb) siprintf("%x\n",x);call   0x8048298 <printf@plt> ;调用printf函数(gdb) p/x (char[8])*0xbfb00324$4 = {0x0, 0x0, 0x0, 0x0, 0x0, 0x40, 0x16, 0xc0}

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

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

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

● %f --> int

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

f1(){        int x = 1;        printf("%f\n",x);  ;输出的值是随机的}

对应的汇编代码:

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

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

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

f1(){         char x = 0x80;        printf("%x\n",x);} 

对应的汇编代码:

movb    $1, -1(%ebp)movsbl  -1(%ebp),%eax  ;把x符号扩展为4个字节movl    %eax, 4(%esp)movl    $.LC0, (%esp)call    printf


4 结论

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

5参考

[1] http://www.cnblogs.com/jillzhang/archive/2007/06/24/793901.html

 

原创粉丝点击