Динамическая память

Выделение и освобождение памяти

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

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

double numbers[5] = {1.0, 2.0, 3.0, 4.0, 5.0};

Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.

Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h:

  • malloc(). Имеет прототип

    void *malloc(unsigned s);

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

  • calloc(). Имеет прототип

    void *calloc(unsigned n, unsigned m);

    Выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL

  • realloc(). Имеет прототип

    void *realloc(void *bl, unsigned ns);

    Изменяет размер ранее выделенного блока памяти, на начало которого указывает указатель bl, до размера в ns байт. Если указатель bl имеет значение NULL, то есть память не выделялась, то действие функции аналогично действию malloc

  • free(). Имеет прототип

    void *free(void *bl);

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

malloc

Функция malloc() выделяет память длиной для определенного количества байт и возвращает указатель на начало выделенной памяти. Через полученный указатель мы можем помещать данные в выделенную память. Рассмотрим простой пример:

#include <stdio.h>
#include <stdlib.h>	// для подключения функции malloc

int main(void)
{
	int *ptr = malloc(sizeof(int));	// выделяем память для одного int
	*ptr = 24;		// помещаем значение в выделенную память

	printf("%d \n", *ptr);
	free(ptr);
}

Здесь с помощью функции malloc выделяется память для одного объекта int. Чтобы узнать, сколько байтов надо выделить, передаем в функцию malloc размер типа int на текущей и в результате получаем указатель ptr, который указывает на выделенную память

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

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

int *ptr = malloc(sizeof *ptr);

Для универсальности возвращаемого значения в качестве результата функция malloc() (как и calloc() и realloc()) возвращает указатель типа void *. Но в нашем случае создается массив типа int, для управления которым используется указатель типа int *, поэтому выполняется неявное приведение результата функции malloc к типу int *.

Далее через этот указатель с помощью операции разыменования помещаем в выделенный участок памяти число 24:

*ptr = 24;

В дальнейшем, используя указатель, можно получить значение из выделенного участка памяти:

printf("%d \n", *ptr);

После завершения работы освобождаем память, передавая указатель в функцию free():

free(ptr);

Стоит отметить, что теоретически мы можем столкнуться с тем, что функции malloc() не удастся выделить требуемую память, и тогда она возвратит NULL. Чтобы избежать подобной ситуации перед использованием указателя мы можем проверять его на значение NULL:

#include <stdio.h>
#include <stdlib.h>	// для подключения функции malloc

int main(void)
{
	int *ptr = malloc(sizeof(int));	// выделяем память для одного int
	if(ptr != NULL)
	{
		*ptr = 24;		// помещаем значение в выделенную память
		printf("%d \n", *ptr);
	}
	free(ptr);
}

Немотря на освобождение памяти с помощью функции free() указатель сохраняет свой адрес, и теоретически мы можем обращаться к памяти по данному указателю. Однако полученные значения уже будут неопределенными и недетеминированными. Поэтому некоторые советуют после освобождения памяти также устанавливать для указателя значение NULL:

free(ptr);
ptr = NULL;

Выделение памяти для массива

Подобным образом можно выделять память и под набор объектов. Например, выделим память для массива из 4-х чисел int:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int n = 4;
	int *ptr = malloc(n * sizeof(int));	// выделяем память для 4-х чисел int
	if(ptr)
	{
		// помещаем значения в выделенную память
		ptr[0] = 1;
		ptr[1] = 2;
		ptr[2] = 3;
		ptr[3] = 5;
		// получаем значения
		for(int i = 0; i < n; i++)
		{
			printf("%d", ptr[i]);
		}
	}

	free(ptr);
}

Выделение памяти для структуры

Аналогичным образом можно выделять память для одной или набора структур:

#include <stdio.h>
#include <stdlib.h>

struct person
{
	char* name;
	int age;
};

