零元素的数组有什么必要性?

131

12
请投票重新开放此问题。我认为这个问题不是重复的,因为其他帖子中没有涉及非标准“结构体黑客”、零长度和定义良好的C99特性“柔性数组成员”的组合。我也认为,对于C编程社区而言,揭示Linux内核中任何模糊代码都是有益的。主要是因为许多人认为Linux内核是一种尖端的C代码,原因不明。然而实际上,它是一个可怕的混乱,充斥着从未被视为C经典的非标准漏洞。 - Lundin
5
不是复制品——这不是我第一次看到有人没必要关闭问题了。而且我认为这个问题可以增加 Stack Overflow 的知识库。 - Aniket Inge
也可以在comp.lang.c FAQ的问题2.6中找到解释。 - Keith Thompson
1
可能是在C/C++中定义大小为0的数组会发生什么?的重复问题。 - Jim Fell
@Aniket 如果一个问题干净地重定向到另一个问题,就不会有“知识”损失...这只是为了减少重复而付出额外点击的代价。 - rubenvb
显示剩余3条评论
5个回答

149

这是一种可以使用变量大小的数据,而不必调用malloc(在这种情况下是kmalloc)两次的方法。您可以像这样使用它:

struct bts_action *var = kmalloc(sizeof(*var) + extra, GFP_KERNEL);

这曾经不是标准的做法,被视为一种hack(正如Aniket所说),但它在C99中已经得到了标准化。现在它的标准格式是:

这曾经不是标准的做法,被视为一种hack(正如Aniket所说),但它在C99中已经得到了标准化。现在它的标准格式是:

struct bts_action {
     u16 type;
     u16 size;
     u8 data[];
} __attribute__ ((packed)); /* Note: the __attribute__ is irrelevant here */

注意,你没有为data字段指定大小。同时,这个特殊的变量只能出现在结构体的末尾。


在C99中,这个问题在6.7.2.1.16中有所说明(重点在我身上):

作为特例,一个拥有多个命名成员的结构体的最后一个元素可能具有不完整的数组类型;这被称为灵活数组成员。在大多数情况下,灵活数组成员将被忽略。特别地,在结构体的大小方面,它的大小就像灵活数组成员被省略一样,除了它可能有比省略要求更多的填充。但是,当一个点(或->)运算符具有左操作数(指向)一个具有灵活数组成员的结构体,并且右操作数指定该成员时,它的行为就好像该成员被替换为最长的数组(具有相同元素类型),该数组不会使对象大于被访问的对象;即使这将导致偏移量与替换数组的偏移量不同,数组的偏移量仍然应保持与灵活数组成员的偏移量相同。如果此数组没有任何元素,它的行为就好像它有一个元素,但如果尝试访问该元素或生成超出它的指针,则行为是未定义的。

换句话说,如果你有:

struct something
{
    /* other variables */
    char data[];
}

struct something *var = malloc(sizeof(*var) + extra);

你可以使用索引在 [0,extra) 范围内访问 var->data。请注意,sizeof(struct something) 仅给出其他变量的大小,即使 data 的大小为0。


值得一提的是,标准实际上还给出了关于如何使用malloc来分配这样一个结构体的例子(6.7.2.1.17):

struct s { int n; double d[]; };

int m = /* some value */;
struct s *p = malloc(sizeof (struct s) + sizeof (double [m]));

标准文档同一地点的另一个有趣的注释是(重点在于我):

假设调用 malloc 成功,指针 p 指向的对象在大多数情况下表现得好像 p 被声明为:

struct { int n; double d[m]; } *p;

在某些情况下,这种等价可能会被打破; 特别是,成员d的偏移量可能不相同


要明确的是,问题中的原始代码在C99(或C11)标准中仍然不是标准的,仍然被认为是一种hack。C99标准化必须省略数组边界。 - M.M
什么是 [0, extra) - S.S. Anne
3
@JL2210,https://en.wikipedia.org/wiki/Interval_(mathematics)#Terminology - Shahbaz

38
这其实是一个针对GCC (C90) 的黑科技。
它也被称为struct hack
所以下一次,我会说:
struct bts_action *bts = malloc(sizeof(struct bts_action) + sizeof(char)*100);

它等价于说:

struct bts_action{
    u16 type;
    u16 size;
    u8 data[100];
};

我可以创建任意数量的这样的结构对象。


8

这个想法是允许在结构体末尾使用可变大小的数组。可以假设bts_action是某个数据包,它具有固定大小的标头(typesize字段),以及可变大小的data成员。通过将其声明为长度为0的数组,它就像任何其他数组一样进行索引。然后,您可以分配一个bts_action结构体,例如1024字节的data大小,如下所示:

size_t size = 1024;
struct bts_action* action = (struct bts_action*)malloc(sizeof(struct bts_action) + size);

See also: http://c2.com/cgi/wiki?StructHack


2
@Aniket:我不确定那个想法从哪里来。 - sheu
在C++中需要,在C中不需要。 - user339222
3
@sheu,这是因为你写 malloc 的风格让你多次重复自己,如果 action 的类型发生更改,你就必须多次修复它。自行比较以下两者,你就会知道:struct some_thing *variable = (struct some_thing *)malloc(10 * sizeof(struct some_thing));struct some_thing *variable = malloc(10 * sizeof(*variable));,后者更短、更干净,而且更容易修改。 - Shahbaz

