【问题标题】:What happens if I define a 0-size array in C/C++?如果我在 C/C++ 中定义一个 0 大小的数组会发生什么?
【发布时间】:2012-04-01 03:10:31
【问题描述】:

只是好奇,如果我在代码中定义一个零长度数组int array[0];,实际上会发生什么? GCC 完全没有抱怨。

示例程序

#include <stdio.h>

int main() {
    int arr[0];
    return 0;
}

澄清

我实际上是想弄清楚零长度数组是否以这种方式初始化,而不是像 Darhazer 的 cmets 中的可变长度那样被指向,是否被优化了。

这是因为我必须将一些代码发布到野外,所以我试图弄清楚我是否必须处理 SIZE 被定义为 0 的情况,这发生在一些代码中静态定义int array[SIZE];

我真的很惊讶 GCC 没有抱怨,这导致了我的问题。从我收到的答案来看,我认为没有警告主要是由于支持尚未使用新 [] 语法更新的旧代码。

因为我主要想知道这个错误,所以我将 Lundin 的答案标记为正确(Nawaz 的答案是第一个,但它并不完整)——其他人指出它对尾部填充结构的实际用途,虽然相关,并不是我想要的。

【问题讨论】:

  • @AlexanderCorwin:不幸的是,在 C++ 中,由于存在未定义的行为、非标准扩展和其他异常情况,自己尝试一些东西通常不是获得知识的途径。
  • @JustinKirk 通过测试并看到它有效,我也被困住了。由于我在帖子中收到的批评,我了解到测试并使其正常工作并不意味着它是有效和合法的。所以有时自检是无效的。
  • @JustinKirk,请参阅Matthieu's answer,了解您将在何处使用它的示例。它也可能在数组大小是模板参数的模板中派上用场。问题中的示例显然与上下文无关。
  • @JustinKirk:Python 中的[] 甚至C 中的"" 的目的是什么?有时,您有一个需要数组的函数或宏,但您没有任何数据可放入其中。
  • 什么是“C/C++”? 这是两种不同的语言

标签: c++ c arrays


【解决方案1】:

在标准 C 和 C++ 中,不允许零大小数组..

如果您使用的是 GCC,请使用 -pedantic 选项对其进行编译。它会给出警告,说:

zero.c:3:6: warning: ISO C forbids zero-size array 'a' [-pedantic]

如果是 C++,它会给出类似的警告。

【讨论】:

  • 在 Visual C++ 2010 中:error C2466: cannot allocate an array of constant size 0
  • -Werror 只是将所有警告变成错误,这并不能修复 GCC 编译器的错误行为。
  • C++ Builder 2009 也正确给出了错误:[BCC32 Error] test.c(3): E2021 Array must have at least one element
  • 除了-pedantic -Werror,你也可以只做-pedantic-errors
  • 零大小的数组与零大小的std::array 并不完全相同。 (旁白:我记得但找不到 VLA 被考虑并明确拒绝在 C++ 中的来源。)
【解决方案2】:

我要补充一点,gcc 的在线文档中有一个whole page 关于这个论点。

一些引用:

GNU C 允许使用零长度数组。

在 ISO C90 中,您必须将内容的长度设为 1

3.0 之前的 GCC 版本允许静态初始化零长度数组,就好像它们是灵活数组一样。除了那些有用的情况外,它还允许在会损坏以后数据的情况下进行初始化

所以你可以

int arr[0] = { 1 };

然后繁荣 :-)

【讨论】:

  • 我可以像int a[0] 一样,然后a[0] = 1 a[1] = 2 吗??
  • @SurajJain 如果你想覆盖你的堆栈:-) C 不会检查索引与你正在编写的数组的大小,所以你可以a[100000] = 5 但如果你很幸运,你'如果你幸运的话,它只会让你的应用崩溃:-)
  • Int a[0] ;表示一个可变数组(零大小数组),我现在如何分配它
  • @SurajJain “C 不检查索引与您正在编写的数组的大小”的哪一部分不清楚? C 中没有索引检查,您可以在数组末尾写入并导致计算机崩溃或覆盖内存中的宝贵位。所以如果你有一个 0 个元素的数组,你可以写在 0 个元素的末尾之后。
  • 看这个quora.com/…