int main(void)
{
	// выделяем память для одной структуры person
	struct person *ptr = malloc(sizeof(struct person));
	if(ptr)
	{
		// помещаем значения в выделенную память
		ptr->name = "Tom";
		ptr->age = 38;

		// получаем значения
		printf("%s : %d", ptr->name, ptr->age);		// Tom : 38
	}

	free(ptr);
	return 0;
}

calloc

Функция calloc() имеет прототип

void *calloc(unsigned n, unsigned m);

Она выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL

В отличие от функции malloc() она инициализирует все выделенные байты памяти нулями. Например, выделим память для одного объекта int:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	// выделяем память для одного объекта int
	int *ptr = calloc(1, sizeof(int));
	if(ptr)
	{
		// получаем значение по умолчанию - 0
		printf("Initial value: %d", *ptr);	// Initial value: 0
		// устанавливаем новое значение
		*ptr = 15;
		// получаем новое значение
		printf("New value: %d", *ptr);		// New value: 15
	}

	free(ptr);
	return 0;
}

Консольный вывод:

Initial value: 0
New value: 15

Подобным образом можно выделить память и для других объектов. Например, выделим память для массива из 4-х объектов int:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	// выделяем память для 4-х объектов int
	int n = 4;
	int *ptr = calloc(n, sizeof(int));
	if(ptr)
	{
		// устанавливаем значения
		ptr[0] = 1;
		ptr[1] = 2;
		ptr[2] = 3;
		ptr[3] = 5;
		// получаем значения
		for(int i = 0; i < n; i++)
		{
			printf("%d", ptr[i]);
		}
	}

	free(ptr);
}

realloc

Функция realloc() позволяет изменить размер памяти, ранее выделенной с помощью функций malloc() b calloc(). Имеет прототип

void *realloc(void *bl, unsigned ns);

Первый параметр представляет указатель на ранее выделенный блок памяти. А второй параметр представляет новый размер блока памяти в байтах.

Если указатель bl имеет значение NULL, то есть память не выделялась, то действие функции аналогично действию malloc

Рассмотрим небольшой пример:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	// выделяем память для 1-го объекта int
	int size = sizeof(int);
	int *ptr = malloc(size);
	if(ptr)
	{
		// отображаем адрес и размер памяти
		printf("Addresss: %p \t Size: %d\n", (void*)ptr, size);
	}
 	// расширяем память до размера 4-х объектов int
	size = 4 * sizeof(int);
 	int *ptr_new = realloc(ptr, size);
	// если выделение памяти прошло успещно
	if(ptr_new)
	{
		printf("Reallocation\n");
		// заново отображаем адрес и размер памяти
 		printf("Addresss: %p \t Size: %d\n", (void*)ptr_new, size);
		free(ptr_new);	// освобождаем новый указатель
	}
	else
	{
		free(ptr);	// освобождаем старый указатель
	}
}

Здесь сначала выделяем память для одного объекта int с помощью функции malloc.

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

Если память успешно выделена, то выводим на консоль адрес и размер выделенного блока памяти. Затем с помощью функции realloc расширяем память до 4 объектов int

size = 4 * sizeof(int);
int *ptr_new = realloc(ptr, size);

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

Консольный вывод в моем случае

Addresss: 0000018B078A82F0       Allocated: 4
Reallocation
Addresss: 0000018B078A82F0       Allocated: 16

Стоит отметить, что нам необязательно создавать новый указатель, мы можем присвоить значение старому указателю:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
	int size = sizeof(int);
	int *ptr = malloc(size);
	if(ptr)
	{
		printf("Addresss: %p \t Allocated: %d\n", (void*)ptr, size);
	}
	size = 4 * sizeof(int);
 	ptr = realloc(ptr, size);	// используем старый указатель
	if(ptr)
	{
		printf("Reallocation\n");
 		printf("Addresss: %p \t Allocated: %d\n", (void*)ptr, size);
	}
	free(ptr);
}
Дополнительные материалы
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850