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

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

本文关键字:未定义 printf 为什么      更新时间:2023-10-16

语句

printf("%fn",0.0f);

打印 0.

然而,声明

printf("%fn",0);

打印随机值。

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

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

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

附言如果我使用

int i = 0;
printf("%fn", i);

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

该标准不保证所有位零是0.0的有效表示(尽管它通常是(,或任何double值,或者intdouble的大小相同(记住它是double,而不是float(,或者,即使它们的大小相同,它们也以相同的方式作为参数传递给可变参数函数。

它可能发生在您的系统上"工作"。这是未定义行为的最糟糕症状,因为它使诊断错误变得困难。

N1570 7.21.6.1 第9段:

。如果任何参数不是相应参数的正确类型 转换规范,行为未定义。

float类型的参数被提升为double,这就是printf("%fn",0.0f)工作的原因。小于 int 的整数类型的参数将提升为 intunsigned int 。这些促销规则(由N1570 6.5.2.2第6段指定(在printf("%fn", 0)的情况下没有帮助。

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

首先,正如其他几个答案中提到的,但在我看来,没有足够清楚地说明:在库函数接受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 标准只是说"这不是工作所必需的,你靠自己",但让我详细说明它不起作用的两个最常见原因;这可能会帮助您理解为什么不需要它。

首先,由于历史原因,当您通过变量参数列表传递float时,它会被提升double ,在大多数现代系统上,宽度为 64 位。 因此,printf("%f", 0)只将 32 个零位传递给期望其中 64 个的被调用方。

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

通常,当您调用一个需要double的函数,但您提供了一个int,编译器将自动为您转换为double。这不会发生在printf,因为参数的类型没有在函数原型中指定 - 编译器不知道应该应用转换。

为什么使用整数文本而不是浮点文本会导致此行为?

因为除了const char* formatstring之外printf()没有类型化参数作为第一个参数。它使用C样式的省略号(...(来表示其余的。

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

您将具有与尝试时相同的未定义行为

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

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

如果转换规范无效,则行为未定义。C11dr §7.21.6.1 9

UB的候选病因。

  1. 它是每个规范的UB,编译是华丽的 - 'nuf 说。

  2. doubleint的大小不同。

  3. doubleint可以使用不同的堆栈(通用堆栈与 FPU 堆栈(传递其值。

  4. double 0.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("%fn",0);
  ^

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

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

我不确定是什么令人困惑。

您的格式字符串需要一个double;您提供一个int

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

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

最重要的是:标准允许第二个参数是或floatdouble,没有别的。

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

您获得此行为的原因是特定于平台的,但可能如下:

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

此"未确定值"问题的主要原因是将指针转换为传递给printf变量参数部分的int值,以va_arg宏执行的double类型的指针。

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

因此,当取消引用此指针时,它会返回一个不确定的值,或者更好的是"值",其中包含部分作为参数传递给printf的值,其余部分可能来自另一个堆栈缓冲区甚至代码区域(引发内存错误异常(,真正的缓冲区溢出


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

普列夫

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..
.... 


双值参数代码案例在vprintf(考虑GNU IMPL.(中真正的实现 管理是:

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);



引用

  1. GNU Project glibc 实现 "printf"(vprintf((
  2. printf 的实例化代码示例
  3. va_arg的实例化代码示例