PAYATHON 2026

C 中 unsigned char * 的 strict aliasing 豁免有何作用?

数据小许

unsigned char * 可以按字节合法查看或复制任意对象的存储表示,但这些字节的布局并不固定。

你的第二种写法避免了第一种写法中的严格别名违规,但 pointer[7] 的最高位是否是 double 的符号位,仍取决于平台。

第一种写法为什么有问题?

这里声明的对象是 double:

double result = number;

随后,代码通过 unsigned long 类型的左值读写该对象:

unsigned long * pointer = (unsigned long *)&result;
if (*pointer >> 63) *pointer ^= 1ul << 63;

即使 double 和 unsigned long 恰好大小相同,也不能通过 unsigned long 类型的左值这样访问 double 对象。通过 *pointer 读取或修改它会违反 C 的对象访问类型规则,产生 undefined behaviour。

这里需要区分指针转换和对象访问:

  • 转换指针不会把 double 对象变成 unsigned long 对象,也不会完成数值转换。
  • 通过转换后的指针访问对象,才涉及这里的严格别名问题。指针转换本身还需要满足相应的对齐要求。

例如,(unsigned long)result 表示数值转换,与 (unsigned long *)&result 的含义完全不同。

源码中的 result 确实已经初始化,不能把该警告简单解释成漏写了初始化。程序存在 undefined behaviour 时,编译器的优化和数据流分析可能给出看似矛盾的诊断;但仅凭这段警告,无法确定具体编译器为何给出这个提示。

为什么字符类型可以访问任意对象?

C 允许通过字符类型的左值访问对象的表示。字符类型包括 char、signed char 和 unsigned char,并非只有 unsigned char 具有这种权限。

unsigned char 通常最适合处理原始字节,因为它没有符号解释的问题,而且每一种位模式都表示一个有效值。

例如,可以合法地查看一个 double 的各个存储字节:

#include <stddef.h>
#include <stdio.h>

void dump_bytes(const void *object, size_t size)
{
    const unsigned char *bytes = object;
    for (size_t i = 0; i < size; ++i) {
        printf("%02x ", (unsigned int)bytes[i]);
    }
    putchar('\n');
}

int main(void)
{
    double value = -2.0;
    dump_bytes(&value, sizeof value);
    return 0;
}

这段代码可以读取对象表示,但输出的字节顺序和具体内容取决于实现。

字符类型访问可用于检查对象表示、复制存储内容,以及实现按字节工作的通用内存操作。不过,直接复制结构体或浮点对象的内存,并不能保证得到可跨平台使用的文件格式或网络格式。

为什么 int *、short * 和 long * 没有相同权限?

严格别名规则让编译器能够根据访问类型判断两个指针是否可能指向同一对象,从而省去不必要的重复读取或调整指令顺序。

如果所有整数类型都可以任意访问其他类型的对象,一次整数写入就可能修改附近代码正在使用的浮点对象,编译器能够依赖的类型信息也会明显减少。

字符类型为通用存储操作提供了一个例外:程序可以逐字节访问对象,而不必让所有类型都获得任意别名权限。其他整数类型还可能有更严格的对齐要求、填充位等限制,并不适合作为通用字节访问单位。

因此,不能把规则概括为“整数地址和浮点地址之间不能转换”。能否访问对象,取决于访问时使用的类型是否被允许。C 还允许其他情况,例如兼容类型、对应的有符号与无符号类型。

unsigned char 版本还依赖哪些条件?

下面这段代码对浮点布局作出了具体假设:

if ((pointer[7] >> 7) & 1) pointer[7] ^= 1 << 7;

要把它解释成清除 double 的符号位,至少需要确认:

  • 一个字节是 8 位。
  • double 占 8 个字节,并采用预期的浮点表示,例如 IEEE 754 binary64。
  • 该表示在内存中的排列使第 8 个字节的最高位恰好是符号位。

在常见的、采用小端存储的 IEEE 754 binary64 平台上,这种布局通常成立;C 语言本身没有统一保证这些条件。

原来的 unsigned long 版本也有额外问题:unsigned long 不保证是 64 位。如果它只有 32 位,*pointer >> 63 和 1ul << 63 的移位次数就超出了类型宽度,同样会产生 undefined behaviour。

字符访问权限也不保证任意修改后的字节都能表示有效的 double。若修改产生了实现不允许读取的表示,之后再以 double 类型读取仍可能出问题。

这段 magnitude() 应该怎样修改?

如果只是为平方根迭代计算普通有限数的绝对值,可以直接使用数值运算,无须依赖对象布局:

double magnitude(double number)
{
    return number < 0.0 ? -number : number;
}

这个版本不需要 math.h,也没有上述别名、位宽和字节序问题。不过,传入 -0.0 时会保留 -0.0;它也不保证清除 NaN 的符号位。

如果需要标准浮点绝对值函数的语义,应使用声明于 math.h 的 fabs()。如果确实需要将对象表示复制到整数对象中处理,可以考虑 memcpy():它能避免通过不兼容类型指针解引用,但仍需单独确认整数宽度、浮点格式和位排列。

指针转换是否影响优化,要看它带来了什么访问关系。合法的字符类型访问可能让编译器考虑更多别名情况,而违反规则的访问没有可靠的程序语义,不能用来换取优化。

另外,平方根迭代中的固定阈值 1e-16 是绝对误差判据,不能保证适用于所有数量级;修复 magnitude() 并不等于验证了整个平方根算法的精度和收敛性。

备注:内容仅供参考。