C语言中初始成员、公共初始序列、匿名联合体与严格别名规则的交互及场景合法性问询
咱们先从你已经摸透的两个核心规则说起,再一步步拆解你提出的这个混合场景——毕竟C的严格别名规则确实容易绕晕人😅
先复习下你提到的两个合法场景(对应C23标准)
1. 结构体与初始成员的指针别名完全合法
你举的parent和child例子完全正确:C23明确规定,指向结构体对象的指针可以安全转换为指向其第一个成员的指针,反之亦然(对应C23 6.7.2.1 §15)。所以你写的:
child child_obj = {}; parent* parent_ptr = (parent*) &child_obj; // 完全合规,读写都没问题 int* int_ptr = (int*) parent_ptr; // 同样合法
完全符合标准,因为parent是child的第一个成员,而int又是parent的第一个成员,嵌套的转换也受规则保护。
2. 联合体指针与成员指针自由转换,公共初始序列可交叉访问
你写的string_or_num联合体例子也没毛病:C23允许指向联合体对象的指针转换为指向其任意成员的指针,反之亦然(6.7.2.1 §16)。另外,对于共享公共初始序列的结构体成员(比如三个结构体都以int type_id作为第一个成员),只要它们是联合体的成员,通过任一成员的指针访问公共初始序列的部分是合法的(6.5.2.3 §3)——这就是你直接访问obj.type_id不违规的原因。
string_or_num obj = {}; if(obj.type_id == 2) // 合法,公共初始序列规则覆盖 memcpy("Hello World!", obj.fixed_string, 13); dynamic_string* ds_ptr = (dynamic_string*) &obj; // 合法,联合体指针转成员指针
现在拆解你的混合场景:匿名联合体作为结构体初始成员的情况
你把匿名联合体作为child的第一个成员,然后想用不同父结构体的指针直接指向child对象,咱们逐个分析:
1. parent_a* a_ptr = (parent_a*) &child_obj; 和 parent_b* b_ptr = (parent_b*) &child_obj;
这两个操作完全符合C23严格别名规则,理由分两步走:
- 首先,
child的第一个成员是匿名联合体,根据规则1,&child_obj可以安全转换为指向这个匿名联合体的指针(相当于union { parent_a _base_a; parent_b _base_b; parent_c _base_c; }*)。 - 然后,根据规则2,这个联合体指针可以自由转换为指向其任意成员的指针——也就是
parent_a*或parent_b*。把这两步合并,直接把&child_obj转成parent_a*/parent_b*完全合规,读写这些指针指向的成员(包括公共的type_id,各自的data/decimal)都不违反严格别名规则。
⚠️ 注意:这里要区分“严格别名违规”和“联合体活跃成员问题”——如果你通过a_ptr写了_base_a,然后立刻通过b_ptr读_base_b.decimal,得到的值是不确定的,但这不是严格别名违规,只是标准不保证这个值的有效性(毕竟联合体成员共享存储空间),严格别名规则本身是允许这种操作的。
2. parent_c* c_ptr = (parent_c*) &child_obj; 和 double* db_ptr = (double*) &child_obj;
这两个操作同样符合严格别名规则,逻辑和上面一致:
&child_obj可以转成匿名联合体的指针,再转成parent_c*(规则2),所以指针转换合法。- 进一步把
c_ptr转成double*(因为parent_c的第一个成员是double,规则1),或者直接把&child_obj转成double*(本质是跳过中间步骤,直接对应到匿名联合体中_base_c.ccccombo_breaker的地址),也都是合规的。
同样要注意:parent_c和其他父结构体没有公共初始序列,所以如果之前通过a_ptr/b_ptr写了数据,再通过c_ptr/db_ptr读,得到的是不确定的值,但这依然不属于严格别名违规——严格别名规则只限制“用不兼容类型的指针访问对象”,而这里的指针转换都是标准允许的,只是值的有效性需要你自己把控。
精准的C23标准条款参考
为了让你100%放心,直接贴关键条款原文:
6.7.2.1 §15:指向结构体对象的指针,经过适当转换后,指向其初始成员(或者该成员的初始成员,以此类推),反之亦然。结构体对象中可能存在未命名的填充,但在初始成员之前不会有填充。
6.7.2.1 §16:指向联合体对象的指针,经过适当转换后,指向其任意成员,反之亦然。联合体对象中可能存在未命名的填充,但在成员之间不会有填充。
6.5.2.3 §3:对于一个联合体类型的表达式,如果它包含几个共享公共初始序列的结构体类型的成员(见6.7.2.1),并且如果该联合体对象当前值的类型是这些结构体中的一个,那么允许通过指向其他结构体类型的指针表达式访问公共初始序列的部分,就像它是第一个结构体类型的对象一样。
最后划重点
所有你列出的指针转换和读写操作,都不违反C23的严格别名规则——区别只在于parent_c的情况没有公共初始序列,跨成员访问会得到不确定值,但这是联合体存储特性导致的,和严格别名无关。
备注:内容来源于stack exchange,提问作者nickelpro