【解决方案3】:

按照标准,是不允许的。

然而,C 编译器的当前做法是将这些声明视为灵活数组成员 (FAM) 声明:

C99 6.7.2.1, §16:作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为灵活数组成员。

FAM 的标准语法是:

struct Array {
  size_t size;
  int content[];
};

这个想法是你可以这样分配它:

void foo(size_t x) {
  Array* array = malloc(sizeof(size_t) + x * sizeof(int));

  array->size = x;
  for (size_t i = 0; i != x; ++i) {
    array->content[i] = 0;
  }
}

您也可以静态使用它(gcc 扩展):

Array a = { 3, { 1, 2, 3 } };

这也称为 tail-padded 结构(该术语早于 C99 标准的发布)或 struct hack(感谢 Joe Wreschnig 指出) .

然而,这种语法直到最近在 C99 中才被标准化(并且保证了效果)。在需要恒定大小之前。

  • 1 是一种便携的方式,虽然它很奇怪。
  • 0 更擅长表明意图,但就标准而言并不合法,并被一些编译器(包括 gcc)作为扩展支持。

然而,尾部填充实践依赖于存储可用这一事实(小心 malloc),因此通常不适合堆栈使用。

【讨论】:

  • @Lundin:我在这里没有看到任何 VLA,所有大小在编译时都是已知的。 flexible array 术语来自gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Zero-Length.html,据我了解,这里有资格int content[];。由于我对 C 的艺术术语不太了解...您能否确认我的推理是否正确?
  • @MatthieuM.: C99 6.7.2.1, §16: 作为一种特殊情况,具有多个命名成员的结构的最后一个元素可能具有不完整的数组类型;这称为灵活数组成员。
  • 这个成语也被称为"struct hack",而且我遇到的熟悉这个名字的人比“tail-padded structure”还多(以前从未听说过,除了可能作为对为将来的 ABI 兼容性填充结构)或我在 C99 中第一次听到的“灵活数组成员”。
  • 对 struct hack 使用大小为 1 的数组可以避免编译器发牢骚,但这只是“可移植的”,因为编译器编写者足够好,承认这种用法是事实上的标准。如果不是因为禁止零大小数组、程序员随后将单元素数组用作糟糕的替代品,以及编译器编写者的历史态度,即即使标准没有要求,它们也应该满足程序员的需求,编译器编写者可以只要foo 是一个单元素数组,就可以轻松有效地将foo[x] 优化为foo[0]
  • @RobertSsupportsMonicaCellio:它在答案中明确显示,但在结束。我也预先加载了解释,以便从一开始就更清楚。
【解决方案4】:

这是完全非法的,而且一直都是,但是很多编译器 忽略发出错误信号。我不确定你为什么要这样做。 我知道的一种用途是从布尔值触发编译时错误:

char someCondition[ condition ];

如果condition 为假,那么我会收到编译时错误。因为 编译器确实允许这样做,但是,我已经开始使用:

char someCondition[ 2 * condition - 1 ];

这给出了 1 或 -1 的大小,我从未找到编译器 这将接受 -1 的大小。

【讨论】:

  • 这是一个有趣的 hack。
  • 我认为这是元编程中的一个常见技巧。如果STATIC_ASSERT 的实现使用它,我不会感到惊讶。
  • 为什么不只是:#if condition \n #error whatever \n #endif
  • @Jerfov2 因为在预处理时可能不知道条件,只有编译时才知道
【解决方案5】:

数组的大小不能为零。

ISO 9899:2011 6.7.6.2:

如果表达式是常量表达式,它的值应大于零。

