我正在浏览这个示例,它有一个输出十六进制位模式的函数来表示任意浮点数。
void ExamineFloat(float fValue)
{
printf("%08lx\n", *(unsigned long *)&fValue);
}
为什么要取fValue的地址,转换为无符号长指针,然后取消引用?是不是所有的工作都等同于直接转换为无符号长?
printf("%08lx\n", (unsigned long)fValue);
我试过了,答案不一样,很困惑。
答案 0 :(得分:27)
(unsigned long)fValue
根据“通常的算术转换”,这会将float
值转换为unsigned long
值。
*(unsigned long *)&fValue
此处的目的是获取存储fValue
的地址,假设此地址没有float
但unsigned long
,然后读取unsigned long
1}}。目的是检查用于在内存中存储float
的位模式。
如图所示,这会导致未定义的行为。
原因:您可能无法通过指向与对象类型“不兼容”的类型的指针来访问对象。 “兼容”类型例如是(unsigned
)char
以及每个其他类型或共享相同初始成员的结构(在此处说到C)。有关详细(C11)列表,请参阅§6.5/ 7 N1570(请注意,我对“兼容”的使用与引用文本中的使用不同 - 更广泛。)
解决方案:转换为unsigned char *
,访问对象的各个字节并从中汇集unsigned long
:
unsigned long pattern = 0;
unsigned char * access = (unsigned char *)&fValue;
for (size_t i = 0; i < sizeof(float); ++i) {
pattern |= *access;
pattern <<= CHAR_BIT;
++access;
}
注意(正如@CodesInChaos指出的那样)上面将浮点值视为首先存储其最高有效字节(“big endian”)。如果你的系统对浮点值使用不同的字节顺序,你需要调整它(或重新排列上面unsigned long
的字节,对你来说更实用)。
答案 1 :(得分:4)
浮点值具有内存表示形式:例如,字节可以使用IEEE 754表示浮点值。
第一个表达式*(unsigned long *)&fValue
将这些字节解释为unsigned long
值的表示。事实上,在C标准中,它会导致未定义的行为(根据所谓的“严格别名规则”)。在实践中,必须考虑诸如字节序之类的问题。
第二个表达式(unsigned long)fValue
符合C标准。它有一个确切的含义:
C11(n1570),§6.3.1.4实际浮动和整数
当实数浮动类型的有限值被转换为除
_Bool
以外的整数类型时,小数部分被丢弃(即,该值被截断为零)。如果整数部分的值不能用整数类型表示,则行为是未定义的。
答案 2 :(得分:4)
*(unsigned long *)&fValue
不等同于直接转换为unsigned long
。
转换为(unsigned long)fValue
会将fValue
的值转换为unsigned long
,使用将float
值转换为unsigned long
值的常规规则。 unsigned long
中该值的表示(例如,就位而言)可能与float
中表示相同值的方式完全不同。
转换*(unsigned long *)&fValue
正式具有未定义的行为。它将fValue
占用的内存解释为unsigned long
。实际上(即经常发生这种情况,即使行为未定义),这通常会产生与fValue
完全不同的值。
答案 3 :(得分:3)
C中的类型转换同时进行类型转换和值转换。浮点→无符号长转换会截断浮点数的小数部分,并将值限制为无符号长整数的可能范围。从一种类型的指针转换为另一种指针没有必要的值更改,因此使用指针类型转换是一种在更改与该表示关联的类型时保持相同的内存中表示的方法。
在这种情况下,它是一种能够输出浮点值的二进制表示的方法。
答案 4 :(得分:1)
正如其他人已经注意到的那样,将指向非char类型的指针转换为指向不同非char类型的指针然后解除引用是未定义的行为。
printf("%08lx\n", *(unsigned long *)&fValue)
调用未定义的行为并不一定意味着运行试图执行此类歪曲的程序将导致硬盘擦除或使鼻子从鼻子中爆发(未定义行为的两个标志)。在sizeof(unsigned long)==sizeof(float)
并且两种类型具有相同对齐要求的计算机上,printf
几乎肯定会执行人们期望它做的事情,即打印浮点值的十六进制表示问题。
这应该不足为奇。 C标准公开邀请实现来扩展语言。许多这些扩展都在严格来说是未定义行为的领域。例如,POSIX函数dlsym返回void*
,但此函数通常用于查找函数的地址而不是全局变量。这意味着dlsym
返回的void指针需要转换为函数指针,然后取消引用以调用该函数。这显然是未定义的行为,但它仍适用于任何符合POSIX标准的平台。这在哈佛架构机器上不起作用,在该机器上,函数指针的大小与数据指针的大小不同。
类似地,将指向float
的指针转换为指向无符号整数的指针然后解除引用几乎适用于任何几乎任何编译器的计算机,其中该无符号整数的大小和对齐要求相同作为float
。
那就是说,使用unsigned long
可能会让你陷入困境。在我的计算机上,unsigned long
长度为64位,具有64位对齐要求。这与浮点数不兼容。最好在我的电脑上使用uint32_t
- 就是这样。
工会黑客是解决这个烂摊子的一种方式:
typedef struct {
float fval;
uint32_t ival;
} float_uint32_t;
分配给float_uint32_t.fval
并从``float_uint32_t.ival`访问曾经是未定义的行为。在C中不再是这种情况。我知道没有编译器为工会黑客吹嘘鼻子恶魔。这不是C ++中的UB。这是非法的。在C ++ 11之前,兼容的C ++编译器不得不抱怨是合规的。
围绕这个混乱的任何更好的方法是使用%a
格式,该格式自1999年以来一直是C标准的一部分:
printf ("%a\n", fValue);
这很简单,易于携带,并且不存在未定义行为的可能性。这将打印所讨论的双精度浮点值的十六进制/二进制表示。由于printf
是一个古老的函数,因此在调用float
之前,所有double
个参数都会转换为printf
。根据1999版C标准,此转换必须准确。人们可以通过致电scanf
或其姐妹来获取确切的价值。