使用共同的初始序列初始化两个结构的并集

时间:2019-02-15 08:51:07

标签: c language-lawyer unions

问题:如果联合包含两个具有兼容类型的常见初始序列的结构,那么如果我们使用一个结构初始化初始序列的一部分,而其余部分则初始化该行为,则行为定义良好序列使用另一个结构?

考虑以下代码片段:

union u_t{
    struct {
        int i1;
        int i2;
    } s1;

    struct {
        int j1;
        int j2;
    } s2;
};

int main(){
    union u_t *u_ptr = malloc(sizeof(*u_ptr));
    u_ptr -> s1.i1 = 10;
    u_ptr -> s2.j2 = 11;

    printf("%d\n", u_ptr -> s2.j1 + u_ptr -> s1.i2); //prints 21
}

DEMO

问题是“印刷21”行为是否定义明确。标准N1570 6.5.2.3(p6)指定以下内容:

  

如果一个联合包含几个共享共同的首字母的结构   序列(请参见下文),以及联合对象当前是否包含一个   在这些结构中,可以检查共同的姓名缩写   其中任何一部分的完成类型声明的一部分   工会的形象可见。

因此可以检查常见的初始序列(在这种情况下为整个结构)。但是问题在于,在这种情况下,联合似乎包含s2对象,其中j2是唯一的初始化成员。

我认为我们最终会出现 未指定 行为,因为我们仅初始化了s2.j2s2.j1并未初始化,因此它应该包含未指定的值。

2 个答案:

答案 0 :(得分:2)

关于别名:

常见的初始序列仅与两种结构类型的别名有关。在这里这不是问题,并且您的两个结构甚至是兼容的类型,因此指向它们的指针可以不使用任何技巧而成为别名。剖析C11 6.2.7:

  

6.2.7兼容类型和复合类型
  如果两个类型相同,则它们具有兼容类型。 /-/此外,如果两个结构,联合或枚举类型的标记和成员满足以下要求,则它们是兼容的:

     

如果使用标记声明,则   其他应使用相同标签声明。

这里都没有用标记声明结构。

  

如果两者均在其范围内的任何位置完成   各自的翻译单位,则需要满足以下附加要求:

它们都已完成(定义)。

  

有   是他们的成员之间一对一的对应关系,这样每对   声明对应的成员具有兼容的类型;

这些结构适用。

  

如果一对成员是   用对齐方式说明符声明,另一个用等效对齐方式声明   说明符如果该对中的一个成员用名称声明,则另一个声明   同名。

比对说明符不适用。

  

对于两个结构,相应的成员应在   相同的顺序。

这是真的。

结论是您的两个结构都是兼容类型。这意味着您不需要任何技巧,例如常见的初始序列。严格的别名规则仅声明(6.5 / 7):

  

只能通过具有以下类型之一的左值表达式访问对象的存储值:
  —与对象的有效类型兼容的类型,

在这种情况下。

此外,如其他答案中所述,此处的实际数据的有效类型为int,因为分配的存储不产生任何有效类型,因此它成为用于左值访问的第一种类型。这也意味着编译器不能假设指针不会别名。

此外,严格的别名规则为结构和联合成员的左值访问提供了例外:

  

集合或联合类型,其中包括上述类型之一   成员

然后,您具有最常见的初始序列。就别名而言,这是定义良好的。


关于类型修饰:

您实际关心的似乎不是混叠,而是通过联合键入punning。 C11 6.5.2.3/3模糊地保证了这一点:

  

后缀表达式,后跟。运算符和标识符指定结构或联合对象的成员。该值是命名成员的值(95),如果第一个表达式是左值,则该值是左值。

这是规范性文字,而且写得很烂-没有人能理解程序/编译器应如何基于此行为。内容丰富的脚注95)对此进行了很好的解释:

  

95)如果用于读取联合对象的内容的成员与上次用于在该对象中存储值的成员不同,则该值的对象表示的适当部分将重新解释为对象表示按照6.2.6中所述的新类型(有时称为“类型调整”的过程)。这可能是陷阱的表示形式。

根据您的情况,您触发了从一种结构类型到另一种兼容结构类型的类型转换。这是绝对安全的,因为它们是同一类型,并且不存在对齐或陷阱问题。

请注意,此处的C ++有所不同。

答案 1 :(得分:1)

C11标准(n1570)在[6.5 Expressions]/6的{​​{3}}中声明:

  

已分配的对象没有声明的类型。

footnote指出:

  

6用于访问其存储值的对象的有效类型是该对象的声明类型(如果有)。如果通过具有非字符类型的左值将值存储到没有声明类型的对象中,则该左值的类型将成为该访问和不修改该访问的后续访问的对象的有效类型。储值。

当您访问printf语句中用于打印的存储值时,您还将遵循[6.5 Expressions]/6中规定的规则。

这与您从N1570 6.5.2.3(p6)提供的引文相结合,提供了“一个特殊的保证,以简化并集的使用”。

从实用的角度看,如果您查看[6.5 Expressions]/7,就会发现这是实际发生的情况。

        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     eax, 8
        mov     edi, eax
        call    malloc
        mov     qword ptr [rbp - 8], rax //Here
        mov     rax, qword ptr [rbp - 8] //Here
        mov     dword ptr [rax], 10      //Here 
        mov     rax, qword ptr [rbp - 8] //Here
        mov     dword ptr [rax + 4], 11  //Here 
        mov     rax, qword ptr [rbp - 8]
        mov     ecx, dword ptr [rax]
        mov     rax, qword ptr [rbp - 8]
        add     ecx, dword ptr [rax + 4]
        movabs  rdi, offset .L.str
        mov     esi, ecx
        mov     al, 0
        call    printf
        xor     ecx, ecx
        mov     dword ptr [rbp - 12], eax # 4-byte Spill
        mov     eax, ecx
        add     rsp, 16
        pop     rbp
        ret
.L.str:
        .asciz  "%d\n"