Как компилятор выделяет память, не зная размер во время компиляции?
Я написал программу на 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] для типичных процессоров, которые используют стек, который "растет к нулю".
когда говорят, что компилятор выделяет память для переменных в время компиляции, это означает, что размещение этих переменных определяется и встроено в исполняемый код, который генерирует компилятор, а не то, что компилятор делает пространство для них доступным во время работы. Фактическое динамическое выделение памяти выполняется сгенерированной программой при ее запуске.