声明
printf("%f\n",0.0f);
打印0。
然而,声明
printf("%f\n",0);
打印随机值。
我意识到我表现出某种不明确的行为,但我无法弄清楚具体原因。
所有位为0的浮点值仍然是有效值float
,值为0
我的计算机上float
和int
的大小相同(如果相关的话)。
为什么在printf
中使用整数文字而不是浮点文字导致此行为?
P.S。如果我使用
,可以看到相同的行为int i = 0;
printf("%f\n", i);
答案 0 :(得分:119)
"%f"
格式需要double
类型的参数。你给它一个int
类型的参数。这就是行为未定义的原因。
标准不保证all-bits-zero是0.0
的有效表示(尽管通常是),或任何double
值,或int
和{ {1}}大小相同(请记住它double
,而不是double
),或者,即使它们的大小相同,它们也会作为参数传递给可变函数以相同的方式。
它可能发生在"工作"在你的系统上。这是未定义行为的最坏可能症状,因为它很难诊断错误。
N1570 7.21.6.1第9段:
...如果任何参数不是相应的正确类型 转换规范,行为未定义。
类型float
的参数会提升为float
,这就是double
有效的原因。比printf("%f\n",0.0f)
窄的整数类型的参数被提升为int
或int
。这些促销规则(由N1570 6.5.2.2第6段规定)对unsigned int
的情况没有帮助。
答案 1 :(得分:58)
首先,正如其他几个答案所述,但在我看来,并没有明确说明: 工作在大多数上下文中提供整数库函数采用double
或float
参数。编译器将自动插入转换。例如,sqrt(0)
定义明确,行为与sqrt((double)0)
完全相同,对于那里使用的任何其他整数类型表达式也是如此。
printf
不同。它有所不同,因为它需要可变数量的参数。它的功能原型是
extern int printf(const char *fmt, ...);
因此,当你写
printf(message, 0);
编译器没有关于printf
期望第二个参数的类型的任何信息。它只有参数表达式的类型,即int
。因此,与大多数库函数不同,程序员可以确保参数列表与格式字符串的期望相匹配。
(现代编译器可以查看格式字符串并告诉您类型不匹配,但他们不会开始插入转换来实现您的意思,因为当你注意到时,你的代码现在应该更好,而不是几年后用一个不太有用的编译器重建时。)
现在,问题的另一半是:给定(int)0和(float)0.0,在大多数现代系统中,都表示为32位,所有这些都是零,为什么它不起作用无论如何,偶然? C标准只是说"这不是必须工作的,你是自己的#34;但是让我说明为什么它不能工作的两个最常见的原因;这可能会帮助您理解为什么它不是必需的。
首先,由于历史原因,当您通过变量参数列表传递float
时,它会提升到double
,在大多数现代系统中,它是 64 位宽。所以printf("%f", 0)
只向被调用者传递32个零位,期望64个。
第二个同样重要的原因是浮点函数参数可以在与整数参数不同的位置中传递。例如,大多数CPU具有用于整数和浮点值的单独寄存器文件,因此,如果它们是整数,则参数0到4可以进入寄存器r0到r4,如果它们是浮点,则可以是f0到f4。因此printf("%f", 0)
在寄存器f1中查找该零,但它根本不存在。
答案 2 :(得分:13)
为什么使用整数文字而不是浮点文字会导致此行为?
因为printf()
除了const char* formatstring
之外没有类型参数作为第一个参数。其余部分使用c样式省略号(...
)。
它只是决定如何根据格式字符串中给出的格式类型来解释传递的值。
你会遇到与尝试
时相同的未定义行为 int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB
答案 3 :(得分:13)
通常,当您调用期望double
的函数但提供int
时,编译器会自动为您转换为double
。 printf
不会发生这种情况,因为函数原型中没有指定参数的类型 - 编译器不知道应该应用转换。
答案 4 :(得分:12)
使用不匹配的printf()
说明符"%f"
并输入(int) 0
会导致未定义的行为。
如果转换规范无效,则行为未定义。 C11dr§7.21.6.19
UB的候选原因。
根据规范是UB,编译是ornery - 'nuf说。
double
和int
的大小不同。
double
和int
可以使用不同的堆栈传递其值(一般与FPU堆栈。)
double 0.0
可能不能由全零位模式定义。 (罕见)
答案 5 :(得分: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);
^
或
$ 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
正在产生未定义的行为,因为您传递的是不兼容的参数类型。
答案 6 :(得分:9)
我不确定是什么令人困惑。
您的格式字符串需要double
;您提供的是int
。
这两种类型是否具有相同的位宽是完全不相关的,除了它可以帮助您避免从这样的破坏代码中获取硬内存冲突异常。
答案 7 :(得分:4)
"%f\n"
参数的类型为printf()
时, double
才能保证可预测的结果。接下来,可变参数函数的额外参数是默认参数提升的主题。整数参数属于整数提升,它永远不会产生浮点类型的值。 float
参数会提升为double
。
最重要的是:标准允许第二个参数为float
或double
而不是其他任何内容。
答案 8 :(得分:4)
为什么正式的UB现在已经在几个答案中进行了讨论。
您具体获取此行为的原因与平台有关,但可能如下:
printf
期望其参数符合标准的vararg传播。这意味着float
将是double
,任何小于int
的内容都将是int
。int
的{{1}}。您的double
可能是32位,即int
64位。这意味着从应该参数所在的位置开始的四个堆栈字节是double
,但是后面的四个字节具有任意内容。这是用于构造显示的值的内容。答案 9 :(得分:0)
这个"未定值的主要原因"问题代表在int
值传递给printf
变量参数部分的指针的转换为double
宏执行的va_arg
类型的指针。
这会导致引用未完全初始化的内存区域,并将值作为参数传递给printf,因为double
大小的内存缓冲区大于int
大小。
因此,当该指针被解除引用时,它返回一个未确定的值,或更好的"值"其中包含作为参数传递给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..
....
在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);
<强> 引用 强>