编译器如何在不知道编译时的大小的情况下分配内存?

时间:2017-09-24 06:04:20

标签: c arrays memory c99 variable-length-array

我编写了一个C程序,它接受来自用户的整数输入,用作整数数组的大小,并使用该值声明一个给定大小的数组,我通过检查大小确认它阵列。

代码:

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%ld",sizeof(k));
    return 0;
}

令人惊讶的是它是正确的!该程序能够创建所需大小的数组 但所有静态内存分配都是在编译时完成的,并且在编译期间n的值是未知的,那么为什么编译器能够分配所需大小的内存呢?

如果我们可以像那样分配所需的内存,那么使用malloc()calloc()进行动态分配的用途是什么?

5 个答案:

答案 0 :(得分:72)

这不是&#34;静态内存分配&#34;。您的数组k是可变长度数组(VLA),这意味着此数组的内存是在运行时分配的。大小将由n的运行时值确定。

语言规范没有规定任何特定的分配机制,但在典型的实现中,k通常最终会成为一个简单的int *指针,其中实际的内存块在运行时分配在堆栈上时间。

对于VLA sizeof运算符也会在运行时进行评估,这就是您在实验中从中获取正确值的原因。只需使用%zu(不是%ld)打印size_t类型的值。

malloc(和其他动态内存分配函数)的主要目的是覆盖适用于本地对象的基于范围的生存期规则。即分配有malloc的内存仍然分配&#34;永远&#34;,或直到您使用free显式解除分配。分配有malloc的内存不会在块结束时自动解除分配。

在你的例子中,VLA没有提供这种&#34;范围失败&#34;功能。您的数组k仍然遵循常规的基于范围的生存期规则:它的生命周期在块的结尾处结束。因此,在一般情况下,VLA不可能替换malloc和其他动态内存分配函数。

但在特定情况下,当你不需要&#34;击败范围&#34;并且只需使用malloc来分配运行时大小的数组,VLA可能确实被视为malloc的替代。请记住,VLA通常是在堆栈上分配的,并且到目前为止在堆栈上分配大块内存仍然是一个相当可疑的编程实践。

答案 1 :(得分:11)

在C中,编译器支持VLA(可变长度数组)的方法取决于编译器 - 它不必使用malloc(),并且可以(并且经常)使用有时称为“堆叠”记忆 - 例如使用不属于标准C的系统特定函数,如alloca()。如果它确实使用堆栈,则数组的最大大小通常比使用malloc()小得多,因为现代操作系统允许程序堆栈内存的配额要小得多。

答案 2 :(得分:10)

可变长度数组的内存显然无法静态分配。然而,它可以在堆栈上分配。通常,这涉及使用“帧指针”来跟踪功能堆栈帧的位置,以面对堆栈指针的动态确定的更改。

当我尝试编译你的程序时,似乎实际发生的是可变长度数组被优化了。所以我修改了你的代码以强制编译器实际分配数组。

#include <stdio.h>
int main(int argc, char const *argv[])
{
    int n;
    scanf("%d",&n);
    int k[n];
    printf("%s %ld",k,sizeof(k));
    return 0;
}

Godbolt使用gcc 6.3编译手臂(使用arm因为我可以阅读arm ASM)将其编译为https://godbolt.org/g/5ZnHfa。 (评论我的)

main:
        push    {fp, lr}      ; Save fp and lr on the stack
        add     fp, sp, #4    ; Create a "frame pointer" so we know where
                              ; our stack frame is even after applying a 
                              ; dynamic offset to the stack pointer.
        sub     sp, sp, #8    ; allocate 8 bytes on the stack (8 rather
                              ; than 4 due to ABI alignment
                              ; requirements)
        sub     r1, fp, #8    ; load r1 with a pointer to n
        ldr     r0, .L3       ; load pointer to format string for scanf
                              ; into r0
        bl      scanf         ; call scanf (arguments in r0 and r1)
        ldr     r2, [fp, #-8] ; load r2 with value of n
        ldr     r0, .L3+4     ; load pointer to format string for printf
                              ; into r0
        lsl     r2, r2, #2    ; multiply n by 4
        add     r3, r2, #10   ; add 10 to n*4 (not sure why it used 10,
                              ; 7 would seem sufficient)
        bic     r3, r3, #7    ; and clear the low bits so it is a
                              ; multiple of 8 (stack alignment again) 
        sub     sp, sp, r3    ; actually allocate the dynamic array on
                              ; the stack
        mov     r1, sp        ; store a pointer to the dynamic size array
                              ; in r1
        bl      printf        ; call printf (arguments in r0, r1 and r2)
        mov     r0, #0        ; set r0 to 0
        sub     sp, fp, #4    ; use the frame pointer to restore the
                              ; stack pointer
        pop     {fp, lr}      ; restore fp and lr
        bx      lr            ; return to the caller (return value in r0)
.L3:
        .word   .LC0
        .word   .LC1
.LC0:
        .ascii  "%d\000"
.LC1:
        .ascii  "%s %ld\000"

答案 3 :(得分:3)

此构造的内存(称为“可变长度数组”,VLA)以与alloca类似的方式分配在堆栈上。具体如何发生这取决于你正在使用哪个编译器,但实际上它是一个在知道时计算大小的情况,然后从堆栈指针中减去[1]总大小。

你确实需要malloc和朋友,因为当你离开这个功能时,这个分配会“死亡”。 [它在标准C ++中无效]

[1]对于使用“向零增长”的堆栈的典型处理器。

答案 4 :(得分:0)

如果说编译器在编译时为变量分配内存,则意味着这些变量的位置决定并嵌入编译器生成的可执行代码中,而不是编译器在它工作时为它们提供空间。 实际的动态内存分配由生成的程序在运行时执行。