• 通过在结构类型中使用灵活数组成员(FAM),我们是否将程序暴露给未定义行为的可能性?
  • 一个程序是否可以使用FAM并仍然是严格符合要求的程序?
  • 是否需要将灵活数组成员的偏移量放在结构的末尾?

  • 这些问题适用于C99 (TC3)C11 (TC1)
    #include <stdio.h>
    #include <stdlib.h>
    #include <stddef.h>
    
    int main(void) {
        struct s {
            size_t len;
            char pad;
            int array[];
        };
    
        struct s *s = malloc(sizeof *s + sizeof *s->array);
    
        printf("sizeof *s: %zu\n", sizeof *s);
        printf("offsetof(struct s, array): %zu\n", offsetof(struct s, array));
    
        s->array[0] = 0;
        s->len = 1;
    
        printf("%d\n", s->array[0]);
    
        free(s);
        return 0;
    }
    

    输出:
    sizeof *s: 16
    offsetof(struct s, array): 12
    0
    

    最佳答案

    简短答案

  • 是。 使用FAM的常见约定使我们的程序容易出现不确定的行为。话虽这么说,但我不知道任何现有的不符合规范的实现方式。
  • 可能,但是不太可能。 即使我们实际上没有达到未定义的行为,我们仍然可能会失败严格的一致性。
  • 编号 FAM的偏移量不必位于结构的末尾,它可以覆盖任何尾随填充字节。

  • 答案适用于C99 (TC3)C11 (TC1)

    长答案

    FAM最初是在C99(TC0)(1999年12月)中引入的,其原始规范要求FAM的偏移量在结构的末尾。原始规范定义明确,因此不会导致不确定的行为,也不会成为严格遵循标准的问题。
    C99 (TC0) §6.7.2.1 p16(1999年12月)



    问题在于常见的C99实现(例如GCC)未遵循标准要求,并允许FAM覆盖任何尾随填充字节。他们的方法被认为是更有效的,并且由于他们遵循该标准的要求(会导致向后兼容的破坏),委员会选择更改该规范,并且从C99 TC2(2004年11月)开始不再需要该标准。 FAM在结构末尾的偏移量。
    C99 (TC2) §6.7.2.1 p16(2004年11月)



    新规范删除了要求FAM偏移在结构末尾的声明,并且引入了非常不幸的结果,因为该标准赋予实现自由,不保留结构或结构中任何填充字节的值。工会处于一致状态。进一步来说:
    C99 (TC3) §6.2.6.1 p6


    这意味着,如果我们的FAM元素中的任何一个与(或覆盖)任何尾随填充字节相对应,则在存储到该结构的成员时,它们(可能)采用未指定的值。我们甚至不必考虑这是否适用于FAM本身存储的值,即使严格解释为仅适用于FAM以外的成员,也足以造成损害。
    #include <stdio.h>
    #include <stdlib.h>
    #include <stddef.h>
    
    int main(void) {
        struct s {
            size_t len;
            char pad;
            int array[];
        };
    
        struct s *s = malloc(sizeof *s + sizeof *s->array);
    
        if (sizeof *s > offsetof(struct s, array)) {
            s->array[0] = 123;
            s->len = 1; /* any padding bytes take unspecified values */
    
            printf("%d\n", s->array[0]); /* indeterminate value */
        }
    
        free(s);
        return 0;
    }
    

    一旦我们存储到该结构的成员,填充字节就占据未指定的字节,因此对与任何尾随填充字节相对应的FAM元素的值所做的任何假设现在都是错误的。这意味着任何假设都会导致我们无法严格遵守。

    未定义的行为

    尽管填充字节的值是“未指定的值”,但是对于受它们影响的类型不能说相同,因为基于未指定值的对象表示可以生成陷阱表示。因此,描述这两种可能性的唯一标准术语是“不确定值”。如果FAM的类型恰好具有陷阱表示,则访问它不仅是未指定值的问题,而且是未定义行为的问题。

    但是,等等,还有更多。如果我们同意描述该值的唯一标准术语是“不确定值”,那么即使FAM的类型碰巧没有陷阱表示,由于对C的正式解释,我们也达到了不确定的行为。标准委员会认为,将不确定的值传递给标准库函数是未定义的行为。

    07-26 09:11