【问题标题】:What happens to a float variable when %d is used in a printf?在 printf 中使用 %d 时浮点变量会发生什么情况?
【发布时间】:2011-11-20 18:15:03
【问题描述】:

我正在尝试通过阅读 C Programming Language, 2nd Edition 来学习 C。我有一些编程经验,但没有 C 语言。

我目前在第 1 章。我有以下代码:


  float f;
  for (f = 0.0; f <= 3; f += 1.1)
      printf("A: %3f B: %6.2f\n", f, f + 0.15);

它打印输出:

A: 0.000000 B:   0.15
A: 1.100000 B:   1.25
A: 2.200000 B:   2.35

看起来不错。


现在我将 printf 更改如下:

printf("A: %3d B: %6.2f\n", f, f + 0.15);

新的输出是

A:   0 B:   0.00
A: -1610612736 B:   0.00
A: -1610612736 B: -625777476808257557292155887552002761191109083510753486844893290688350183831589633800863219712.00

这里发生了什么?我希望浮点数转换为 int 因为我使用了 %d 但事实并非如此。另外,为什么值 B 也出错了? f这里怎么了?

【问题讨论】:

  • 你没有要求它把它转换成一个整数,你告诉它它一个整数!垃圾进,垃圾出。

标签: c floating-point printf


【解决方案1】:

Printf 将按照您告诉它的方式处理您指向的内存。没有进行转换。它将表示浮点数的内存视为 int。因为两者的存储方式不同,所以你得到的本质上是一个随机数。

如果你想将你的浮点数输出为整数,你应该先转换它:

printf("A: %3d B: %6.2f\n", (int)f, f + 0.15); 

【讨论】:

  • 好的,有道理。那么 f 的值是否因为我没有将其转换为 int 而改变?如果不是,那么 printf 的第二部分正在使用 %6.2f 那么至少应该有正确的值吗?
【解决方案2】:

当你打电话时:

printf("A: %3d B: %6.2f\n", f, f + 0.15);

C 自动将float 值转换为double(这是在调用带有可变参数的函数时进行的标准转换,例如int printf(const char *fmt, ...);)。为了论证起见,我们假设sizeof(int) 是4,sizeof(double) 是8(也有例外,但它们很少而且相差甚远)。

因此,调用将一个指针压入堆栈,加上一个用于f 的 8 字节双精度,以及另一个用于f + 0.15 的 8 字节双精度。在处理格式字符串时,%d 告诉printf(),您在格式字符串之后将一个 4 字节的int 推入堆栈。由于那不是您所做的,因此您调用了未定义的行为;根据 C 标准,接下来发生的任何事情都可以。

然而,最有可能的实现会愉快地读取 4 个字节并将它们打印出来,就好像它们是 int 一样(它相信你会说实话)。然后遇到%6.2f格式;它将从堆栈中读取 8 个字节作为double。这可能会导致未对齐访问的内存故障(它需要一个 64 位机器,并要求 double 在 8 字节边界上对齐,例如 SPARC),或者它将读取 4来自f 的字节和来自f + 0.15 的4 个字节,将它们放在一起以创建一些相当意想不到的double 值 - 正如您的示例所示。

【讨论】:

  • 很好的答案,所以您可以 printf("A: %d%d B: %6.2",... 并弄错 A vals 但 B 不会受到影响
  • 在大量相当合理的假设下,是的。该行为是严格未定义的,但在实践中,是的。
【解决方案3】:

可以打印任何整数值,无论浮点参数如何:

  printf("A: %d B: %6.2f\n", f, f + 0.15);

以下是在英特尔架构上打印任意整数的方法:

  int print_it(int, int /* nameless but printed */, float f)
  {
      printf("A: %d B: %6.2f\n", f, f + 0.15);
  }
  int main()
  {
      print_it(0, 12 /* will be printed */, 0.0);
      print_it(0, 123 /* printed */, 1.1);
      print_it(0, 1234 /* printed */ , 2.2);
  }

这个输出:

A: 12 B:   0.00
A: 123 B:   1.10
A: 1234 B:   2.20

说明:显然,不匹配的格式字符串和参数会导致未定义的行为。然而,有时这是可以预测的。在 Intel 架构上,前几个参数由寄存器传递。浮点值在不同的寄存器上传递。

尽管具有与问题中相同的 printf 指令,但输出是不同的。发生的情况是 12、123、1234 通过负责第二个非浮点参数的通用寄存器传递。由于printf只有一个非浮点参数,所以第二个非fp参数的寄存器不变。这个寄存器保留了它从print_it(0, int_value, fp_value)的第二个参数得到的值。

但是原版给垃圾了:

  for (f = 0.0; f <= 3; f += 1.1)
      printf("A: %3f B: %6.2f\n", f, f + 0.15);

它给出了不同的垃圾,因为printf 在内部调用了其他函数。这些函数会破坏printf("... %d ...", ...) 读取的通用寄存器。

显然,这种行为只发生在将浮点参数传递到一组单独的寄存器中的系统上。显然,只有当编译器优化不以某种方式修改代码时才会发生这种情况,因为当未定义的行为在发挥作用时,它被允许做一些疯狂的事情。

【讨论】:

    【解决方案4】:

    对于大多数函数,如果您传递 float 但函数需要 int,编译器会自动将 float 转换为 int。但是printf 很特别(非常特别)。 %d 需要 int您的 工作是向它传递 int。在printf 的情况下,编译器中没有自动机制能够为您执行转换。

    话虽如此,但好的编译器会检测并警告此问题。如果你没有,你需要一个更好的。

    更长的解释是,对于大多数函数,函数原型给出了所有参数的数量和类型,这是让编译器知道它可能需要引入转换的机制。但printf 的原型是

    extern int printf(const char *, ...);
    

    这三个点... 的字面意思是,“这里会有可变数量的参数,不知道有多少或什么类型”。当printf 实际运行时,它在格式字符串中找到%d,告诉它期待一个已传递的int,如果您传递的不是@,那么进行任何转换都为时已晚987654336@.

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-03-19
      • 2021-07-19
      • 2013-07-09
      • 2015-04-04
      相关资源
      最近更新 更多