为什么printf(“%f”,0); 给未定义的行为?
语句printf("%f\n",0.0f);
打印0。
但是,语句printf("%f\n",0);
打印随机值。
我意识到我正在展示某种不确定的行为,但是我无法弄清楚为什么特别。
所有位都是0的float
值仍然是一个有效值为0的float
。
float
和int
在我的机器上是相同的大小(如果甚至是相关的)。
为什么在printf
中使用整数文字而不是浮点文字会导致这种行为?
"%f"
格式需要double
types的参数。 你给它一个int
types的参数。 这就是为什么行为是不确定的。
标准并不保证all-bits-zero是一个0.0
的有效表示(虽然通常是),或者是任何double
值,或者int
和double
是相同的大小(记住它是double
,而不是float
),或者,即使它们的大小相同,它们也会以相同的方式作为parameter passing给可变参数。
它可能碰巧在你的系统上“工作”。 这是未定义行为的最糟糕的可能症状,因为这使诊断错误变得困难。
N1570 7.21.6.1第9段:
…如果任何参数不是相应转换规范的正确types,则行为是不确定的。
float
types的参数被提升为double
,这就是为什么printf("%f\n",0.0f)
有效。 整数types比int
小的参数被提升为int
或unsigned int
。 这些促销规则(由N1570 6.5.2.2第6段规定)对于printf("%f\n", 0)
无效。
首先,正如其他几个答案中提到的那样,但是在我看来,没有足够清楚地说明:在大多数情况下,它的作用是在一个库函数采用double
或float
参数的情况下提供一个整数。 编译器会自动插入一个转换。 例如, sqrt(0)
定义良好,其行为与sqrt((double)0)
完全相同,对于其中使用的任何其他整型expression式也是如此。
printf
是不同的。 这是不同的,因为它需要可变数量的参数。 它的function原型是
extern int printf(const char *fmt, ...);
所以,当你写
printf(message, 0);
编译器没有任何有关printf
期望第二个参数的信息。 它只有参数expression式的types,它是int
,通过。 因此,与大多数库函数不同的是,编程人员需要确保参数列表与格式string的期望相匹配。
(现代编译器可以查看格式string,并告诉你,你有一个types不匹配,但他们不会开始插入转换来完成你的意思,因为更好的代码应该现在打破,当你会注意到,比几年以后用不太有用的编译器重新编译时)。
现在,问题的另一半是:在大多数现代系统中,(int)0和(float)0.0都表示为32位,全部为零,为什么无意中无法正常工作呢? C标准只是说“这不是必须工作,你是自己的”,但让我说出为什么它不工作的两个最常见的原因; 这可能会帮助你理解为什么它不是必需的。
首先,由于历史原因,当你通过一个variables参数列表传递一个float
值时,它被提升为 double
,在大多数现代系统中,这个值是64位宽。 所以printf("%f", 0)
只传递32个零位给一个被调用者,期望他们中的64个。
第二个同样重要的原因是浮点函数参数可能在整数参数不同的地方传递。 例如,大多数CPU具有用于整数和浮点值的单独的寄存器文件,所以可能是参数0到4进入寄存器r0到r4(如果它们是整数)的规则,但是如果它们是浮点的则从f0到f4。 所以printf("%f", 0)
在寄存器f1中查找那个零,但是根本不存在。
为什么使用整数文字而不是浮点文字会导致这种行为?
因为printf()
除了const char* formatstring
之外没有types化的参数, const char* formatstring
它和第一个一样。 它使用了一个c样式的省略号( ...
)。
这只是决定如何根据格式string中给出的格式types来解释通过那里的值。
你会有和尝试时一样的未定义的行为
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UB
通常当你调用一个需要double
的函数,但是你提供了一个int
,编译器会自动转换为你的double
。 这在printf
中不会发生,因为函数原型中没有指定参数的types – 编译器不知道应该应用转换。
使用不匹配的printf()
说明符"%f"
和types(int) 0
导致未定义的行为。
如果转换规范无效,则行为是不确定的。 C11dr§7.21.6.19
UB的候选原因。
-
这是每个规格的UB,编译是ornery – “nuf说。
-
double
和int
是不同的大小。 -
double
和int
可以使用不同的堆栈(通用与FPU堆栈)传递它们的值。 -
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("%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
会产生未定义的行为,因为你传递了一个不兼容的参数types。
我不确定有什么困惑。
你的格式string需要一个double
; 你提供一个int
。
这两种types是否具有相同的位宽是完全不相关的,除了它可以帮助您避免像这样破碎的代码得到硬内存违例exception。
仅当第二个printf()
参数的types为double
时, "%f\n"
才能保证可预测的结果。 接下来,可变参数的额外参数是默认参数提升的主题。 整数参数属于整数提升,从不会导致浮点types的值。 float
参数被提升了double
。
最好:标准允许第二个参数是或float
或double
,没有别的。
为什么这是正式的UB现在已经在几个答案中讨论。
具体的这个行为的原因是依赖于平台,但可能是以下几点:
-
printf
期望根据标准variables传播的参数。 这意味着一个float
将是一个double
float
而小于int
将是一个int
。 - 你传递一个
int
在函数期望double
。 你的int
可能是32位,你的double
64位。 这意味着从参数应该坐的地方开始的四个堆栈字节是0
,但接下来的四个字节有任意的内容。 这就是用来构build显示的值。
这个“未定值”问题的主要原因在于将指针转换为传递给printf
variables参数部分的int
值的指针,转换为va_arg
macros执行的double
types的指针。
这会导致引用一个未完全初始化的内存区域,并将值作为parameter passing给printf,因为double
大小的内存缓冲区大于int
大小。
因此,当这个指针被解引用时,它返回一个未定的值,或者更好的是一个“值”,其中包含的值作为parameter passing给printf
,其余部分可能来自另一个堆栈缓冲区甚至代码区(引发一个内存故障exception), 一个真正的缓冲区溢出 。
它可以考虑“printf”和“va_arg”的semplificated代码实现的这些特定部分…
的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。)中双值参数代码案例pipe理的真正实现是:
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);
引用
- gnu项目glibc执行“printf”(vprintf))
- printf的semplification代码的例子
- va_arg的semplification代码的例子