为什么这个 VLA(可变长度数组)定义不可靠?

Why is this VLA (variable-length array) definition unreliable?

为什么这段定义和使用 VLA(可变长度数组)的代码不能可靠地工作?

#include <stdio.h>

int main(void)
{
    int n;
    double vla[n];

    if (scanf("%d", &n) != 1)
        return 1;
    for (int i = 0; i < n; i++)
    {
        if (scanf("%lf", &vla[i]) != 1)
            return 1;
    }
    for (int i = 0; i < n; i++)
        printf("[%d] = %.2f\n", i, vla[i]);
    return 0;
}

诊断

问题代码中,变量nvla的定义中使用时未初始化。事实上,GCC 设置繁琐,显示的代码会产生编译错误(如果您粗心大意从编译选项中省略了 -Werror,它会发出警告 — 不要那样做!):

$ gcc -std=c11 -O3 -g -Wall -Wextra -Werror -Wstrict-prototypes -Wmissing-prototypes -Wshadow -pedantic-errors  vla37.c -o vla37  
vla37.c: In function ‘main’:
vla37.c:6:5: error: ‘n’ is used uninitialized [-Werror=uninitialized]
    6 |     double vla[n];
      |     ^~~~~~
vla37.c:5:9: note: ‘n’ declared here
    5 |     int n;
      |         ^
cc1: all warnings being treated as errors
$

(来自机器 运行ning RedHat RHEL 7.4 上的 GCC 11.2.0。)

麻烦的是编译器在声明时必须知道数组的大小,但是n中的值是未定义的(indeterminate),因为它是未初始化的。它可能很大;它可以是零;它可能是负面的。

处方

解决这个问题的方法很简单——在使用它来声明 VLA 之前确保大小已知且合理:

#include <stdio.h>

int main(void)
{
    int n;

    if (scanf("%d", &n) != 1)
        return 1;

    double vla[n];
    for (int i = 0; i < n; i++)
    {
        if (scanf("%lf", &vla[i]) != 1)
            return 1;
    }
    for (int i = 0; i < n; i++)
        printf("[%d] = %.2f\n", i, vla[i]);
    return 0;
}

现在您可以 运行 结果:

$ vla41 <<<'9 2.34 3.45 6.12 8.12 99.60 -12.31 1 2 3'
[0] = 2.34
[1] = 3.45
[2] = 6.12
[3] = 8.12
[4] = 99.60
[5] = -12.31
[6] = 1.00
[7] = 2.00
[8] = 3.00
$

(假定您的 shell 是 Bash 或与 Bash 兼容并支持 'here strings'(<<<'…' 表示法。)

问题和此答案中显示的代码不足以处理 I/O 错误;它检测输入问题但不向用户提供有用的反馈。 显示的代码未验证 n 的值是否合理。您应该确保大小大于零且小于某个上限。最大大小取决于存储在 VLA 中的数据大小和您所在的平台。

如果您使用的是类 Unix 机器,您可能有 8 MiB 的堆栈;如果你在 Windows 机器上,你可能有 1 MiB 的堆栈;如果您使用的是嵌入式系统,则可用的堆栈可能会少得多。您还需要为其他代码保留一些堆栈 space,因此为了便于讨论,您可能应该检查数组大小是否不超过 1024 — 对于 double,它一点也不大,但它为大多数家庭作业程序提供了大量 space。调整更大的数字以满足您的目的,但是当数字增长时,您应该使用 malloc() 等动态分配数组而不是使用堆栈上的 VLA。例如,在 Windows 机器上,如果您使用 int 类型的 VLA,将大小设置为 262,144(256 * 1024)以上几乎可以保证您的程序会崩溃,并且它可能会在稍小的时候崩溃比那个尺寸。

要学习的课程

  • 使用严格的警告选项进行编译。
  • 使用 -Werror 或其等效项进行编译,以便将警告视为错误。
  • 确保在定义数组之前初始化定义 VLA 大小的变量。
    • 不太小(不是零,不是负数)。
    • 不太大(在 Windows 上不超过 1 兆字节,在 Unix 上不超过 8 兆字节)。
    • 为其他代码留出足够的空间。

请注意,所有支持 VLA 的编译器也支持在函数内任意点定义的变量。这两个功能都是在 C99 中添加的。 VLA 在 C11 中是可选的——编译器应该定义 __STDC_NO_VLA__ 如果它根本不支持 VLA 但声称符合 C11 或更高版本。