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

Tre*_*key 86 c c++ printf undefined-behavior implicit-conversion

该声明

printf("%f\n",0.0f);
Run Code Online (Sandbox Code Playgroud)

打印0.

但是,声明

printf("%f\n",0);
Run Code Online (Sandbox Code Playgroud)

打印随机值.

我意识到我表现出某种未定义的行为,但我无法弄明白为什么具体.

所有位都为0的浮点值仍然有效float,值为0.
float并且int在我的机器上具有相同的大小(如果它甚至相关).

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

如果我使用PS,可以看到相同的行为

int i = 0;
printf("%f\n", i);
Run Code Online (Sandbox Code Playgroud)

Kei*_*son 119

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

该标准并不能保证所有位归零是一个有效的表示0.0(尽管它经常是),或任何的double价值,或者说intdouble具有相同的尺寸(记住它的double,不是float),或者,即使是相同的大小,它们以相同的方式作为参数传递给可变参数函数.

它可能恰好在您的系统上"工作".这是未定义行为的最坏可能症状,因为它使诊断错误变得困难.

N1570 7.21.6.1第9段:

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

类型的参数float被提升为double,这就是为什么printf("%f\n",0.0f)有效.整数类型的窄参数比int被提升到intunsigned int.这些促销规则(由N1570 6.5.2.2第6段规定)对此情况没有帮助printf("%f\n", 0).

  • 这里有几个优秀的核心要点.首先,它是'double`而不是'float`,因此OP的宽度假设可能不会(可能不会)保持.其次,假设整数零和浮点零具有相同的位模式也不成立.干得好 (13认同)
  • @LucasTrzesniewski:好的,但我不知道我的回答是如何回答问题的.我确实声明`float`被提升为`double`而没有解释原因,但这不是主要观点. (2认同)
  • @ robertbristow-johnson:编译器不需要为`printf`设置特殊的钩子,虽然例如gcc确实有一些它可以诊断错误(*如果*格式字符串是文字).编译器可以从`<stdio.h>`看到`printf`的声明,它告诉它第一个参数是`const char*`,其余的由`,...`表示.不,`%f`用于`double`(并且`float`被提升为`double`),'%lf`用于`long double`.C标准没有说明堆栈.它只在正确调用时才指定`printf`的行为. (2认同)
  • @ robertbristow-johnson:在古老的发呆中,"lint"经常进行一些gcc现在执行的额外检查.传递给`printf`的`float`被提升为`double`; 这没有什么神奇之处,它只是调用可变参数函数的语言规则.`printf`本身通过格式字符串知道调用者声称*传递给它的内容; 如果该声明不正确,则行为未定义. (2认同)
  • 小修正:`l`长度修饰符"对后续的`a`,`A`,`e`,`E`,`f`,`F`,`g`或`G`转换说明符没有影响","long double"转换的长度修饰符是"L".(@ robertbristow-johnson可能也有兴趣) (2认同)

zwo*_*wol 58

首先,正如其他几个答案所述,但在我看来,并没有明确说明:它在大多数情况下提供一个整数,其中库函数采用doublefloat参数.编译器将自动插入转换.例如,sqrt(0)定义明确并且行为完全sqrt((double)0)如此,对于那里使用的任何其他整数类型表达式也是如此.

printf是不同的.它是不同的,因为它需要可变数量的参数.它的功能原型是

extern int printf(const char *fmt, ...);
Run Code Online (Sandbox Code Playgroud)

因此,当你写

printf(message, 0);
Run Code Online (Sandbox Code Playgroud)

编译器不具有什么类型的任何信息,printf 预计这第二个参数是.它只有参数表达式的类型,也就是说int.因此,与大多数库函数不同,程序员可以确保参数列表与格式字符串的期望相匹配.

(现代编译器可以查看格式字符串并告诉您类型不匹配,但他们不会开始插入转换来实现您的意思,因为当您注意到时,您的代码现在应该更好地破解,多年后用一个不太有用的编译器重建.)

现在,问题的另一半是:给定(int)0和(float)0.0,在大多数现代系统中,都表示为32位,所有这些都是零,为什么它不能正常工作呢?C标准只是说"这不是必须工作的,你是独立的",但是让我说出为什么它不起作用的两个最常见的原因; 这可能会帮助你理解为什么它不是必需的.

首先,由于历史的原因,当你传递一个float通过可变参数列表,它被提拔double,这在大多数现代系统,是64个位宽.因此printf("%f", 0),只有32个零位传递给被调用者,期望64个.

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

  • @ Random832现在,可变参数和普通函数的调用约定之间的唯一区别是_may_是提供给可变参数的一些额外数据,例如提供的真实参数数量的计数.否则一切都与正常功能完全相同.例如,参见http://www.x86-64.org/documentation/abi.pdf的第3.2节,其中对可变参数的唯一特殊处理是在"AL"中传递的提示.(是的,这意味着`va_arg`的实现比以前复杂得多.) (3认同)

πάν*_*ῥεῖ 13

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

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

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

你会遇到与尝试时相同的未定义行为

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

  • `printf`的一些特定实现可能以这种方式工作(除了传递的项是值,而不是地址).C标准没有指定*how*`printf`和其他可变参数函数是否有效,它只是指定了它们的行为.特别是,没有提到堆栈帧. (3认同)

Mar*_*som 13

通常当你调用一个期望a的函数double但是你提供的函数时,int编译器会自动转换为a double.这种情况不会发生printf,因为函数原型中没有指定参数的类型 - 编译器不知道应该应用转换.

  • 此外,`printf()`*特别是*的设计使其参数可以是任何类型.您必须知道format-string中每个元素所期望的类型,并且必须正确提供它. (4认同)

chu*_*ica 12

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

如果转换规范无效,则行为未定义.C11dr§7.21.6.19

UB的候选原因.

  1. 根据规范是UB,编译是ornery - 'nuf说.

  2. double并且int具有不同的尺寸.

  3. double并且int可以使用不同的堆栈传递它们的值(一般与FPU堆栈.)

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


wyr*_*yrm 10

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

$ 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);
  ^
Run Code Online (Sandbox Code Playgroud)

要么

$ 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.
Run Code Online (Sandbox Code Playgroud)

因此,printf产生未定义的行为是因为您传递的是不兼容的参数类型.


Lig*_*ica 9

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

你的格式字符串需要一个double; 你代之以提供int.

这两种类型具有相同的位宽是完全不相关的,除了它可以帮助您避免从这样的破坏代码中获取硬内存冲突异常.

  • @Voo:那个格式字符串修饰符_is_不幸命名了,但我仍然不明白为什么你认为这里可以接受`int`. (3认同)
  • 令人困惑的是,对于_most_库函数,将一个整数文字"0"提供给一个类型为"double"的参数将做正确的事情.对于初学者来说,编译器不会对`%[efg]`所解决的`printf`参数槽进行相同的转换是不明显的. (2认同)