为什么要转换为指针然后取消引用?

我正在通过这个例子,它有一个函数输出一个hex位模式来表示任意浮点数。

void ExamineFloat(float fValue) { printf("%08lx\n", *(unsigned long *)&fValue); } 

为什么要取fValue的地址,转换为无符号长指针,然后取消引用? 是不是所有的工作都等同于直接转换为无符号长?

 printf("%08lx\n", (unsigned long)fValue); 

我试过了,答案不一样,很困惑。

 (unsigned long)fValue 

根据“通常的算术转换”,这会将float值转换为unsigned long值。

 *(unsigned long *)&fValue 

这里的意图是获取存储fValue的地址,假设在该地址处没有float而是unsigned long整数,然后读取该unsigned long 。 目的是检查用于将float存储在内存中的位模式。

如图所示,这会导致未定义的行为。

原因:您可能无法通过指向与对象类型“不兼容”的类型的指针来访问对象。 “兼容”类型是例如( unsignedchar和每个其他类型,或者共享相同初始成员的结构(在这里说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的字节,对你来说更实用)。

浮点值具有内存表示:例如,字节可以表示使用IEEE 754的浮点值。

第一个表达式*(unsigned long *)&fValue将这些字节解释为unsigned long值的表示 。 事实上,在C标准中,它会导致未定义的行为(根据所谓的“严格别名规则”)。 在实践中,必须考虑诸如字节序之类的问题。

第二个表达式(unsigned long)fValue符合C标准。 它有一个确切的含义:

C11(n1570),§6.3.1.4实数浮点数和整数

当实数浮动类型的有限值被转换为除_Bool之外的整数类型时,小数部分被丢弃(即,该值被截断为零)。 如果整数部分的值不能用整数类型表示,则行为是未定义的。

*(unsigned long *)&fValue不等于直接转换为unsigned long

转换为(unsigned long)fValue(unsigned long)fValue的值转换为unsigned long ,使用将float值转换为unsigned long值的常规规则。 unsigned long整数中的该值的表示(例如,就位而言)可能与在float表示相同值的方式完全不同。

转换*(unsigned long *)&fValue正式具有未定义的行为。 它将fValue占用的内存解释为unsigned long fValue 。 实际上(即经常发生这种情况,即使行为未定义),这通常会产生与fValue完全不同的值。

C中的类型转换同时进行类型转换和值转换。 浮点→无符号长转换会截断浮点数的小数部分,并将值限制为无符号长整数的可能范围。 从一种类型的指针转​​换为另一种指针没有必要的值更改,因此使用指针类型转换是一种在更改与该表示关联的类型时保持相同的内存中表示的方法。

在这种情况下,它是一种能够输出浮点值的二进制表示的方法。

正如其他人已经注意到的那样,将指向非char类型的指针转​​换为指向不同非char类型的指针然后解除引用是未定义的行为。

printf("%08lx\n", *(unsigned long *)&fValue)调用未定义的行为并不一定意味着运行试图执行此类歪曲的程序将导致硬盘擦除或使鼻子从鼻子中爆发(未定义行为的两个标志)。 在sizeof(unsigned long)==sizeof(float)并且两个类型具有相同对齐要求的计算机上, printf几乎肯定会按照预期的那样做,即打印浮动的hex表示有问题的点值。

这应该不足为奇。 C标准公开邀请实现来扩展语言。 许多这些扩展都在严格来说是未定义行为的领域。 例如,POSIX函数dlsym返回void* ,但此函数通常用于查找函数的地址而不是全局变量。 这意味着需要将dlsym返回的void指针dlsym转换为函数指针,然后取消引用以调用该函数。 这显然是未定义的行为,但它仍适用于任何符合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); 

这很简单,易于携带,并且不存在未定义的行为。 这将打印所讨论的双精度浮点值的hex/二进制表示。 由于printf是一个古老的函数,所以所有float参数在调用printf之前都会转换为double 。 根据1999版C标准,此转换必须准确。 人们可以通过调用scanf或其姐妹来获取该确切的值。