该声明
printf("%f\n",0.0f);
打印0。
但是,声明
printf("%f\n",0);
打印随机值。
我意识到我表现出某种不确定的行为,但我不知道具体为什么。
所有位均为0的浮点值仍然是有效的float
,值为0。
float
并且int
在我的计算机上具有相同的大小(如果相关)。
为什么使用整数文字而不是浮点文字printf
会导致此行为?
PS如果我使用可以看到相同的行为
int i = 0;
printf("%f\n", i);
该声明
printf("%f\n",0.0f);
打印0。
但是,声明
printf("%f\n",0);
打印随机值。
我意识到我表现出某种不确定的行为,但我不知道具体为什么。
所有位均为0的浮点值仍然是有效的float
,值为0。
float
并且int
在我的计算机上具有相同的大小(如果相关)。
为什么使用整数文字而不是浮点文字printf
会导致此行为?
PS如果我使用可以看到相同的行为
int i = 0;
printf("%f\n", i);
(uint64_t)0
而不是,以0
查看是否仍然出现随机行为(假设double
并uint64_t
具有相同的大小和对齐方式)。由于在不同的寄存器中传递不同的类型,因此在某些平台(例如x86_64)上,输出仍然可能是随机的。
Answers:
的"%f"
格式需要类型的参数double
。您正在给它一个类型为的参数int
。这就是行为未定义的原因。
该标准并不能保证所有位归零是一个有效的表示0.0
(尽管它经常是),或任何的double
价值,或者说int
并double
具有相同的尺寸(记住它的double
,不是float
),或者,即使是相同的大小,以相同的方式将它们作为参数传递给可变参数函数。
它可能会在您的系统上“工作”。这是未定义行为的最严重症状,因为这使诊断错误变得很困难。
N1570 7.21.6.1第9段:
...如果任何参数不是对应转换规范的正确类型,则行为未定义。
类型的参数float
被提升为double
,这就是为什么printf("%f\n",0.0f)
起作用的原因。整数类型的参数比int
提升为int
或窄unsigned int
。在的情况下,这些促销规则(由N1570 6.5.2.2第6段指定)无济于事printf("%f\n", 0)
。
请注意,如果将常量传递给0
需要double
参数的非变量函数,则假定函数的原型是可见的,则行为定义良好。例如,sqrt(0)
(after #include <math.h>
)隐式地将参数0
从转换int
为double
-,因为编译器可以从声明中sqrt
看到它需要一个double
参数。它没有的此类信息printf
。可变参数函数之类printf
的函数很特殊,在编写对其的调用时需要格外小心。
double
并不是float
这样,因此OP的宽度假设可能不成立(可能不成立)。第二,整数零和浮点零具有相同位模式的假设也不成立。做得好
float
一点double
,但没有说明原因,但是那不是重点。
printf
,例如,例如gcc确实有一些钩子,因此它可以诊断错误(如果格式字符串是文字)。编译器可以看到printf
from 的声明,该声明<stdio.h>
告诉它第一个参数是a const char*
,其余参数用表示, ...
。不,%f
是的double
(float
是,被提升为double
),%lf
是的long double
。C标准对堆栈一无所知。它指定printf
仅在正确调用时的行为。
float
传递到printf
被提升到double
; 没有什么神奇的,这只是调用可变参数函数的语言规则。printf
本身通过格式字符串知道调用方声称要传递给它的内容;如果该声明不正确,则行为是不确定的。
l
长度调节剂“对一个下面没有影响a
,A
,e
,E
,f
,F
,g
,或G
转换说明”,对于一个长度调节long double
转换L
。(@ robertbristow-johnson可能也会有兴趣)
首先,正如我在其他几个答案中所提到的那样,但我认为还不够清楚:在图书馆函数采用or或参数的大多数情况下,它确实可以提供整数。编译器将自动插入一个转换。例如,是定义明确的,并且行为将与完全相同,并且在那里使用的任何其他整数类型表达式也是如此。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标准只是说“这不是必需的,您自己一个人做”,但是让我说明为什么它不起作用的两个最常见的原因。可能会帮助您了解为什么不需要它。
首先,由于历史的原因,当你传递一个float
通过可变参数列表,它被提拔到double
,这在大多数现代系统,是64个位宽。因此printf("%f", 0)
,仅将32个零位传递给期望有64个零位的被调用方。
同样重要的第二个原因是,浮点函数参数可能在与整数参数不同的地方传递。例如,大多数CPU对于整数和浮点值都有单独的寄存器文件,因此可能的规则是,参数0到4如果是整数,则输入到寄存器r0到r4;如果参数是浮点,则输入f0到f4。因此,printf("%f", 0)
在寄存器f1中查找该零,但根本不存在。
()
。
AL
。(是的,这意味着实现va_arg
比以前复杂得多。)
ret n
8086 的指令,那里n
是一个硬编码的整数,因此不适用于可变函数。但是我不知道是否有任何C编译器实际上利用了它(非C编译器当然有)。
通常,当您调用需要a的函数double
但提供a时int
,编译器会自动double
为您转换为a 。不会发生这种情况printf
,因为在函数原型中未指定参数的类型-编译器不知道应应用转换。
printf()
尤其要进行设计,使其参数可以是任何类型。您必须知道format-string中每个元素期望的类型,并且必须正确提供。
为什么使用整数文字而不是浮点文字会导致此行为?
因为printf()
除了const char* formatstring
第一个参数外没有类型化的参数。...
其余所有内容均使用c形省略号()。
只是根据格式字符串中给出的格式类型决定如何解释在那里传递的值。
您将具有与尝试时相同的不确定行为
int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB
printf
可能会以这种方式工作(除了传递的项目是值,而不是地址)。C标准没有规定如何 printf
和其他参数可变功能的工作,它只是规定了他们的行为。特别是,没有提到堆栈帧。
printf
确实有一个类型化参数,即格式字符串,类型为const char*
。顺便说一句,这个问题被标记为C和C ++,而C确实更相关。我可能不会reinterpret_cast
举个例子。
使用不匹配的printf()
说明符"%f"
和类型(int) 0
会导致未定义的行为。
如果转换规范无效,则行为未定义。C11dr§7.21.6.19
UB的候选原因。
根据规范,它是UB,编译是精炼-'nuf说。
double
并且int
大小不同。
double
并int
可以使用不同的堆栈(常规vs. 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
正在产生未定义的行为,因为您正在向它传递不兼容的参数类型。
我不确定是什么令人困惑。
您的格式字符串应为double
; 您改为提供int
。
两种类型是否具有相同的位宽完全无关紧要,除了它可以帮助您避免像这样的破损代码避免硬存储器冲突异常。
int
此处可以接受。
int
限定为有效的浮动模式?二进制补码和各种浮点编码几乎没有共同之处。
0
给键入的参数double
将可以解决问题。对于初学者来说,编译器不会对printf
寻址的参数槽进行相同的转换,这并不明显%[efg]
。
造成此“不确定值”问题的主要原因在于,将指针转换为int
传递给printf
变量参数部分的值,并将其转换为宏执行的double
类型的指针va_arg
。
这会导致引用未完全初始化的内存区域,而该内存区域未使用作为参数传递给printf的值进行初始化,因为double
size内存缓冲区区域大于int
size。
因此,取消引用此指针时,将返回一个不确定的值,或者更好的是一个“值”,该值部分包含作为参数传递给的值printf
,对于其余部分,它可能来自另一个堆栈缓冲区甚至是一个代码区(引发内存故障异常), 真正的缓冲区溢出。
它可以考虑的“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。)是:
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);
参考资料
printf
期待一个double
,而你给它一个int
。float
并且int
可能是你机器上的大小相同,但0.0f
实际上是转换为double
当推入可变参数的参数列表(和printf
期望)。简而言之,printf
根据您使用的说明符和所提供的参数,您无法实现讨价还价的目的。