问题

将一个double型(int型)格式化输出为int型(double型)时发生了什么?

回答
好的,咱们来聊聊这个话题,就当是平时吃饭聊天时碰到的一个技术点。

把一个 `double` 类型的数据,你想让它看起来像个 `int` 类型一样输出,或者反过来,把一个 `int` 类型的数据,让它以 `double` 类型的方式呈现,这背后实际发生的是数据类型的转换和输出格式的控制。

咱们一步步来看,会讲得细致点。

当你“格式化”一个 `double` 为 `int` 时发生了什么?

假设你有一个 `double` 类型的变量,比如 `double pi = 3.14159;`。你想让它输出的时候只显示整数部分,就像 `int num = 3;` 这样。

1. 隐式类型转换(截断):
在很多情况下,如果你直接将 `double` 赋值给 `int`,编译器会自动进行一个“截断”(truncation)操作。
截断意味着什么? 它会直接丢弃掉小数点后面的所有部分,不管小数点后是什么数字。
举个例子:`double val = 3.999;` 如果你直接 `int num = val;`,那么 `num` 的值就是 `3`,而不是 `4`。小数点后面的 `.999` 被无情地砍掉了。
这跟我们通常理解的“四舍五入”是不一样的。四舍五入是基于一个规则(例如,小数点后第一位大于等于5则向前一位进一),而截断就是简单粗暴地“砍掉”。

2. 格式化输出的控制(比如使用 `printf` 或 `cout`):
如果你不是直接赋值给 `int`,而是在输出时指定了格式,比如 C 语言的 `printf` 函数,你会这样写:`printf("%d", pi);` 或者 C++ 的 `cout`:`cout << (int)pi;`。
这里的 `(int)pi` 就是一个显式类型转换。它告诉编译器:“我明知道 `pi` 是个 `double`,但我就是要把它当作一个 `int` 来处理。”
转换过程依然是截断。`printf("%d", pi)` 这条语句,在内部,`printf` 函数会根据 `%d` 这个格式说明符,尝试将传递进来的 `pi` 的值(尽管它原本是 `double` 的内存表示)解释为一个整数。这个解释的过程,本质上就是对 `double` 的二进制表示进行截断,丢弃了小数部分。
即使你直接写 `printf("%d", 3.14159);`,编译器在看到 `%d` 时,也会对 `3.14159` 这个字面量进行截断处理,将其视为整数 `3` 来输出。

所以,当 `double` 被“格式化”输出为 `int` 时,最核心的操作就是截断,丢弃了小数部分。

当你“格式化”一个 `int` 为 `double` 时发生了什么?

现在我们换个方向,假设你有一个 `int` 类型的变量,比如 `int count = 10;`。你想让它输出时,像个 `double` 一样,带个 `.0`,变成 `10.0`。

1. 隐式类型提升(扩展):
当你在一个期望 `double` 的地方使用 `int` 时,通常会发生隐式类型提升(promotion)。
提升是什么意思? 编译器会非常“智能”地将你的 `int` 值转换成一个 `double` 值。
对于整数 `10`,将其转换为 `double` 并不复杂。它的二进制表示会被扩展,并且小数点后的部分会用零来填充。所以,整数 `10` 就会变成浮点数 `10.0`。这个过程没有数据丢失(因为整数本身就没有小数部分需要处理)。

2. 格式化输出的控制(比如使用 `printf` 或 `cout`):
如果你使用 `printf` 函数,你会这样写:`printf("%f", count);` 或者 C++ 的 `cout`:`cout << (double)count;`。
这里的 `printf("%f", count)` 是一个更典型的例子。`%f` 格式说明符告诉 `printf`:“请将我接收到的值当作一个浮点数来处理和输出。”
转换过程就是整数提升。当 `count` (值是 `10`) 被传递给 `printf` 时,因为格式是 `%f`,`printf` 会将 `10` 的内存表示(作为整数)解释成一个 `double`,也就是 `10.0`。
C++ 的 `cout << (double)count;` 也是一样的道理,` (double)count ` 显式地将整数 `10` 转换成了浮点数 `10.0`,然后 `cout` 就会按照 `double` 的默认格式输出 `10.0`。

所以,当 `int` 被“格式化”输出为 `double` 时,核心操作是整数提升,将整数值无损地转换为浮点数表示,并在输出时默认显示小数部分(通常是 `.0`)。

总结一下,关键点在于:

`double` > `int`:核心是“截断”,小数部分被丢弃。 这是有损的转换。
`int` > `double`:核心是“提升”,整数值被精确地转换为浮点数表示,小数部分默认为 `.0`。 这是无损的转换。

这些转换在编程中非常常见,理解它们能帮助我们写出更健壮的代码,避免一些意想不到的结果。比如,你可能期望 `printf("%f", 5);` 输出 `5.000000`,但如果你不小心写成了 `printf("%d", 5.0);`,输出就变成了 `5`,而你期望的是 `5.0` 或者别的什么。对类型的理解能帮你避免这类小麻烦。

