gpt4 book ai didi

c - 无论如何,VLA 的意义何在?

转载 作者:行者123 更新时间:2023-12-02 14:14:37 28 4
gpt4 key购买 nike

我了解什么是可变长度数组以及它们是如何实现的。这个问题是关于它们为什么存在。

我们知道 VLA 只允许在功能块(或原型(prototype))中使用,并且它们基本上不能在堆栈上的任何地方(假设正常实现):C11,6.7.6.2-2:

If an identifier is declared as having a variably modified type, it shall be an ordinary identifier (as defined in 6.2.3), have no linkage, and have either block scope or function prototype scope. If an identifier is declared to be an object with static or thread storage duration, it shall not have a variable length array type.



让我们举一个小例子:
void f(int n)
{
int array[n];
/* etc */
}

有两种情况需要注意:
  • n <= 0 :f必须防止这种情况,否则行为未定义:C11,6.7.6.2-5(强调我的):

    If the size is an expression that is not an integer constant expression: if it occurs in a declaration at function prototype scope, it is treated as if it were replaced by *; otherwise, each time it is evaluated it shall have a value greater than zero. The size of each instance of a variable length array type does not change during its lifetime. Where a size expression is part of the operand of a sizeof operator and changing the value of the size expression would not affect the result of the operator, it is unspecified whether or not the size expression is evaluated.

  • n > stack_space_left / element_size : 没有找到剩余多少堆栈空间的标准方法(因为只要涉及标准,就没有堆栈这样的东西)。所以这个测试是不可能的。唯一明智的解决方案是为 n 设置一个预定义的最大可能大小,说 N , 以确保不会发生堆栈溢出。

  • 换句话说,程序员必须确保 0 < n <= N一些 N选择。但是,该程序应该适用于 n == N无论如何,因此不妨声明一个大小恒定的数组 N而不是可变长度 n .

    我知道引入 VLA 是为了取代 alloca (正如在 this answer 中提到的),但实际上它们是一样的(在堆栈上分配可变大小的内存)。

    所以问题是为什么 alloca因此 VLA 存在,为什么它们没有被弃用?在我看来,使用 VLA 的唯一安全方法是使用有界大小,在这种情况下,采用具有最大大小的普通数组始终是一个可行的解决方案。

    最佳答案

    出于我不太清楚的原因,几乎每次讨论中出现 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 声明一个相当有趣和不寻常的属性,这是我们以前从未见过的:这个 typedef 声明生成可执行代码(!)。此外,它不仅生成可执行代码,还生成极其重要的可执行代码。如果我们以某种方式忘记初始化与此类 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 实际上是具有类型 int (*)[m][k] 。此类型不受 n 的值的影响。我特意向数组添加了一些额外的维度以保持其对运行时值的依赖。)

    VLA 和作为函数参数的“经典”数组之间的兼容性也得到了以下事实的支持:编译器不必为可变修改的参数提供任何关于其大小的附加隐藏信息。相反,语言语法强制用户公开传递这些额外信息。在上面的例子中,用户被迫首先包含参数 n , mk进入函数参数列表。不声明 n , mk首先,用户将无法声明 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);
    }

    此代码旨在引起您对以下事实的注意:此代码大量使用可变修改类型的有值(value)的属性。没有 VLA 就不可能优雅地实现。这就是为什么在 C 中迫切需要这些属性来替换以前在它们的位置上使用的丑陋的 hack 的主要原因。然而同时,在上述程序中,甚至没有在本地内存中创建一个 VLA,这意味着 VLA 批评的这个流行 vector 根本不适用于这段代码。

    基本上,上面的最后两个示例简要说明了 VLA 支持的要点。

    关于c - 无论如何,VLA 的意义何在?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/22530363/

    28 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com