C语言中使用联合的指针成员进行类型双关是否属于未定义行为(UB)?
这个问题的答案得结合C标准规则和具体平台的实现细节来看,不能一概而论——在大多数主流平台(比如x86_64、ARM)上,你的代码是安全且符合预期的,但从C标准的严格定义来说,它存在潜在的未定义行为风险,具体原因如下:
先看C标准的核心规则
以C17标准为例,它对联合的类型双关有明确规定:
当读取联合的非活跃成员(即最后一次赋值的成员之外的成员)时,会将该成员对应的内存表示重新解释为新成员类型的对象表示(这个过程常被称为“类型双关”),除非这个新的对象表示是陷阱表示,否则行为是定义明确的。
放到你的场景里,需要判断两个关键问题:
int_sequence和untyped_sequence的内存布局是否兼容?- 将
int*的对象表示重新解释为void*是否会产生陷阱表示?
1. 结构体的内存布局兼容性
你的两个结构体定义:
typedef struct { void* data; size_t size; size_t item_size; } untyped_sequence; typedef struct { int* data; size_t size; size_t item_size; } int_sequence;
它们的成员顺序、数量完全一致,唯一区别是第一个成员的指针类型不同。在几乎所有主流平台上:
void*和int*的大小相同(都是平台的指针宽度,比如64位系统上是8字节)- 结构体的填充规则一致(两个结构体都不会在成员之间产生额外的填充字节)
所以它们的内存布局完全重合,联合中typed和untyped成员的每个字节都对应相同位置。但C标准不强制要求不同指针类型的大小相同,也不强制要求不同结构体(即使成员布局相似)的填充规则一致——如果在某个特殊平台上,void*的大小比int*大,或者结构体有不同的填充,那两个结构体的内存布局就不兼容,此时访问非活跃成员就是UB。
2. 指针类型的对象表示与陷阱表示
C标准允许不同的指针类型有不同的对象表示,也允许指针类型存在陷阱表示(即某些内存值不是有效的指针值)。比如在一些古老的分段内存平台上,int*和void*可能有不同的段属性,此时把int*的内存表示直接当作void*使用,就会得到一个无效的void*,这属于陷阱表示,访问它就是UB。
但在主流的平坦内存模型平台上,void*和int*的对象表示完全相同,不存在陷阱表示的问题,所以这种类型双关是安全的。
你的代码为什么在主流编译器上能正常工作?
GCC、Clang、MSVC这些主流编译器都针对常见平台做了优化,它们默认假设指针类型的大小和表示一致,结构体的填充规则也统一,所以会正确处理这种联合类型双关的场景,不会产生警告,运行结果也符合预期。但这只是编译器的实现行为,不是C标准的强制要求。
如何避免潜在的UB?
如果你想写出完全符合C标准、没有UB风险的代码,不需要用联合来做类型双关,直接显式构造untyped_sequence对象即可:
// 替换原来的调用 untyped_zero_first((untyped_sequence){ .data = s.typed.data, .size = s.typed.size, .item_size = s.typed.item_size });
这种方式通过显式转换和初始化,完全符合C标准的规则,在任何平台上都是安全的。
备注:内容来源于stack exchange,提问作者Johann Gerell