网友意见

user avatar

仅限x86/64环境下的Linux实现:

代码:

       #include <stdio.h>  int main() {     double x = -1.0;     printf("%d
", x);     return 0; }     

32位模式下

对于printf来说,编译器就是把参数压栈,有几个参数就压几个,gcc编译完之后汇编是这样的:

        541:   ff 75 f4                pushl  -0xc(%ebp)  544:   ff 75 f0                pushl  -0x10(%ebp)  547:   8d 90 18 e6 ff ff       lea    -0x19e8(%eax),%edx  54d:   52                      push   %edx  54e:   89 c3                   mov    %eax,%ebx  550:   e8 5b fe ff ff          call   3b0 <printf@plt>     

因为x是double,是64位,所以541-544的代码,是压入x的值。在x86平台上,这个值(-1)是0xbff00000 00000000

所以上述代码使用gcc test.c -m32 -o test,那么输出的值应该恒定是0,因为0xbff0000000000000的低32位都是0

另外,如果代码改改,用%lld输出的话,恒定输出的是-4616189618054758400,等于-1的浮点在内存中的值。

64位模式下

64位于32位不同的在于,64位用寄存器传参,如果是整数类型的参数,使用rdi,rsi,rdx,rcx这些寄存器。如果是浮点类型的参数,那么使用xmm0,xmm1,...xmm7这些作为参数,不够用的时候才会考虑用栈(注:不同编译器的ABI不同,Win和Linux的就不一样)

反汇编效果

        667:   f2 0f 10 45 e8          movsd  -0x18(%rbp),%xmm0  66c:   48 8d 3d a5 00 00 00    lea    0xa5(%rip),%rdi        # 718 <_IO_stdin_used+0x8>  673:   b8 01 00 00 00          mov    $0x1,%eax  678:   e8 a3 fe ff ff          callq  520 <printf@plt>     

第一个参数是rdi,这个没什么问题,第二个参数,对于printf来说,它看到的是%d,所以要从rsi寄存器里取值,而汇编代码里,没有对rsi初始化,第二个参数是放在了xmm0里。所以这就是为什么你每次运行的结果不一样了。

把代码简单的修改一下:

       #include <stdio.h>  int main() {     double x = -1;     int y = 100;     printf("%d
", x, y);     return 0; }     

增加一行

       int y = 100;     

那么这个代码在64位下恒定输出的就是100

反汇编

        66d:   89 d6                   mov    %edx,%esi  66f:   48 89 45 e8             mov    %rax,-0x18(%rbp)  673:   f2 0f 10 45 e8          movsd  -0x18(%rbp),%xmm0  678:   48 8d 3d 99 00 00 00    lea    0x99(%rip),%rdi        # 718 <_IO_stdin_used+0x8>  67f:   b8 01 00 00 00          mov    $0x1,%eax  684:   e8 97 fe ff ff          callq  520 <printf@plt>     

可以看到esi(rsi),也就是第二个参数是被赋值了,那么这种改法,每次运行的结果就是确定的。


你的第二个问题是把整数作为参数传入,然后使用浮点模式显示

32位模式下

反汇编如下:

        544:   ff 75 e0                pushl  -0x20(%ebp)  547:   8d 83 58 e6 ff ff       lea    -0x19a8(%ebx),%eax  54d:   50                      push   %eax  54e:   e8 5d fe ff ff          call   3b0 <printf@plt>     

这个运行结果是不确定的,因为double浮点是64位的,而int在32位下是32位的,printf会尝试取得一个长度为64位的变量,但你只传入了32位,剩下的部分是栈上的垃圾数据(大概率是返回值之类的),这种情况下,每次调用的结果都会不一样。

如果要让它显示成一个固定的值,把int变量变成long long即可

       int main() {     long long i = -1;     printf("%f
", i);     return 0; }     

每次固定显示-nan

64位模式下

反汇编如下

        65c:   89 c6                   mov    %eax,%esi  65e:   48 8d 3d b3 00 00 00    lea    0xb3(%rip),%rdi        # 718 <_IO_stdin_used+0x8>  665:   b8 00 00 00 00          mov    $0x0,%eax  66a:   e8 b1 fe ff ff          callq  520 <printf@plt>     

这里,rsi就是那个整型变量,前面说了,如果用浮点数,编译器会使用xmm0~xmm7传参,这里并没有给xmm0赋值,所以理论上这里输出的内容是不确定的,但具体是不是不确定的,还要看GLIBC里的printf的实现。

如果要输出一个稳定的值,还有一个办法:


       int main() {     int i = 1;     double r = 1234;     printf("%f
", i, r);     return 0; }     

多传一个参数进去,printf就会利用这个参数了。


这个问题涉及的内容比较杂,如果要了解细节的话,需要学习以下知识点:

1. 不同编译器和操作系统的ABI以及寄存器的使用规则
2. 浮点数在内存的表示方式
3. LIBC里的printf的具体实现方式

类似的话题

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有