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



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

633   5  

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] для типичных процессоров, которые используют стек, который "растет к нулю".

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

Comments

    Ничего не найдено.