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

How does the compiler allocate memory without knowing the size at compile time?

我编写了一个 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() 的动态分配有什么用?

此构造的内存称为 "variable length array",VLA,以与 alloca 类似的方式在堆栈上分配。具体如何发生取决于您使用的编译器,但本质上是计算已知大小,然后从堆栈指针中减去 [1] 总大小的情况。

你确实需要malloc和朋友因为这个分配"dies"时你离开了这个功能。 [并且它在标准 C++ 中无效]

[1] 对于使用 "grows towards zero".

堆栈的典型处理器

这不是 "static memory allocation"。您的数组 k 是可变长度数组 (VLA),这意味着此数组的内存是在 运行 时分配的。大小将由 n.

的 运行 时间值决定

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

对于 VLA sizeof 运算符也在 运行 时间进行评估,这就是为什么您在实验中从中获得正确值的原因。只需使用 %zu(而不是 %ld)来打印 size_t.

类型的值

malloc(以及其他动态内存分配函数)的主要目的是覆盖适用于本地对象的基于作用域的生命周期规则。 IE。使用 malloc 分配的内存仍然分配 "forever",或者直到您使用 free 显式取消分配它。使用 malloc 分配的内存不会在块末尾自动释放。

VLA,如您的示例,不提供此 "scope-defeating" 功能。您的数组 k 仍然遵守常规的基于作用域的生命周期规则:它的生命周期在块的末尾结束。因此,在一般情况下,VLA 不可能取代 malloc 和其他动态内存分配函数。

但在特定情况下,当您不需要 "defeat scope" 并且只需使用 malloc 来分配 运行 时间大小的数组时,VLA 可能确实被视为替代品malloc。请再次记住,VLA 通常在堆栈上分配,并且直到今天在堆栈上分配大块内存仍然是一个相当有问题的编程实践。

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

可变长度数组的内存显然不能静态分配。然而,它可以分配在堆栈上。通常,这涉及使用 "frame pointer" 来跟踪函数堆栈帧的位置,以应对堆栈指针的动态确定更改。

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

#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 因为我可以读取 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[=11=]0"
.LC1:
        .ascii  "%s %ld[=11=]0"

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