上述文本对于纯数组(第 1 段)都是正确的。对于 VLA(可变长度数组),如果表达式的值小于或等于零,则行为未定义(第 5 段)。这是 C 标准中的规范性文本。不允许编译器以不同的方式实现它。

gcc -std=c99 -pedantic 对非 VLA 情况发出警告。

【讨论】:

  • “它实际上必须给出一个错误”——标准中不承认“警告”和“错误”之间的区别(它只提到“诊断”),并且是编译必须的唯一情况停止[即警告和错误之间的实际区别] 在于遇到#error 指令。
  • 仅供参考,作为一般规则,(C 或 C++)标准仅说明编译器必须允许,而不是必须禁止。在某些情况下,他们会声明编译器应该发出“诊断”,但这与他们得到的一样具体。其余的留给编译器供应商。编辑:Random832 也说了什么。
  • @Lundin “不允许编译器构建包含零长度数组的二进制文件。”该标准绝对没有说任何此类。它只是说,当给定的源代码包含一个数组的大小时,它必须生成至少一个诊断消息,该数组的长度为 0 常量表达式。标准禁止编译器构建二进制文件的唯一情况是遇到#error 预处理器指令。
  • @Lundin 为所有正确的情况生成一个二进制文件满足#1,为不正确的情况生成或不生成一个二进制文件不会影响它。对于#3,打印警告就足够了。此行为与 #2 无关,因为标准未定义此源代码的行为。
  • @Lundin:关键是你的说法是错误的;只要发出诊断,就允许符合标准的编译器构建包含零长度数组的二进制文件。
【解决方案6】:

如果允许,结构中的零大小数组声明将很有用,并且如果语义是这样的(1)它们将强制对齐但不分配任何空间,并且(2)索引数组将被视为已定义在结果指针与结构位于同一内存块内的情况下的行为。任何 C 标准都不允许这种行为,但一些较旧的编译器在它成为标准之前允许它允许使用空括号进行不完整数组声明。

通常使用大小为 1 的数组实现的 struct hack 是不可靠的,我认为编译器没有任何要求避免破坏它。例如,我希望如果编译器看到int a[1],它有权将a[i] 视为a[0]。如果有人试图通过类似的方式解决 struct hack 的对齐问题

