【问题标题】:Why does a C/C++ compiler need know the size of an array at compile time?为什么 C/C++ 编译器需要在编译时知道数组的大小?
【发布时间】:2011-05-19 12:00:15
【问题描述】:

我知道 C99(以及 C++)之前的 C 标准规定堆栈上数组的大小必须在编译时知道。但这是为什么呢?堆栈上的数组是在运行时分配的。那么为什么大小在编译时很重要?希望有人向我解释编译器在编译时将如何处理大小。谢谢。

这样一个数组的例子是:

void func()
{
    /*Here "array" is a local variable on stack, its space is allocated
     *at run-time. Why does the compiler need know its size at compile-time?
     */
   int array[10]; 
}

【问题讨论】:

    标签: c++ c


    【解决方案1】:

    要了解为什么可变大小的数组实现起来更复杂,您需要了解一下自动存储持续时间(“本地”)变量通常是如何实现的。

    局部变量倾向于存储在运行时堆栈中。堆栈基本上是一个大的内存数组,它被顺序分配给局部变量,并有一个指向当前“高水位线”的索引。这个索引是堆栈指针

    进入函数时,栈指针向一个方向移动,为局部变量在栈上分配内存;当函数退出时,堆栈指针向另一个方向移回,以释放它们。

    这意味着局部变量在内存中的实际位置仅参考函数入口1处的堆栈指针的值来定义。函数中的代码必须通过堆栈指针的偏移量来访问局部变量。要使用的确切偏移量取决于局部变量的大小。

    现在,当所有局部变量的大小在编译时固定时,堆栈指针的这些偏移量也是固定的 - 因此它们可以直接编码到编译器发出的指令中。例如,在这个函数中:

    void foo(void)
    {
        int a;
        char b[10];
        int c;
    

    a 可能被访问为STACK_POINTER + 0b 可能被访问为STACK_POINTER + 4c 可能被访问为STACK_POINTER + 14

    但是,当您引入一个可变大小的数组时,这些偏移量就不能再在编译时计算;其中一些会根据数组在函数调用时的大小而有所不同。这使编译器编写者的事情变得更加复杂,因为他们现在必须编写访问STACK_POINTER + N 的代码——而且由于N 本身不同,它也必须存储在某个地方。这通常意味着进行两次访问 - 一次访问 STACK_POINTER + <constant> 以加载 N,然后另一次加载或存储感兴趣的实际局部变量。


    1.事实上,“函数入口处的堆栈指针的值”是一个非常有用的值,它有自己的名称 - 帧指针 - 许多 CPU 提供了一个单独的寄存器专用于存储帧指针。在实践中,通常是计算局部变量位置的帧指针,而不是堆栈指针本身。

    【讨论】:

    • caf:这是一个很好的解释 +1。可能你也可以引入帧指针的概念
    • @caf:很抱歉吹毛求疵,但术语是“VLA”或可变长度数组,而不是可变大小数组 AFAIK
    • @Chubsdad:VLA 确实是 C 中使用的术语,但我相信这个概念/解释也适用于其他编译语言,而且我使用的通用术语似乎很清楚。
    • @caf: one note :) 事情很简单,只要函数中有一个 VLA --> 只需将其存储放在最后(毕竟标准中没有保证变量将按照它们声明的顺序排列)。不幸的是,您仍然需要考虑多个 VLA 案例...
    • @Mattheiu M:确实如此,至少在具有显式帧指针的情况下。
    【解决方案2】:

    支持并不是一件极其复杂的事情,所以C89不允许这样做的原因是不是,因为当时不可能。

    然而,它不在 C89 中的原因有两个:

    1. 如果在编译时数组大小未知,则运行时代码的效率会降低。
    2. 支持这一点会使编译器编写者的生活更加艰难。

    从历史上看,C 编译器应该(相对)易于编写是非常重要的。此外,应该有可能使编译器足够简单和小,以在适度的计算机系统上运行(按照 80 年代的标准)。 C 的另一个重要特性是生成的代码应该始终非常高效,没有任何意外,

    我认为不幸的是,这些值不再适用于 C99。

    【讨论】:

    • 嗯,它不在C89中的主要原因是因为C89是在“标准化现有实践,不要发明”的前提下编写的,它是't 在现有的 C 实现中。
    • 我想说 C99 的一个很好的部分也是标准化现有实践 - 如果我记得的话,VLA、复合文字、long long、命名结构/联合元素初始化器等都在 gcc 中早于 C99正确的。当然有像tgmath.h 这样丑陋的东西,但我想说丑陋的发明只是新事物的一小部分。 gcc 团队至少应该得到与委员会一样的夸大 C 标准的功劳。 :-)
    【解决方案3】:

    编译器必须生成代码来为堆栈上的帧创建空间以保存数组和其他局部局部变量。为此,它需要数组的大小。

    【讨论】:

    • 显然它在编译时不需要它,因为这是在 C99 中修改的。
    • 处理非常量的栈帧布局需要更多的复杂性,而且没有帧指针很难/不可能做到。
    • @R.:这绝对不是那么复杂,但是是的,问题是复杂性和运行时性能下降。
    • 很多 C 的设计都是基于容易(对于编译器编写者)实现的。这就是为什么它是最底层的“高级”语言之一。早期版本的 C 语言不一定会遗漏某些东西,而不仅仅是不重要的东西。
    • @R..:这有点痛苦,但是否保留帧指针(至少在通常意义上)对复杂性的影响很小。
    【解决方案4】:

    在 C++ 中,这变得更加难以实现,因为存储在堆栈中的变量必须在发生异常或从给定函数或范围返回时调用其析构函数。跟踪要销毁的变量的确切数量/大小会增加额外的开销和复杂性。虽然在 C 中可以使用帧指针之类的东西来隐式释放 VLA,但在 C++ 中这对您没有帮助,因为需要调用这些析构函数。

    此外,VLA 可能会导致拒绝服务安全漏洞。如果用户能够提供最终用作 VLA 大小的任何值,那么他们可以使用足够大的值来导致进程中的堆栈溢出(并因此失败)。

    最后,C++ 已经有了一个安全有效的可变长度数组 (std::vector<t>),因此没有理由为 C++ 代码实现此功能。

    【讨论】:

    • vector 不如 VLA相当好,因为它需要堆内存分配,这可能很慢,尤其是在线程环境中。
    • @Zan:VLA 不如向量好,因为它可以破坏堆栈。我猜不同的人有不同的笔画。我宁愿慢而不是终止进程,TYVM :) (但 +1 评论)
    【解决方案5】:

    取决于你如何分配数组。

    如果您将其创建为局部变量并指定长度,那么这很重要,因为编译器需要知道在堆栈上为数组元素分配多少空间。如果不指定数组的大小,则它不知道为数组元素留出多少空间。

    如果你只创建一个指向数组的指针,那么你需要做的就是为指针本身分配空间,然后你就可以在运行时动态创建数组元素。但在这种数组创建形式中,您是在堆中为数组元素分配空间,而不是在堆栈中。

    【讨论】:

    • 我同意“堆”实现要容易得多,但这意味着虚假堆分配(成本)和缓存局部性会受到影响。
    • 哦,我不是在提倡其中一种。您的要求将决定这一点。这只是另一种方法。
    【解决方案6】:

    假设您在堆栈上创建了一个可变大小的数组。函数所需的堆栈帧的大小在编译时是未知的。因此,C 假设某些运行时环境需要预先知道这一点。因此有限制。 C 可以追溯到 1970 年代初期。当时的许多语言都有“静态”的外观和感觉(比如 Fortran)

    【讨论】:

    • 当时的许多语言也具有非常“动态”的外观和感觉(如 Lisp、Forth)。
    • 在当时,这些语言与 C 相比性能很差,这可能是阻止人们尝试用 C 做同样事情的一个重要因素。
    猜你喜欢
    • 2015-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-24
    • 1970-01-01
    相关资源
    最近更新 更多