Управление памятью

Выбор типа памяти для хранения объектов

Последнее обновление: 08.09.2023

Для хранения объектов в программе на языке С в общем случае у нас есть 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
}

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

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

Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850