类型定义结构{ uint32_t 大小; uint8_t 数据[4]; // 使用四个,以避免填充影响结构的大小 }

编译器可能会变得聪明并假设数组大小确实是四:

;如所写 foo = myStruct->data[i]; ;按照解释(假设 little-endian 硬件) foo = ((*(uint32_t*)myStruct->data) >> (i

这样的优化可能是合理的,特别是如果myStruct-&gt;data 可以在与myStruct-&gt;size 相同的操作中加载到寄存器中。我不知道标准中会禁止这种优化,尽管它当然会破坏任何可能会访问超出第四个元素的内容的代码。

【讨论】:

  • 灵活数组成员被添加到 C99 作为 struct hack 的合法版本
  • 标准确实说对不同数组成员的访问不会发生冲突,这会使优化变得不可能。
  • @BenVoigt:C 语言标准没有指定同时写入一个字节和读取包含一个字的效果,但是 99.9% 的处理器确实指定写入将成功并且该字将包含字节的新版本或旧版本以及其他字节的未更改内容。如果编译器以此类处理器为目标,会有什么冲突?
  • @supercat:C 语言标准保证同时写入两个不同的数组元素不会发生冲突。因此,您认为(边写边读)可以正常工作的论点是不够的。
  • @BenVoigt:如果一段代码要例如以某种顺序写入数组元素0、1和2,不允许将所有四个元素读入long,修改三个,然后写回所有四个,但我认为允许将所有四个元素读入a long,修改三,将低 16 位写回短,将 16-23 位写回一个字节。你会不同意吗?并且只需要读取数组元素的代码将被允许简单地将它们读入 long 并使用它。
【解决方案7】:

零长度数组的另一个用途是制作可变长度对象(C99 之前)。 零长度数组 不同于 灵活数组,后者有 [] 而没有 0。

引用自gcc doc

在 GNU C 中允许使用零长度数组。它们作为结构的最后一个元素非常有用,它实际上是可变长度对象的标头:

 struct line {
   int length;
   char contents[0];
 };
 
 struct line *thisline = (struct line *)
   malloc (sizeof (struct line) + this_length);
 thisline->length = this_length;

在 ISO C99 中,您将使用灵活的数组成员,它在语法和语义上略有不同:

  • 灵活的数组成员被写为没有 0 的内容[]。
  • 灵活数组成员的类型不完整,因此可能无法应用 sizeof 运算符。

一个真实的例子是struct kdbus_itemkdbus.h(Linux 内核模块)中的零长度数组。

【讨论】:

  • 恕我直言,标准没有充分的理由禁止零长度数组;它可以将零大小的对象作为结构的成员,并将它们视为void* 用于算术目的(因此将禁止添加或减去指向零大小对象的指针)。虽然灵活数组成员大多比零大小的数组好,但它们也可以充当一种“联合”来给事物起别名,而无需为后面的内容添加额外级别的“语法”间接(例如,给定struct foo {unsigned char as_bytes[0]; int x,y; float z;} 可以访问会员x..z...
  • ...直接不用说例如myStruct.asFoo.x 等。此外,IIRC、C 竭力在结构中包含灵活数组成员,因此不可能拥有包含多个其他已知长度内容的灵活数组成员的结构。
  • @supercat 一个很好的理由是维护有关访问外部数组边界的规则的完整性。作为结构体的最后一个成员,C99 灵活数组成员实现了与 GCC 零大小数组完全相同的效果,但不需要在其他规则中添加特殊情况。恕我直言,sizeof x-&gt;contents 是 ISO C 中的错误,而不是在 gcc 中返回 0,这是一个改进。不是结构成员的零大小数组引入了许多其他问题。
  • @MM:如果将两个相等的指针减去一个大小为零的对象定义为产生零(就像减去指向任何大小的对象的相等指针一样),并减去不相等的指针零大小的对象被定义为产生未指定的值?如果标准已指定实现可以允许将包含 FAM 的结构嵌​​入到另一个结构中,前提是后者结构中的下一个元素是与 FAM 具有相同元素类型的数组或以此类数组开头的结构,并且只要...
  • ...它将 FAM 识别为阵列的别名(如果对齐规则会导致阵列以不同的偏移量着陆,则需要进行诊断),这将非常有用。事实上,没有什么好的方法可以让方法接受指向通用格式 struct {int n; THING dat[];} 的结构的指针,并且可以处理静态或自动持续时间的东西。
【解决方案8】:

按照标准,你当然不能有零大小的数组,但实际上每个最流行的编译器都会让你做到这一点。所以我会试着解释为什么它会很糟糕

#include <cstdio>

int main() {
    struct A {
        A() {
            printf("A()\n");
        }
        ~A() {
            printf("~A()\n");
        }
        int empty[0];
    };
    A vals[3];
}

我就像一个人类会期望这样的输出:

A()
A()
A()
~A()
~A()
~A()

Clang 打印:

A()
~A()

GCC 打印这个:

A()
A()
A()

这很奇怪,所以如果可以的话,最好不要在 C++ 中使用空数组。

GNU C 中还有一个扩展,它可以让你在 C 中创建零长度数组 ,但据我所知,结构中应该至少有一个成员,或者你如果你使用 C++,会得到非常奇怪的例子。

【讨论】:

    猜你喜欢
    • 2021-10-30
    • 1970-01-01
    • 2012-05-27
    • 1970-01-01
    • 2017-06-11
    • 1970-01-01
    • 1970-01-01
    • 2022-11-17
    • 1970-01-01
    相关资源
    最近更新 更多