7
该代码不是有效的C语言代码(参见此链接)。由于Linux内核无需考虑可移植性,因此使用了许多非标准代码。
他们所做的是使用大小为0的数组进行GCC非标准扩展。符合标准的程序应该写成u8 data[];,这意味着完全相同的事情。Linux内核的作者显然喜欢让事情变得更加复杂和非标准,如果有这样的选项出现的话。
在早期C标准中,以空数组结束结构体被称为“结构体hack”。其他答案已经解释了它在其他答案中的目的。在C90标准中,“结构体hack”是未定义的行为,并且可能会导致崩溃,主要是因为C编译器可以在结构体的末尾添加任意数量的填充字节。这些填充字节可能会与您尝试在结构体末尾“hack”的数据发生冲突。
GCC很早就做了一个非标准扩展,将其从未定义行为更改为定义良好的行为。然后C99标准采用了这个概念,因此任何现代的C程序都可以使用这个功能而不会有风险。在C99/C11中,它被称为“柔性数组成员”。

3
我认为“Linux内核不关注可移植性”这种说法值得怀疑。也许你的意思是对其他编译器的可移植性?确实,它与gcc的特性非常相关。 - Shahbaz
3
然而,我认为这段特定的代码并不是主流代码,可能因为它的作者没有太注意到而被遗漏了。许可证显示它是关于一些德州仪器驱动程序,所以内核的核心程序员可能没有关注它。我相信内核开发人员会根据新标准或新优化不断更新旧代码,只是太大了无法确保每个细节都更新! - Shahbaz
2
@Shahbaz,“显而易见”的部分,我指的是可移植性到其他操作系统,这自然是毫无意义的。但他们似乎也不在乎对其他编译器的可移植性,他们使用了许多GCC扩展,Linux很可能永远不会被移植到另一个编译器上。 - Lundin
3
关于任何标记为德州仪器(Texas Instruments)的内容,TI本身以在其各种芯片的应用笔记中生产出最无用、最糟糕、最幼稚的C代码而声名狼藉。如果代码来自TI,则关于从中解释出有用信息的可能性一文不值。 - Lundin
5
Linux和gcc是密不可分的这一说法是正确的。由于操作系统本身就很复杂,因此Linux内核也相当难以理解。但我的观点是,由于第三方糟糕的编码实践,我们不能说“Linux内核的作者显然喜欢让事情变得不必要地复杂和非标准化,如果有一个这样的选项出现”的话。请注意,这会伤害到他人。 - Shahbaz
显示剩余4条评论

2

零长度数组的另一个用途是作为结构体内的命名标签,以协助编译时检查结构体偏移量。

假设您有一些大型结构定义(跨越多个缓存行),您希望确保它们在开始和中间跨越边界时都对齐到缓存行边界。

struct example_large_s
{
    u32 first; // align to CL
    u32 data;
    ....
    u64 *second;  // align to second CL after the first one
    ....
};

在代码中,您可以使用GCC扩展声明它们,例如:

__attribute__((aligned(CACHE_LINE_BYTES)))

但是你仍然希望确保在运行时执行此操作。

ASSERT (offsetof (example_large_s, first) == 0);
ASSERT (offsetof (example_large_s, second) == CACHE_LINE_BYTES);

这对于单个结构体可以起作用,但是要涵盖许多具有不同成员名称需要对齐的结构体将会很困难。您很可能会得到类似下面的代码,您必须找到每个结构体第一个成员的名称:
assert (offsetof (one_struct,     <name_of_first_member>) == 0);
assert (offsetof (one_struct,     <name_of_second_member>) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, <name_of_first_member>) == 0);
assert (offsetof (another_struct, <name_of_second_member>) == CACHE_LINE_BYTES);

与其这样做,您可以在结构体中声明一个零长度的数组,作为带有一致名称但不占用任何空间的命名标签。

#define CACHE_LINE_ALIGN_MARK(mark) u8 mark[0] __attribute__((aligned(CACHE_LINE_BYTES)))
struct example_large_s
{
    CACHE_LINE_ALIGN_MARK (cacheline0);
    u32 first; // align to CL
    u32 data;
    ....
    CACHE_LINE_ALIGN_MARK (cacheline1);
    u64 *second;  // align to second CL after the first one
    ....
};

那么运行时断言代码将更容易维护:
assert (offsetof (one_struct,     cacheline0) == 0);
assert (offsetof (one_struct,     cacheline1) == CACHE_LINE_BYTES);
assert (offsetof (another_struct, cacheline0) == 0);
assert (offsetof (another_struct, cacheline1) == CACHE_LINE_BYTES);

有趣的想法。只是需要注意的是,标准不允许0长度数组,所以这是编译器特定的事情。此外,在结构定义中引用gcc对0长度数组行为的定义可能是一个好主意,至少可以显示它是否会在声明之前或之后引入填充。 - Shahbaz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接