为什么这个 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;
}
诊断
问题代码中,变量n
在vla
的定义中使用时未初始化。事实上,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 或更高版本。
为什么这段定义和使用 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;
}
诊断
问题代码中,变量n
在vla
的定义中使用时未初始化。事实上,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 或更高版本。