Лекция 7. Динамическое распределение памяти. Списки

1. Динамическое распределение памяти

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

Распределение памяти осуществляется функцией
void *malloc(size_t size);

Прототип функции находится в файлах <stdlib.h> и <malloc.h>.

Данная функция выделяет память, объём которой в байтах указывается как параметр функции. Функция возвращает адрес выделенной памяти или NULL, если память не может быть выделена. NULL – это специальная константа, которая представляет собой недействительный указатель.

Функция возвращает указатель на пустой тип void *, который может быть преобразован к любому типу и должен быть преобразован к нужному типу. Обязательно надо проверять результат, возвращаемый функцией!

Существуют также ещё две функции, которые позволяют сделать выделение памяти более гибким:
void *calloc(size_t num, size_t size); void *realloc(void *memblock, size_t size);

Первая функция выделяет память под массив из num элементов, каждый из которых имеет размер size. Элементы массива инициализируются нулями.

Вторая функция изменяет размер блока, выделенного ранее функциями malloc, calloc или realloc. Параметр memblock содержит адрес блока для изменения, а параметр size – необходимый размер блока. Положение блока в оперативной памяти может измениться, при этом содержимое будет скопировано.

После того, как отпала необходимость в выделенной памяти, надо освободить её:
void free(void *memblock);

p = (int *)malloc(sizeof(int)); // С помощью операции sizeof определяем размер переменной типа int
if (!p)
{ printf("Not enough memory\n"); return; }
...
free(p);

В языке С++ были введены новые операторы для выделения и освобождения динамической памяти: new и delete.

int *p;
p = new int; // Выделение памяти для одно переменной типа int
delete p;
// Освобождение выделенной памяти
p = new int [10]; // Выделение памяти для массива из 10 элементов типа int
delete [] p; // Освобождение выделенной памяти

При использовании операторов new и delete также необходимо проверять, была ли выделена память. Обычно в случае ошибки оператор new генерирует исключение bad_alloc, но можно изменить его поведение так, чтобы в случае ошибки возвращалось значение 0.

#include <new> int *p; try { p = new int; } catch (std::bad_alloc) { printf("Not enough memory\n"); return; } ... delete p;
// Для использования bad_alloc и nothrow
p = new(std::nothrow) int[10]; if (!p) { printf("Not enough memory\n"); return; } ... delete [] p; // Теперь исключение не генерируется

2. Списки

Однонаправленный список – это структура, каждый элемент которой содержит адрес следующего элемента. В последнем элементе в данный адрес записывается значение специальной константы NULL для обозначения конца списка. Адрес первого элемента нужно хранить в специальной переменной.

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

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

struct S { char str[21]; int a; struct S *next; }; struct S *begin, *cur, *prev;
// Добавление элементов в начало списка
cur = NULL; while (!feof(in)) { begin = (struct S *)malloc(sizeof(struct S)); if (begin == NULL) { printf("Insufficient memory\n"); fclose(in); fclose(out); return; } fscanf(in, "%s%d", begin->str, &(begin->a)); begin->next = cur; cur = begin; }
// Добавление элементов в конец списка
begin = (struct S *)malloc(sizeof(struct S)); if (begin == NULL) { printf("Insufficient memory\n"); fclose(in); fclose(out); return; } fscanf(in, "%s%d", begin->str, &(begin->a)); begin->next = NULL; prev = begin; while (!feof(in)) { cur = (struct S *)malloc(sizeof(struct S)); if (cur == NULL) { printf("Insufficient memory\n"); fclose(in); fclose(out); return; } fscanf(in, "%s%d", cur->str, &(cur->a)); cur->next = NULL; prev->next = cur; prev = cur; }

3. Примеры

