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

在 Linux 内核代码中,我发现了以下我无法理解的东西。

 struct bts_action {
u16 type;
u16 size;
u8 data[0];
} __attribute__ ((packed));

密码在这里: http://lxr.free-electrons.com/source/include/linux/ti_wilink_st.h

零元素数组的需要和用途是什么?

11142 次浏览

这实际上是一种黑客行为,对于 海湾合作委员会(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];
};

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

其思想是允许在结构的末尾使用可变大小的数组。据推测,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);

参见: http://c2.com/cgi/wiki?StructHack

这是一种数据大小可变的方法,不需要调用 malloc(在本例中为 kmalloc)两次。你可以这样使用它:

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

这在过去是不标准的,被认为是黑客行为(正如阿尼基特所说) ,但它是 标准化的 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。


注意到标准实际上是如何给出 mallocing 这样一个构造的例子(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 的偏移量可能不相同)。

代码无效 C (看这个)。由于显而易见的原因,Linux 内核丝毫不关心可移植性,因此它使用了大量非标准代码。

他们所做的是一个 GCC 非标准扩展,数组大小为0。一个标准的兼容程序可以编写 u8 data[];,这也意味着同样的事情。Linux 内核的作者显然喜欢把事情变得不必要的复杂和非标准,如果有这样做的选项的话。

在旧的 C 标准中,以空数组结束 struct 被称为“ struct hack”。其他人已经在其他答案中解释了它的目的。在 c90标准中,struct hack 是未定义行为的,可能会导致崩溃,这主要是因为 c 编译器可以在 struct 末尾添加任意数量的填充字节。这样的填充字节可能与您试图在结构的末尾“侵入”的数据发生冲突。

GCC 早期做了一个非标准的扩展,将其从未定义的行为改变为定义良好的行为。然后 C99标准采用了这个概念,因此任何现代 C 程序都可以毫无风险地使用这个特性。它在 C99/C11中被称为 柔性阵列构件柔性阵列构件

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

假设您有一些大型结构定义(跨越多条缓存线) ,您希望确保它们在开始和中间跨越边界的地方与缓存线边界对齐。

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);

您可以在 struct 中声明一个零长度的数组,作为具有一致名称的命名标签,但不占用任何空间。

#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);