Как компилятор выделяет память, не зная размер во время компиляции?

Я написал программу на 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 ответов


это не "статическое выделение памяти". Ваш массив k - массив переменной длины (VLA), что означает, что память для этого массива выделяется во время выполнения. Размер будет определяться значением времени выполнения n.

спецификация языка не диктует какой-либо конкретный механизм распределения, но в типичной реализации ваш k обычно в конечном итоге будет простой int * указатель с фактическим блоком памяти, выделяемым в стеке при запуске время.

для вла sizeof оператор также оценивается во время выполнения, поэтому вы получаете правильное значение из него в своем эксперименте. Просто используйте %zu (не %ld) для печати значений типа size_t.

основная цель malloc (и другие функции динамического выделения памяти)-переопределение правил времени жизни на основе области, которые применяются к локальным объектам. Т. е. память, выделенная с помощью malloc остается выделенным "навсегда", или пока вы явно освободите его с free. Выделенную память с malloc не освобождается автоматически в конце блока.

VLA, как и в вашем примере, не предоставляет эту "поражающую область" функциональность. Ваш массив k по-прежнему подчиняется регулярным правилам времени жизни на основе области: его срок службы заканчивается в конце блока. По этой причине в общем случае VLA не может заменить malloc и другие функции динамического распределения памяти.

но в конкретных случаях, когда вам не нужно "победить область" и просто использовать malloc чтобы выделить массив размера времени выполнения, VLA действительно может рассматриваться как замена malloc. Просто имейте в виду, опять же, что VLAs обычно выделяются в стеке, и выделение больших кусков памяти в стеке по сей день остается довольно сомнительной практикой программирования.


В C средства, с помощью которых компилятор поддерживает VLAs (массивы переменной длины), зависят от компилятора - ему не нужно использовать malloc(), и может (и часто делает) использовать то, что иногда называют "стековой" памятью - например, используя системные функции, такие как alloca() которые не являются частью стандарта C. Если он использует стек, максимальный размер массива обычно намного меньше, чем это возможно с помощью malloc(), потому что современные операционные системы позволяют программам гораздо меньшую квоту стека память.


память для массивов переменной длины явно не может быть статически выделена. Однако он может быть выделен в стеке. Как правило, это связано с использованием "указателя кадра" для отслеживания местоположения кадра стека функций перед динамически определенными изменениями указателя стека.

когда я пытаюсь скомпилировать вашу программу, кажется, что на самом деле происходит то, что массив переменной длины оптимизирован. Поэтому я изменил ваш код, чтобы заставить компилятор фактически выделите массив.

#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 компиляция для arm с помощью 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  "%d0"
.LC1:
        .ascii  "%s %ld0"

память для этой конструкции, которая называется "массив переменной длины", VLA, выделяется в стеке аналогично alloca. Точно, как это происходит, зависит от того, какой именно компилятор вы используете, но по сути это случай вычисления размера, когда он известен, а затем вычитания [1] общего размера из указателя стека.

вам нужно malloc и друзьями, потому что это распределение "умирает", когда вы покидаете функцию. [И это недопустимо в стандарте C++]

[1] для типичных процессоров, которые используют стек, который "растет к нулю".


когда говорят, что компилятор выделяет память для переменных в время компиляции, это означает, что размещение этих переменных определяется и встроено в исполняемый код, который генерирует компилятор, а не то, что компилятор делает пространство для них доступным во время работы. Фактическое динамическое выделение памяти выполняется сгенерированной программой при ее запуске.