Пример 1. Динамическое распределение массива. #include <stdio.h> #include <stdlib.h> void main(int argc, char *argv[]) { double *a, *p, *end; double s; int n, i; FILE *file; if (argc < 2) { printf("Недостаточно параметров!\n"); return; } if ((file = fopen(argv[1], "r")) == NULL) { printf("Невозможно открыть файл '%s'\n", argv[1]); return; } if (fscanf(file, "%d", &n) < 1) { printf ("Ошибка чтения из файла '%s'\n", argv[1]); fclose(file); return; } // Используем один из двух вариантов выделения памяти if ((a = (double *)malloc(n * sizeof(double))) == NULL) { printf("Недостаточно памяти!\n"); fclose(file); return; } if ((a = (double *)calloc(n, sizeof(double))) == NULL) { printf("Недостаточно памяти!\n"); fclose(file); return; } for (i = 0; i < n; i++) if (fscanf(file, "%lf", &a[i]) < 1) { printf ("Ошибка чтения из файла '%s'\n", argv[1]); fclose(file); return; } fclose(file); // Обычный способ обработки массива for (s = 0, i = 0; i < n; i++) s += a[i]; printf("Сумма равна %lf\n", s); // Использование указателя для доступа к элементам массива for (s = 1, p = a, end = p + n; p < end; p++) s *= *p; printf("\Произведение равно %lf\n", s); free(a); // Освобождение выделенной памяти }

Пример 2. Работа со списком (см. пример в лекции 6). #include <stdio.h> #include <conio.h> #include <malloc.h> #include <string.h> struct S { char str[21]; int a; struct S *next; // Можно объявить указатель на еще не объявленную структуру }; void main(int argc, char *argv[]) { struct S *begin, *cur, *prev; int a, check = 0; char str[21] = ""; FILE *in, *out; char ans; if (argc < 3) { printf("Too few arguments.\n"); return; } if ((in = fopen(argv[1], "r")) == NULL) { printf("It is impossible to open file '%s'.\n", argv[1]); return; } if ((out = fopen(argv[2], "w")) == NULL) { printf("It is impossible to open file '%s'.\n", argv[2]); fclose(in); return; } // Строим список, добавляя элементы в конец. Первый элемент вводим отдельно begin = (struct S *)malloc(sizeof(struct S)); if (begin == NULL) { printf("Insufficient memory\n"); fclose(in); fclose(out); return; } fscanf(in, "%s%d", begin->str, &(begin->a)); begin->next = NULL; prev = begin; while (!feof(in)) { cur = (struct S *)malloc(sizeof(struct S)); if (cur == NULL) { printf("Insufficient memory\n"); fclose(in); fclose(out); return; } fscanf(in, "%s%d", cur->str, &(cur->a)); cur->next = NULL; prev->next = cur; prev = cur; } fclose(in); printf("Use Str for search? "); ans = getche(); if (ans == 'y' || ans == 'Y') { printf("\nInput string for search: "); scanf("%s",str); } printf("\nUse A for search? "); ans = getche(); if (ans == 'y' || ans == 'Y') { check = 1; printf("\nInput A: "); scanf("%d", &a); } // Вывод элементов, удовлетворяющих условию for (cur = begin; cur; cur = cur->next) if ((!*str || strcmp(str, cur->str) == 0) && (!check || a == cur->a)) fprintf(out, "%-30s %3d\n", cur->str, cur->a); // Удаление элементов, не удовлетворяющих условию. Адрес предыдущего элемента понадобится при удалении for (prev = NULL, cur = begin; cur; ) if ((*str && strcmp(str, cur->str) != 0) || (check && a != cur->a)) if (prev == NULL) // Если нужно удалить первый элемент { begin = cur->next; free(cur); cur = begin; } else // Удаление не первого элемента { prev->next = cur->next; free(cur); cur = prev->next; } else // Если удалять не надо, просто меняем указатели { prev = cur; cur = cur->next; } for (cur = begin; cur; cur = cur->next) fprintf(out, "%-30s %3d\n", cur->str, cur->a); fclose(out); // Освобождение памяти, выделенной для списка for (cur = begin; cur; ) { prev = cur; cur = cur->next; free(prev); } begin = NULL; }