[英]Why cast to a pointer then dereference?
我正在浏览这个例子,它有一个 function 输出一个十六进制位模式来表示任意浮点数。
void ExamineFloat(float fValue)
{
printf("%08lx\n", *(unsigned long *)&fValue);
}
为什么取 fValue 的地址,转换为无符号长指针,然后取消引用? 所有这些工作不都等同于直接转换为 unsigned long 吗?
printf("%08lx\n", (unsigned long)fValue);
我试过了,答案不一样,很困惑。
(unsigned long)fValue
这会根据“通常的算术转换”将float
值转换为unsigned long
值。
*(unsigned long *)&fValue
这里的目的是获取存储fValue
的地址,假设在该地址处没有float
而是unsigned long
整数,然后读取该unsigned long
。 目的是检查用于将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
的字节,对你来说更实用)。
浮点值具有内存表示:例如,字节可以表示使用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
的值转换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
几乎肯定会按照预期的那样做,即打印浮动的十六进制表示有问题的点值。
这应该不足为奇。 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);
这很简单,易于携带,并且不存在未定义的行为。 这将打印所讨论的双精度浮点值的十六进制/二进制表示。 由于printf
是一个古老的函数,所以所有float
参数在调用printf
之前都会转换为double
。 根据1999版C标准,此转换必须准确。 人们可以通过调用scanf
或其姐妹来获取确切的值。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.