📦10. Распределение памяти. Статическое и динамическое выделение памяти. Понятия «стек» и «куча».

Функции для выделения и освобождения памяти, особенности их использования, пример работы. Ошибки при работе с динамической памятью (утечка памяти, висячий указатель, двойное освобождение).

Распределение памяти

Способ хранения объекта в языке Си определяет его время жизни, то есть часть времени выполнения программы, во время которого объект существует и для него зарезервировано место. Объект, под который выделена память, имеет постоянный адрес и сохраняет свое последнее значение.

Статическое выделение памяти

Статическое выделение памяти - выделение памяти, при котором размер выделяемой памяти известен еще на этапе компиляции. Статические переменные имеют фиксированный адрес. При статическом выделении памяти операции выделения и освобождения памяти встраиваются в программу компилятором автоматически.

Для этого способа выделения памяти существует 2 способа хранения объектов: автоматический и статический.

Стек - это область оперативной памяти, создаваемая для каждого потока. Он работает по принципу FILO - первый вошел, последний вышел. Когда переменная объявляется, она попадает в стек, а когда пропадает из области видимости, то удаляется. Благодаря FILO не происходит ситуации, когда все еще нужная переменная удаляется из стека раньше времени. Размер стека задается во время запуска программы.

Динамическое выделение памяти

Динамическое выделение памяти - выделение памяти в ходе работы программы. Обычно используется в тех случаях, когда заранее неизвестен объем необходимой памяти. В случае динамического выделения памяти ответственность за выделение и освобождение памяти лежит на программисте.

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

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

Функции для работы с динамической памятью

В Си есть malloc.h , в котором определены 4 функции для работы с динамической памятью: malloc , calloc , realloc и free.

malloc

// Объявление malloc
void* malloc(size_t size);

Эта функция служит для выделения динамической памяти в куче. В качестве аргумента принимает размер памяти для выделения в байтах. Возвращает нетипизированный указатель (указатель на void ) на первый байт в выделенной памяти в случае успеха и NULL в случае неудачи.

Для получения нужного типа указателя следует использовать явное приведение типов:

(int*) malloc(sizeof(int));

Или положиться на неявное:

int* ptr = malloc(sizeof(int));

calloc

// Объявление calloc
void* calloc(size_t number, size_t size);

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

Основное отличие в работе в отличие от malloc - calloc заполняет выделенную память значением 0.

Примеры использования аналогичны с malloc .

realloc

// Объявление realloc
void* realloc(void* memblock, size_t size);

Эта функция изменяет размер выделенного блока памяти. В качестве аргументов принимает указатель на ранее выделенный блок памяти и новый размер в байтах. Возвращает нетипизированный указатель (указатель на void ) на первый байт в выделенной памяти в случае успеха и NULL в случае неудачи.

В первую очередь realloc попробует расширить существующий блок, при неудаче попытается выделить память в другом месте кучи, скопировать значения из старого в новое и освободить память из старого блока.

При выделении памяти в новом месте кучи значения из старого не стираются и все еще могут быть прочитаны, чем могут воспользоваться зловредные программы.

Пример использования:

int* array = malloc(8 * sizeof(int));
// заполнение массива
array = realloc(array, 10 * sizeof(int));
// заполнение двух новых элементов

free

// Объявление free
void free(void* memblock);

Эта функция освобождает ранее выделенную память. В качестве аргумента принимает указатель на ранее выделенный блок памяти, который требуется освободить.

Пример использования:

int* array = malloc(8 * sizeof(int));
// пользуемся array
//array больше не нужен
free(array);

Особенности работы с динамической памятью и связанные с ними ошибки

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

  • Объявляя указатель стоит помнить, что по умолчанию в нем ничего не хранится. Это значит, что его разыменование приведет к неопределенному поведению.

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

  • Утечка памяти - это ошибка, которая возникает в ситуации, когда не остается указателей на выделенную память. Если используешь динамическое выделение памяти - проследи за ее освобождением!

  • Висячий указатель - это ошибка, обратная утечке памяти - память освобождена, а указатель на нее все еще существует. При попытке его разыменовать будет неопределенное поведение.

  • Двойное освобождение памяти - это ошибка, которая возникает при использовании функции free дважды. Ведет к неопределенному поведению.

Last updated