Динамическая память
При создании массива с фиксированными размерами под него выделяется определенная память. Например, пусть у нас будет массив с пятью элементами:
1
double numbers[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
Для такого массива выделяется память 5 * 8 (размер типа double) = 40 байт. Таким образом, мы точно знаем, сколько в массиве элементов и сколько он занимает памяти. Однако это не всегда удобно. Иногда бывает необходимо, чтобы количество элементов и соответственно размер выделяемой памяти для массива определялись динамически в зависимости от некоторых условий. Например, пользователь сам может вводить размер массива. И в этом случае для создания массива мы можем использовать динамическое выделение памяти.
Для управления динамическим выделением памяти используется ряд функций, которые определены в заголовочном файле stdlib.h:
malloc(). Имеет прототип
1
void *malloc(unsigned s);
Выделяет память длиной в s байт и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL
calloc(). Имеет прототип
1
void *calloc(unsigned n, unsigned m);
Выделяет память для n элементов по m байт каждый и возвращает указатель на начало выделенной памяти. В случае неудачного выполнения возвращает NULL
realloc(). Имеет прототип
1
void *realloc(void *bl, unsigned ns);
Изменяет размер ранее выделенного блока памяти, на начало которого указывает указатель bl, до размера в ns байт. Если указатель bl имеет значение NULL, то есть память не выделялась, то действие функции аналогично действию malloc
free(). Имеет прототип
1
void *free(void *bl);
Освобождает ранее выделенный блок памяти, на начало которого указывает указатель bl.
Если мы не используем эту функцию, то динамическая память все равно освободится автоматически при завершении работы программы. Однако все же хорошей практикой является вызов функции free(), который позволяет как можно раньше освободить память.
Рассмотрим применение функций на простой задаче. Длина массива неизвестна и вводится во время выполнения программы пользователем, и также значения всех элементов вводятся пользователем:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *block; // указатель для блока памяти
int n; // число элементов массива
// ввод числа элементов
printf("Size of array=");
scanf("%d", &n);
// выделяем память для массива
// функция malloc возвращает указатель типа void*
// который автоматически преобразуется в тип int*
block = malloc(n * sizeof(int));
// вводим числа в массив
for(int i=0;i<n; i++)
{
printf("block[%d]=", i);
scanf("%d", &block[i]);
}
printf("\n");
// вывод введенных чисел на консоль
for(int i=0;i<n; i++)
{
printf("%d \t", block[i]);
}
// освобождаем память
free(block);
return 0;
}
Консольный вывод программы:
Size of array=5
block[0]=23
block[1]=-4
block[2]=0
block[3]=17
block[4]=81
23 -4 0 17 81
Здесь для управления памятью для массива определен указатель block типа int. Количество элементов массива заранее неизвестно, оно представлено переменной n.
Вначале пользователь вводит количество элементов, которое попадает в переменную n. После этого необходимо выделить память для данного количества элементов. Для выделения памяти здесь мы могли бы воспользоваться любой из трех вышеописанных функций: malloc, calloc, realloc. Но конкретно в данной ситуации воспользуемся функцией malloc:
1
block = malloc(n * sizeof(int));
Прежде всего надо отметить, что все три выше упомянутые функции для универсальности возвращаемого значения в качестве результата возвращают указатель типа void *. Но в нашем случае создается массив типа int, для управления которым используется указатель типа int *, поэтому выполняется неявное приведение результата функции malloc к типу int *.
В саму функцию malloc передается количество байтов для выделяемого блока. Это количество подсчитать довольно просто: достаточно умножить количество элементов на размер одного элемента n * sizeof(int).
После выполнения всех действий память освобождается с помощью функции free():
1
free(block);
Важно, что после выполнения этой функции мы уже не сможем использовать массив, например, вывести его значения на консоль:
1
2
3
4
5
free(block);
for(int i=0;i<n; i++)
{
printf("%d \t", block[i]);
}
И если мы попытаемся это сделать, то получим неопределенные значения.
Вместо функции malloc аналогичным образом мы могли бы использовать функцию calloc(), которая принимает количество элементов и размер одного элемента:
1
block = calloc(n, sizeof(int));
Либо также можно было бы использовать функцию realloc():
1
2
int *block = NULL;
block = realloc (block, n * sizeof(int));
При использовании realloc желательно (в некоторых средах, например, в Visual Studio, обязательно) инициализировать указатель хотя бы значением NULL.
Но в целом все три вызова в данном случае имели бы аналогичное действие:
1
2
3
block = malloc(n * sizeof(int));
block = calloc(n, sizeof(int));
block = realloc (block, n * sizeof(int));
Теперь рассмотрим более сложную задачу - динамическое выделение памяти для двухмерного массива:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int **table; // указатель для блока памяти для массива указателей
int *rows; // указатель для блока памяти для хранения информации по строкам
int rowscount; // количество строк
int d; // вводимое число
// ввод количества строк
printf("Rows count=");
scanf("%d", &rowscount);
// выделяем память для двухмерного массива
table = calloc(rowscount, sizeof(int*));
rows = malloc(sizeof(int)*rowscount);
// цикл по строкам
for (int i = 0; i<rowscount; i++)
{
printf("\nColumns count for row %d=", i);
scanf("%d", &rows[i]);
table[i] = calloc(rows[i], sizeof(int));
for (int j = 0; j<rows[i]; j++)
{
printf("table[%d][%d]=", i, j);
scanf("%d", &d);
table[i][j] = d;
}
}
printf("\n");
// вывод введенных чисел на консоль
for (int i = 0; i<rowscount; i++)
{
printf("\n");
for (int j = 0; j<rows[i]; j++)
{
printf("%d \t", table[i][j]);
}
// освобождение памяти для одной строки
free(table[i]);
}
// освобождение памяти
free(table);
free(rows);
return 0;
}
Переменная table представляет указатель на массив указателей типа int*. Каждый указатель table[i] в этом массиве представляет указатель на подмассив элементов типа int, то есть отдельные строки таблицы. А переменная table фактически представляет указатель на массив указателей на строки таблицы.
Для хранения количества элементов в каждом подмассиве определяется указатель rows типа int. Фактически он хранит количество столбцов для каждой строки таблицы.
Сначала вводится количество строк в переменную rowscount. Количество строк - это количество указателей в массиве, на который указывает указатель table. И кроме того, количество строк - это количество элементов в динамическом массиве, на который указывает указатель rows. Поэтому вначале необходимо для всех этих массивов выделить память:
1
2
table = calloc(rowscount, sizeof(int*));
rows = malloc(sizeof(int)*rowscount);
Далее в цикле осуществляется ввод количества столбцов для каждый строки. Введенное значение попадает в массив rows. И в соответствии с введенным значением для каждой строки выделяется необходимый размер памяти:
1
2
scanf("%d", &rows[i]);
table[i] = calloc(rows[i], sizeof(int));
Затем производится ввод элементов для каждой строки.
В конце работы программы при выводе происходит освобождение памяти. В программе память выделяется для строк таблицы, поэтому эту память надо освободить:
1
free(table[i]);
И кроме того, освобождается память, выделенная для указателей table и rows:
1
2
free(table);
free(rows);
Консольный вывод программы:
Rows count=2
Columns count for 1=3
table[0][0]=1
table[0][1]=2
table[0][2]=3
Columns count for 2=2
table[1][0]=4
table[1][1]=5
1 2 3
4 5