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

Я написал программу на 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()?

Ответ 1

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

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

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

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

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

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

Ответ 2

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

Ответ 3

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

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

#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 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"

Ответ 4

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

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

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

Ответ 5

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