为什么 printf("%f",0);给出未定义的行为?

Why does printf("%f",0); give undefined behavior?

声明

printf("%f\n",0.0f);

打印 0。

然而,声明

printf("%f\n",0);

打印随机值。

我意识到我表现出某种未定义的行为,但我无法弄清楚具体原因。

所有位都为 0 的浮点值仍然是有效的 float,值为 0。
floatint 在我的机器上大小相同(如果相关的话)。

为什么在 printf 中使用整数文字而不是浮点文字会导致此行为?

P.S。如果我使用

可以看到相同的行为
int i = 0;
printf("%f\n", i);

我不确定有什么问题。

您的格式字符串需要 double;您改为提供 int.

这两种类型是否具有相同的位宽是完全无关紧要的,除了它可以帮助您避免从这样的损坏代码中获得硬内存违规异常。

"%f" 格式需要一个类型为 double 的参数。你给它一个 int 类型的参数。这就是行为未定义的原因。

该标准不保证全零位是 0.0(尽管通常是)或任何 double 值或 int 的有效表示和 double 大小相同(记住它是 double,而不是 float),或者,即使它们大小相同,它们也会作为参数传递给同理。

您的系统上 "work" 可能会发生这种情况。这是未定义行为的最糟糕症状,因为它很难诊断错误。

N1570 7.21.6.1 第9段:

... If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

float 类型的参数被提升为 double,这就是 printf("%f\n",0.0f) 起作用的原因。窄于 int 的整数类型的参数被提升为 intunsigned int。这些促销规则(由 N1570 6.5.2.2 第 6 段指定)在 printf("%f\n", 0).

的情况下没有帮助

请注意,如果您将常量 0 传递给需要 double 参数的非可变函数,则行为定义明确,假设函数的原型可见。例如,sqrt(0)(在 #include <math.h> 之后)将参数 0int 隐式转换为 double —— 因为编译器可以从 [= 的声明中看到33=] 它需要一个 double 参数。 printf 没有此类信息。像 printf 这样的可变参数函数很特殊,在编写对它们的调用时需要更加小心。

Why does using an integer literal instead of a float literal cause this behavior?

因为 printf() 除了第一个 const char* formatstring 之外没有类型参数。它对所有其余部分使用 c 风格的省略号 (...)。

它只是决定如何根据格式字符串中给出的格式类型来解释传递到那里的值。

您将遇到与尝试时相同的未定义行为

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB

通常当您调用需要 double 的函数时,但您提供了 int,编译器会自动为您转换为 doubleprintf 不会发生这种情况,因为参数的类型未在函数原型中指定 - 编译器不知道应该应用转换。

首先,正如其他几个答案中提到的那样,但在我看来,没有足够清楚地说明:它 确实 中提供整数大多数 库函数采用 doublefloat 参数的上下文。编译器会自动插入一个转换。例如,sqrt(0) 定义明确,其行为与 sqrt((double)0) 完全相同,对于此处使用的任何其他整数类型表达式也是如此。

printf不一样。它是不同的,因为它需要可变数量的参数。其函数原型为

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

因此,当你写

printf(message, 0);

编译器没有关于printf预期第二个参数是什么类型的任何信息。它只有参数表达式的类型,即 int。因此,与大多数库函数不同,程序员需要确保参数列表符合格式字符串的预期。

(现代编译器 可以 查看格式字符串并告诉您类型不匹配,但他们不会开始插入转换来完成您的操作这意味着,因为当您注意到时,您的代码现在应该崩溃,而不是几年后用一个不太有用的编译器重建时。)

现在,问题的另一半是:鉴于 (int)0 和 (float)0.0 在大多数现代系统中都表示为 32 位且全部为零,为什么它不起作用无论如何,偶然? C 标准只是说 "this isn't required to work, you're on your own",但让我阐明它不起作用的两个最常见原因;这可能会帮助您理解为什么它不是必需的。

首先,由于历史原因,当您通过可变参数列表传递 float 时,它会 提升 double,这在大多数现代系统,是 64 位宽。所以 printf("%f", 0) 只将 32 个零位传递给被调用者,而被调用者期望有 64 个零位。

第二个同样重要的原因是浮点函数参数可能在与整数参数不同的地方中传递。例如,大多数 CPU 都有用于整数和浮点值的独立寄存器文件,因此如果参数 0 到 4 是整数,则参数 0 到 4 进入寄存器 r0 到 r4 可能是一个规则,但如果它们是浮点数,则进入 f0 到 f4。所以 printf("%f", 0) 在寄存器 f1 中查找那个零,但根本不存在。

使用不匹配的 printf() 说明符 "%f" 和类型 (int) 0 会导致未定义的行为。

If a conversion specification is invalid, the behavior is undefined. C11dr §7.21.6.1 9

UB 的候选原因。

  1. 它是符合规格的 UB,而且编译很麻烦 - 'nuf 说。

  2. doubleint 大小不同。

  3. doubleint 可以使用不同的堆栈传递它们的值(一般与 FPU 堆栈。)

  4. A double 0.0 可能 不是由全零位模式定义的。 (罕见)

"%f\n" 仅当第二个 printf() 参数的类型为 double 时,才能保证可预测的结果。接下来,可变参数函数的额外参数是默认参数提升的主题。整数参数属于整数提升,它永远不会产生浮点类型的值。并且 float 参数提升为 double.

最重要的是:标准允许第二个参数是或 floatdouble 而不是别的。

为什么它是正式的 UB 现在已经在几个答案中讨论过。

具体出现此行为的原因取决于平台,但可能如下:

  • printf 期望其参数符合标准可变参数传播。这意味着 float 将是 double 而任何小于 int 的东西都将是 int.
  • 您正在传递 int,而函数需要 double。您的 int 可能是 32 位,您的 double 可能是 64 位。这意味着从参数应该位于的位置开始的四个堆栈字节是 0,但接下来的四个字节具有任意内容。这就是用于构建显示值的内容。

这是从编译器警告中学习的好机会之一。

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

因此,printf 正在产生未定义的行为,因为您向它传递的参数类型不兼容。

此 "undetermined value" 问题的主要原因在于将传递给 printf 变量参数部分的 int 值处的指针强制转换为 [=15= 处的指针] va_arg 宏执行的类型。

这导致引用未使用作为参数传递给 printf 的值完全初始化的内存区域,因为 double 大小的内存缓冲区大于 int 大小。

因此,当这个指针被解除引用时,它返回一个未确定的值,或者更好的是一个 "value",其中部分包含作为参数传递给 printf 的值,而对于其余部分可以来自另一个堆栈缓冲区甚至代码区域(引发内存故障异常),真正的缓冲区溢出


它可以考虑 "printf" 和 "va_arg"... 的简化代码实现的这些特定部分

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


the real implementation in vprintf (considering gnu impl.) of double value parameters code case management is:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



references

  1. gnu project glibc implementation of "printf"(vprintf))
  2. example of semplification code of printf
  3. example of semplification code of va_arg