如果明确给出多维数组,为什么 char[][] = {{...}, {...}} 不可能?

Why is char[][] = {{...}, {...}} not possible if explicitly given a multidimensional array?

我浏览了 this 篇文章。我理解所解释的规则,但我想知道在定义常量多维数组并使用给定类型的已知值直接初始化它时,到底是什么阻止了编译器接受以下语法:

const int multi_arr1[][] = {{1,2,3}, {1,2,3}}; // why not?
const int multi_arr2[][3] = {{1,2,3}, {1,2,3}}; // OK

error: declaration of 'multi_arr1' as multidimensional array must have bounds
       for all dimensions except the first

是什么阻止了编译器向右看并意识到我们正在为每个 "subarray" 处理 3 个元素,或者可能仅在程序员通过例如每个子数组的元素数量不同,例如 {1,2,3}, {1,2,3,4}?

例如,在处理一维字符数组时,编译器可以查看 = 右侧的字符串,这是有效的:

const char str[] = "Str";

我想了解发生了什么,因此编译器无法推断数组维度和计算分配大小,因为现在在我看来编译器拥有执行此操作所需的所有信息。我在这里错过了什么?

在存在初始化器的情况下,在实现编译器时没有什么不可能推导出多维数组的最内层维度,但是,C 或C++ 标准,显然,对该功能的需求并不大。

换句话说,标准语言不支持您所追求的。如果有足够多的人需要它,它 可以 得到支持。他们没有。

对于数组,编译器必须知道每个元素有多大才能进行索引计算。例如

int a[3];

是一个整型数组。编译器知道 int 有多大(通常是 4 个字节),因此它可以计算 a[x] 的地址,其中 x 是 0 到 2 之间的索引。

二维数组可以看作是数组的一维数组。例如

int b[2][3];

int的二维数组,但也是int数组的一维数组。即 b[x] 指的是三个 ints 的数组。

即使对于数组的数组,编译器必须知道每个元素的大小的规则仍然适用,这意味着在数组的数组中,第二个数组必须具有固定大小。如果不是,编译器在索引时无法计算地址,即 b[x] 将无法计算。因此,为什么 multi_arr2 在你的例子中是可以的,但 multi_arr1 不是。

What prevents compiler from looking to the right and claim that we are dealing 3 elements for each "subarray" or possibly return error only for cases when programmer passes e.g. different number of elements for each subarray like {1,2,3}, {1,2,3,4}

可能是解析器的限制。当它到达初始化器时,解析器已经通过了声明。最早的 C 编译器非常有限,上面的行为早在现代编译器出现之前就已按预期设置。

简要扩展评论:

What "blocks" the compiler is adherence to the standard (for C or C++, they're different standards, pick one).

What "blocks" the standard from allowing this is no-one wrote a standards proposal for implementing it which was subsequently accepted.

所以,您要问的是,为什么 no-one 有动力去做您认为有用的事情,我只能将其视为 opinion-based。

可能实现这一点或保持一致的语义存在实际困难;这不完全是你问的问题,但它至少可以客观地回答。我怀疑如果有足够的动力,有人可以克服这些困难。大概 no-one 是。

例如,(reference),语法a[]实际上意味着未知边界数组。因为在使用聚合初始化声明时可以在特殊情况下推断出界限,所以您将其视为 a[auto] 之类的东西。也许 that 会是一个更好的提议,因为它没有历史包袱。如果您认为收益值得付出努力,请随时自己写下来。

规则是编译器只根据给定的初始化列表确定数组的第一维。它期望明确指定第二个维度。期间.

要求编译器从初始化器推断内部维度将要求编译器以标准避免的方式追溯工作。

标准允许被初始化的对象引用它们自己。例如:

struct foo { struct foo *next; int value; } head = { &head, 0 };

这定义了 linked 列表的一个节点,该节点最初指向自身。 (据推测,稍后会插入更多节点。)这是有效的,因为 C 2011 [N1570] 6.2.1 7 说标识符 head “具有在其声明符完成后立即开始的范围。” 声明符 是声明语法的一部分,包括标识符名称以及声明的数组、函数、and/or 指针部分(例如,f(int, float)*a[3] 是声明符,在诸如 float f(int, float)int *a[3]).

的声明中

因为 6.2.1 7,程序员可以写这个定义:

void *p[][1] = { { p[1] }, { p[0] } };

考虑初始化程序 p[1]。这是一个数组,因此它会自动转换为指向其第一个元素 p[1][0] 的指针。编译器知道该地址,因为它知道 p[i] 是 1 void * 的数组(对于 i 的任何值)。如果编译器不知道 p[i] 有多大,就无法计算出这个地址。所以,如果 C 标准允许我们这样写:

void *p[][] = { { p[1] }, { p[0] } };

那么编译器将不得不继续扫描过去 p[1] 以便它可以计算为第二维给出的初始化器的数量(在这种情况下只有一个,但我们必须至少扫描到 } 看到了,还可以更多),然后返回计算 p[1].

的值

该标准避免强制编译器执行此类 multiple-pass 工作。要求编译器推断内部维度会违反此目标,因此标准不这样做。

(事实上,我认为该标准可能不需要编译器做任何超过有限数量的 look-ahead,在标记化期间可能只需要几个字符,在解析语法时可能只需要一个标记,但是我不确定。有些东西的值直到 link 时间才知道,例如 void (*p)(void) = &SomeFunction;,但这些值是由 link 人填写的。)

此外,考虑如下定义:

char x[][] =
    {
        {  0,  1 },
        { 10, 11 },
        { 20, 21, 22 }
    };

当编译器读取前两行初始值时,它可能想在内存中准备一份数组副本。因此,当它读取第一行时,它将存储两个值。然后它看到线的末端,所以它可以假设内部维度为 2,形成 char x[][2]。当它看到第二行时,它会分配更多内存(与 realloc 一样)并继续,将接下来的两个值 10 和 11 存储在适当的位置。

当它读到第三行看到22时,它意识到内部维度至少是三。现在编译器不能简单地分配更多内存。它必须重新排列 10 和 11 在内存中相对于 0 和 1 的位置,因为它们之间有一个新元素; x[0][2] 现在存在并且值为 0(到目前为止)。因此,要求编译器推断内部维度,同时还允许每个子数组中有不同数量的初始化器(并根据整个列表中看到的最大初始化器数量来推断内部维度)会给编译器带来大量内存移动的负担。