为什么要将float类型的指针转换为long类型的指针,然后解引用呢?

Why cast a pointer to a float into a pointer to a long, then dereference?

本文关键字:指针 类型 然后 引用 转换 float 为什么 long      更新时间:2023-10-16

我正在经历这个例子,它有一个函数输出十六进制位模式来表示任意浮点数。

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

为什么取fValue的地址,转换为无符号长指针,然后解引用?所有这些工作不都等同于直接转换为unsigned long吗?

printf("%08lxn", (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 (请注意,我对"compatible"的使用与参考文本不同-更广泛。)

解决方案:转换为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所指出的),上面将浮点值视为首先存储其最高位字节("大端")。如果您的系统对浮点值使用不同的字节顺序,则需要进行调整(或重新排列以上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)fValuefValue的值转换为unsigned long,使用floatunsigned long的正常转换规则。该值在unsigned long中的表示(例如,以位表示)可能与float中的表示完全不同。

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

C中的类型转换既进行类型转换又进行值转换。浮点数→Unsigned long转换将截断浮点数的小数部分,并将值限制在Unsigned long的可能范围内。从一种类型的指针转换为另一种类型的指针不需要改变值,因此使用指针类型转换是在改变与该表示相关联的类型的同时保持内存中相同的表示的一种方法。

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

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

printf("%08lxn", *(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——,也就是说。


union hack是解决这个问题的一种方法:

typedef struct {
    float fval;
    uint32_t ival;
} float_uint32_t;

赋值给float_uint32_t.fval并从float_uint32_t进行访问。竞争曾经是一种未定义的行为。在c语言中不再是这种情况了。据我所知,没有一个编译器会因为联合攻击而怒斥鼻魔。这不是c++中的UB。这是非法的。直到c++ 11,一个兼容的c++编译器必须抱怨它是兼容的。


解决这种混乱的更好方法是使用%a格式,它自1999年以来一直是C标准的一部分:

printf ("%an", fValue);

这很简单,容易,可移植,并且没有未定义行为的机会。这将打印所讨论的双精度浮点值的十六进制/二进制表示。由于printf是一个古老的函数,所以在调用printf之前,所有float参数都被转换为double。根据1999版的C标准,这种转换必须是精确的。可以通过调用scanf或它的姊妹函数来获取这个精确的值。

相关文章: