由于我不完全清楚的原因,几乎每次在讨论中出现 C99 VLA 的话题时,人们开始主要讨论将运行时大小的数组声明为本地对象的可能性(即在堆栈上创建它们) ”)。这是相当令人惊讶和误导的,因为 VLA 功能的这一方面 - 支持本地数组声明 - 恰好是 VLA 提供的一种相当辅助的次要功能。它在 VLA 可以做的事情中并没有真正发挥任何重要作用。大多数时候,地方 VLA 声明及其伴随的潜在陷阱的问题被 VLA 批评者强行放到了前台,他们将其用作“稻草人”,旨在破坏讨论并将其陷入几乎不相关的细节中。
C 中 VLA 支持的本质首先是对语言类型概念的革命性定性扩展。它涉及引入诸如可变修改类型之类的全新类型。实际上,与 VLA 相关的每个重要实现细节实际上都附加到它的类型上,而不是附加到 VLA 对象本身。正是在语言中引入了可变修改的类型,这构成了众所周知的 VLA 蛋糕的大部分,而在本地内存中声明此类类型的对象的能力只不过是在蛋糕上的一个微不足道且相当无关紧要的糖霜。
考虑一下:每次在自己的代码中声明这样的内容
/* Block scope */
int n = 10;
...
typedef int A[n];
...
n = 5; /* <- Does not affect `A` */
可变修改类型的大小相关特征A
(例如 的值n
)在控制通过上述 typedef 声明的确切时刻最终确定。对 的值进行的任何更改n
(在此声明的下方A
)都不会影响A
. 停下来想一想这意味着什么。这意味着该实现应该与A
一个隐藏的内部变量相关联,该变量将存储数组类型的大小。这个隐藏的内部变量是n
在运行时在控件传递A
.
这给了上面的 typedef-declaration 一个相当有趣和不寻常的属性,这是我们以前从未见过的:这个 typedef-declaration 生成可执行代码(!)。此外,它不仅会生成可执行代码,还会生成至关重要的可执行代码。如果我们不知何故忘记初始化与这种 typedef 声明相关的内部变量,我们将得到一个“损坏”/未初始化的 typedef 别名。该内部代码的重要性是该语言对此类可变修改的声明施加一些不寻常限制的原因:该语言禁止将控制权从其范围之外传递到其范围内
/* Block scope */
int n = 10;
goto skip; /* Error: invalid goto */
typedef int A[n];
skip:;
再次注意,上面的代码没有定义任何 VLA 数组。它只是为可变修改的类型声明了一个看似无辜的别名。然而,跳过这样的 typedef 声明是非法的。(我们已经熟悉 C++ 中这种与跳转相关的限制,尽管在其他上下文中)。
typedef
需要运行时初始化的代码生成与“经典”语言中的typedef
内容有很大不同。typedef
(它也恰好对在 C++ 中采用 VLA 的方式构成了重大障碍。)
当声明一个实际的 VLA 对象时,除了分配实际的数组内存之外,编译器还会创建一个或多个隐藏的内部变量,这些变量保存所讨论的数组的大小。人们必须明白,这些隐藏变量与数组本身无关,而是与它的可变修改类型相关联。
这种方法的一个重要且显着的结果如下:与 VLA 相关的有关数组大小的附加信息并未直接构建到 VLA 的对象表示中。它实际上存储在数组之外,作为“sidecar”数据。这意味着(可能是多维的)VLA 的对象表示与具有相同维度和相同大小的普通经典编译时大小数组的对象表示完全兼容。例如
void foo(unsigned n, unsigned m, unsigned k, int a[n][m][k]) {}
void bar(int a[5][5][5]) {}
int main(void)
{
unsigned n = 5;
int vla_a[n][n][n];
bar(a);
int classic_a[5][6][7];
foo(5, 6, 7, classic_a);
}
上述代码中的两个函数调用都是完全有效的,并且它们的行为完全由语言定义,尽管我们传递了一个 VLA,其中需要一个“经典”数组,反之亦然。当然,编译器无法控制此类调用中的类型兼容性(因为至少有一种涉及的类型是运行时大小的)。但是,如果需要,编译器(或用户)拥有在调试版本的代码中执行运行时检查所需的一切。
(注意:像往常一样,数组类型的参数总是隐式调整为指针类型的参数。这适用于 VLA 参数声明,就像它适用于“经典”数组参数声明一样。这意味着在上面的示例中,参数a
实际上具有 type int (*)[m][k]
。这种类型不受 值的影响n
。我特意为数组添加了一些额外的维度,以保持它对运行时值的依赖。)
VLA 和“经典”数组作为函数参数之间的兼容性也得到了以下事实的支持:编译器不必为可变修改的参数附带任何关于其大小的附加隐藏信息。相反,语言语法强制用户公开传递这些额外信息。在上面的例子中,用户被迫首先包含参数n
,m
然后k
进入函数参数列表。如果不声明n
,首先m
,k
用户将无法声明a
(另请参阅上面关于 的注释n
)。这些由用户显式传递给函数的参数将带来有关a
.
再举一个例子,通过利用 VLA 支持,我们可以编写以下代码
#include <stdio.h>
#include <stdlib.h>
void init(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
a[i][j] = rand() % 100;
}
void display(unsigned n, unsigned m, int a[n][m])
{
for (unsigned i = 0; i < n; ++i)
for (unsigned j = 0; j < m; ++j)
printf("%2d%s", a[i][j], j + 1 < m ? " " : "\n");
printf("\n");
}
int main(void)
{
int a1[5][5] = { 42 };
display(5, 5, a1);
init(5, 5, a1);
display(5, 5, a1);
unsigned n = rand() % 10 + 5, m = rand() % 10 + 5;
int (*a2)[n][m] = malloc(sizeof *a2);
init(n, m, *a2);
display(n, m, *a2);
free(a2);
}
这段代码旨在提醒您注意以下事实:这段代码大量使用了可变修改类型的有价值的属性。没有 VLA 就不可能优雅地实现。这就是为什么在 C 中迫切需要这些属性来替换以前在它们的位置使用的丑陋的 hack 的主要原因。然而同时,在上述程序中,甚至没有在本地内存中创建一个 VLA,这意味着这种流行的 VLA 批评向量根本不适用于这段代码。
基本上,上面最后两个示例是对 VLA 支持意义的简明说明。