Для хранения объектов в программе на языке С в общем случае у нас есть 3 варианта, где хранить объекты:
Стек (автоматическая память)
Статическая память
Динамическая память (иначе говоря куча или heap)
Стек представляет память фиксированной длины, которая выделяется для каждого потока. В зависимости от операционной системы размер стека может отличаться. При вызове функции в стек помещаются параметры и все автоматические (нестатические) переменные, которые определяются внутри функции. Соответственно, чтобы положить значение переменной стек, достаточно определеить нестатическую переменную внутри функции:
int main(void) { int number = 1; // переменная number хранится в стеке }
После вызова функции эта часть стека автоматически очищается.
Например, если мы посмотрим на ассемблерный вывод компилятора GCC для этой программы, то увидим программу наподобие следующей (применяется синтаксис ассемблера GAS):
.file "app.c" .text .def __main; .scl 2; .type 32; .endef .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $48, %rsp .seh_stackalloc 48 .seh_endprologue call __main movl $1, -4(%rbp) movl $0, %eax addq $48, %rsp popq %rbp ret .seh_endproc
Здесь нас инстресует прежде всего выражение
movl $1, -4(%rbp)
Это выражение помещает число 1 в память, на которую указывает регистр RBP минус 4 байта. Далее в программе через выражение -4(%rbp)
программа может ссылаться на значение
переменной numbers
В конце программы инструкция
addq $48, %rsp
Прибавляет к указателю стека 48 байт, и таким образом освобождает память.
Статическая память представляет блок память фиксированного размера, выделение которой происходит во время компиляции. Значения в статической памяти хранятся на протяжении всей работы программы. Статическая память обеспечивает быстрый доступ к данным, позволяет избежать фрагментации, характерной при использовании динамической памяти.
Но стоит отметить, что применение статической памяти имеет определенные недостатки:
Прежде всего, размер данных должен быть фиксирован и известен на стадии компиляции.
Во-вторых, если определен большой массив данных, большая часть которых не используется, тогда память будет расходоваться впустую.
Третья проблема связана с многопоточным выполнением: статическая память является общей для всех потоков программы, и в этом случае могут потребоваться дополнительные инструменты для разграничения доступа потоков к общим статическим данным.
Четвертая проблема статической памяти связана с тем, что программа может запускаться чуть дольше, поскольку надо выделить память для статических переменных и констант.
Для помещения переменной в статическую память эта переменная определяется с помощью ключевого слова static. Можно определять статические переменные на уровне функции
(такие переменные доступны только в текущей функции) и на уровне файла (доступны только в текущем файле). Также глобальные переменные без слова static
также помещаются
в статическую память:
int n1 = 1; static int n2= 2; int main(void) { static int n3 = 3; }
Здесь все три переменных: n1, n2 и n3 помещаются в статическую память. Различие между ними состоит в уровне доступа. n1 доступна во всей программе, в том числе в других файлах, которые при компиляции компилируются в один исполняемый файл или файл библиотеки. n2 доступна только в текущем файле. n3 доступа только в пределах функциии main.
Например, если мы посмотрим на ассемблерный вывод компилятора GCC для этой программы:
.file "app.c" .text .globl n1 .data .align 4 n1: .long 1 .align 4 n2: .long 2 .def __main; .scl 2; .type 32; .endef .text .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $32, %rsp .seh_stackalloc 32 .seh_endprologue call __main movl $0, %eax addq $32, %rsp popq %rbp ret .seh_endproc .data .align 4 n3.0: .long 3
Здесь мы видим, что все три переменных определены в секции .data
, которая хранит в ассеблере глобальные данные, доступные для всего файла программы. Однако переменная n1 также определена
с директивой .globl
, что позволяет увидеть эту переменную в других файлах программы:
.globl n1
А к n3 при определении добавляет дополнительно число 0:
n3.0: .long 3
И в дальнейшем на эту переменную программа будет ссылаться через идентификатор n3.0
Стоит отметить, что если внутри функции определяются данные, которые имеют фиксированный размер и не изменяются в течение программы, то они также определяются в статической памяти. Например, строки хранятся в статической памяти:
int main(void) { char* message = "hello"; }
Ассемблерный вывод программы:
.file "app.c" .text .def __main; .scl 2; .type 32; .endef .section .rdata,"dr" .LC0: .ascii "hello\0" .text .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $48, %rsp .seh_stackalloc 48 .seh_endprologue call __main leaq .LC0(%rip), %rax movq %rax, -8(%rbp) movl $0, %eax addq $48, %rsp popq %rbp ret .seh_endproc
Здесь мы видим, что строка "hello" определена в секции rdata и проецируется на метку .LC0
:
.section .rdata,"dr" .LC0: .ascii "hello\0"
То есть фактически .LC0
определена как глобальная переменная.
А переменная message, которая хранит адрес этой строки, определена как автоматическая переменная, которая удаляется после завершения работы функции:
leaq .LC0(%rip), %rax ; помещаем в регистр RAX адрес переменной .LC0 movq %rax, -8(%rbp) ; данные из регистра RAX помещаем в стек по адресу RBP-8
В дальнейшем к переменной message в коде ассемблера можно ссылаться через выражение -8(%rbp)
.
То же самое относится к константным массивам, например:
int main(void) { const int nums[] = {22, 34}; }
Здесь данные массива nums также будут храниться в статической памяти.
При использовании динамической памяти обязанность на выделение и особождение памяти ложиться на программиста. В общем случае с помощью функций malloc/calloc/realloc
выделяется память в куче (heap), а с помощью функции free()
память освобождается:
void main() { int* data = malloc(sizeof(int) * 100); // выделение памяти для 100 значений int // работа с data free(data); // освобождение памяти }
Динамическая память позволяет уйти от ограничений размера стека и ограничений фиксированного размера данных статической памяти, однако поскольку программы могут иметь сложную структуру, то должно освобождение памяти может представлять проблему. Кроме того, многократное выделение/особождение памяти может вести к дефрагментации памяти. В итоге даже если суммарный размер свободных фрагментов памяти превышает необходимый, операционная система все равно не сможет выделить память, так как размер наибольшего фрагмента меньше необходимого.
В общем случае если данные невелики по объему и необходимы в пределах одной функции, то следует их помещать в стек, то есть определять как автоматические переменные и констаты, которые автоматически уничтожаются после завершения функции. Например, следующая программа вполне корректна:
#include <stdio.h> #include <stdlib.h> int main(void) { int n = 100; // выделение памяти int* nums = malloc(sizeof(int) * n); // некоторая работа с nums nums[0] = 23; printf("%d\n", nums[0]); // освобождение памяти free(nums); }
Однако эта программа не эффективна. Мы вынуждены вручную управлять памятью. А на выделение и освобождение памяти тратятся дополнительные ресурсы. К тому же массив nums нам необходим локально. И в данном случае эффективнее определить массив как локальную автоматическую переменную:
#include <stdio.h> int main(void) { int nums[100]; // некоторая работа с nums nums[0] = 25; printf("%d\n", nums[0]); }
Если уж необходимо определение размера массива с помощью переменной, то начиная со стандарта С99 такая возможность поддерживается:
#include <stdio.h> int main(void) { int n = 100; int nums[n]; // некоторая работа с nums nums[0] = 25; printf("%d\n", nums[0]); }
Если данные фиксированного размера нужны в течение всей жизни программы, особенно если данные довольно большие, что есть вероятность, что они не поместятся в стек, то можно поместить данные в статическую память.
#include <stdio.h> int nums1[100]; // в статической памяти static int nums2[100]; // в статической памяти int main(void) { static int nums3[100]; // в статической памяти // некоторая работа с nums1,nums2, nums3 }
Другая проблема, связанная с автоматическими переменными и константами, состоит в том, что при передачи их значений между вызовами функций мы вынуждены использовать параметры, что ведет к издержкам, поскольку данные надо скопировать в параметры функции. И в этом случае глобальные (по сути статические) переменные и константы позволят избежать ненужного копирования, что особенно актуально при передаче больших данных.
Если автоматические и статические переменные и константы не подходят в силу различных ограничений, тогда применяется динамическая память.