因此,在严格的别名规则下,有几件事是明确允许的(为了清楚起见,让我们在 C23 中这样做):
第一个也是最明显的是结构体可以使用指向其初始成员的指针进行别名:
typedef struct {
int data;
} parent;
typedef struct {
parent _base;
int metadata;
} child;
int main() {
child child_obj = {};
parent* parent_ptr = (parent*) &child_obj; // Fine to read and write
int* int_ptr = (int*) parent_ptr; // Also fine
}
第二个是具有公共初始序列的对象的联合允许访问这些公共元素,并且指向联合的指针可以自由转换为/可以作为指向其任何元素的指针进行访问。
typedef struct {
int type_id;
size_t size;
char* buffer;
} dynamic_string;
typedef struct {
int type_id;
size_t number;
} just_a_number;
typedef union {
dynamic_string dystr;
just_a_number num;
struct {
int type_id;
char fixed_string[32];
};
} string_or_num;
int main() {
string_or_num obj = {};
if(obj.type_id == 2) // Fine, common initial sequence
memcpy("Hello World!", obj.fixed_string, 13);
// Fine
dynamic_string* ds_ptr = (dynamic_string*) &obj;
just_a_number* num_ptr = (just_a_number*) &obj;
// Also fine, pointer to initial common member
int* int_ptr = (int*) &obj;
}
直观上,我认为我可以将它们组合成如下所示。然而,我对自己的标准没有足够的信心,无法 100% 确定它是犹太洁食
typedef struct {
int type_id;
char data[4];
} parent_a;
typedef struct {
int type_id;
float decimal;
} parent_b;
// No initial sequence
typedef struct {
double ccccombo_breaker;
} parent_c;
typedef struct {
union {
parent_a _base_a;
parent_b _base_b;
parent_c _base_c;
};
int look_at_me_ive_got_three_parents;
} child;
int main() {
child child_obj = {};
// Are these kosher?
parent_a* a_ptr = (parent_a*) &child_obj;
parent_b* b_ptr = (parent_b*) &child_obj;
// How about this?
parent_c* c_ptr = (parent_c*) &child_obj;
double* db_ptr = (double*) &child_obj;
}
需要明确的是,我并不是在问像
parent_c
这样的东西是否是一个好主意,只是问标准对此的看法。通过这些指针的读写会遵循别名规则吗?
如果您拥有标准中的准确语言或标准部分的组合来形成令人信服的案例,则会获得加分。
这些是独立但大部分兼容的规则:
指向结构体第一个成员/联合体任何成员的指针(6.7.2.1 §15 §16),特别是:
的大小足以容纳其最大的成员。的值在 大多数成员可以随时存储在union
对象中。一个指向a的指针union
对象,经过适当转换,指向其每个成员(或者如果成员是位字段, 然后到它所在的单位),反之亦然。union
使用指向字符的指针逐字节检查任何类型 (6.3.2.3 §7)
“严格别名”(第 6.5 章 §6 和 §7)
通用初始序列(6.5.2.3)
此外,还有关于“联合类型双关”(6.5.2.3 §3)的规则,它允许联合的成员被转换/表达为不同的类型,尽管这可能会在以下情况下引发各种定义不明确的行为:未对准、超出范围值、无效/陷阱表示等。
您的问题主要是关于第一条规则:
parent_a* a_ptr = (parent_a*) &child_obj;
parent_b* b_ptr = (parent_b*) &child_obj;
parent_c* c_ptr = (parent_c*) &child_obj;
根据“工会的任何成员”规则,这些都可以。匿名结构/联合意味着
_base_a
、_base_b
和 _base_c
中的任何一个都是“联合的任何成员”,因此转换是“适当转换的”。这些类型碰巧有一个共同的初始序列,这并不真正相关。我们可以有一个非常不兼容的类型的联合。
然而,double* db_ptr = (double*) &child_obj;
有点可疑,因为child_obj
的第一个对象不是double
。指针转换本身几乎总是好的,C 允许对象指针之间几乎任何疯狂的转换(6.3.2.3 §7)。
但是当您取消引用
db_ptr
时,那么您就处于更可疑的领域 - “指向联合体任何成员的指针”规则不适用,因此它变成了严格别名的问题。反过来,这并不反对对可能是 double
的东西进行 double
左值访问。如果存储在那里的二进制内容(全为零)也可以表示为 double
,那么理论上一切都很好。
值得注意的是,这些规则的实际编译器实现的历史并不是很漂亮(特别是不严格的别名和常见的初始序列)。标准对很多事情都不清楚,最好不要依赖标准所说/似乎所说的任何内容,因为这不一定是某个特定编译器解释它的方式。另外,一些编译器甚至没有成为高质量实现的雄心。最佳实践是不要相信编译器能够正确完成所有这些操作。例如,最新的gcc 13.2在面对提到的“逐字节检查”规则时完全变得疯狂,该规则至少从C99开始就已经在C中出现了。