零长度数组

12

最近我看到了一个结构体定义,

struct arr {
    int cnt;
    struct {
        int  size;
        int *name;
    } list[0];
};

现在我不知道为什么要声明list[0]。 我感兴趣的是为什么要使用它。 它有任何优点吗? 如果有,是什么?


2
在 C 语言中,零大小的对象是非法的。你可以使用 [1] 来浪费一点空间(或者计算来弥补它),或者使用 [](但这需要一个 C99 编译器)。 - R.. GitHub STOP HELPING ICE
这是gcc手册中关于长度为零的数组的章节:https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html - Pierz
3个回答

18

这是为了使用动态长度数组。您可以使用malloc()分配内存,并使数组驻留在结构的末尾:

struct arr *my_arr = malloc(sizeof *my_arr + 17 * sizeof *my_arr->list);
my_arr->cnt = 17;
my_arr->list[0].size = 0;
my_arr->list[1].name = "foo";

实际上,能够使用0作为长度是GCC的一个扩展功能(正如评论中所指出的)。在C99中,您可以完全省略大小文字面量以达到相同的效果。

在这些功能被实现之前,通常将长度设置为1,但这会使分配过程变得更加复杂,因为您必须在计算需要的内存时进行补偿。


5
此外,作为一个长度为0的数组,list不会对结构体的大小产生贡献。 sizeof(struct arr) == 4 - Jeff Mercado
4
使用长度为0的能力是GCC的特性。C99的等效方法是完全省略长度,例如struct { ... } list[]; - Bart van Ingen Schenau
@hue:不。在这种情况下,您的结构体中有一个指针。在原始问题中,您有一个数组。这是两个完全不同的东西。 - AnT stands with Russia
3
不行 - 这会导致你的列表数组在内存中存储并分配空间。这样就可以让列表紧随结构体之后。 - ijw
4
@hue: 呃...您已经得到了两个描述该优点的答案。它允许您将结构体和变长数组的内存分配为一个连续的内存块,并通过一次调用malloc完成。如果使用指针,则必须单独分配内存(两个malloc调用,可能不连续),或者使用其他技巧(以实现适当的对齐等)。 - AnT stands with Russia
显示剩余2条评论

11

这被称为“结构体技巧”。您可以在SO或网络上搜索它。

请注意,在C中正式地声明大小为0的数组始终是非法的。您提供的代码正式上甚至无法编译。大多数C编译器将接受0大小的数组声明作为扩展,特别是因为它经常用于“懒惰”的“结构体技巧”(它可以依靠sizeof来确定要分配多少内存,因为0大小的数组据说不会影响结构体的总大小)。

一个可以说更好的结构体技巧实现使用大小为1的数组。

struct arr {
    int cnt;
    struct {
        int  size;
        int *name;
    } list[1];
};

它更好是因为它至少可以正式编译。为了为具有 N 个元素的结构体在 list 中分配内存,使用标准的 offsetof 宏。

arr *a = malloc(offsetof(arr, list) + N * sizeof a->list);

在C99版本的语言规范中,“struct hack”通过无大小数组声明(带空的[])得到支持,因为在C99中,0大小的数组声明也是不合法的。

标准中是否有任何禁止编译器将看到声明 unsigned char foo[1]; 的所有访问编译为 foo[0] 的内容?虽然回答“struct-hack”问题的人们认为大小为1的数组版本是否是“未定义行为”是学术性的问题,但前述优化在某些嵌入式系统环境中可能非常有用,如果合法的话。是否可以这样做(添加警告,编译器可能希望显式禁用对间接访问结构体的最后一个元素)? - supercat
如果我理解你的问题,它会悄悄地导致一些奇怪的行为,例如 &(foo[999]) == &(foo[0])。假设 a[b] === (&a[0])+b。 - Steven R. Loomis
@StevenR.Loomis:你所做的假设只有在b小于a中元素数量时才成立。如果结构体内的数组指针只能在声明大小或分配空间中较小的一个范围内进行索引,那么你描述的情况将是未定义行为。如果我负责标准,我会规定访问超出指定范围的数组将是未定义行为,除非在结构体末尾的单个元素数组可以使用非零索引,如果该结构体是... - supercat
malloccalloc等函数分配的内存段中的最后一个项目。这样的规则将要求编译器继续支持使用“结构体hack”的旧代码,但允许大多数结构体数组强制执行所允许的优化(不仅仅是我提到的单个元素情况,还包括许多其他涉及别名等情况的场景)。例如,对于struct {int a[1], b;} foo; foo.b=3; x=foo.a[something()]; foo.b=0;,编译器是否需要在访问foo.a[]之前将3存储到内存中,或者是否允许省略存储? - supercat
@supercat但运行时不知道a中有多少元素,我的假设只是我理解C如何解释数组下标的陈述。实际上,我会说这种情况是定义好的行为(我正在利用它)。现在b[999]或b[-100]的可能未定义,但我认为行为是定义好的。我不希望编译器在这里检测范围错误。 - Steven R. Loomis
@StevenR.Loomis:声明一系列操作具有“未定义行为”并不意味着编译器应该始终捕获它。这仅仅意味着在编译器偶然注意到某些事情不合法的情况下,他们可以自由地根据假设它不会发生而采取任何他们认为有利的行动。 - supercat

0
另一个优点是,如果您的结构描述了磁盘/网络数据,则可以使用它。如果cnt为0,则数据大小可能仅为cnt的长度。

我在此确认我所担心的,即list[0]无效。


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