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

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

1.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); int *p = (int *)malloc(sizeof(int)); // С помощью операции sizeof определяем размер переменной типа int if (!p) { printf("Not enough memory\n"); return -1; } ... free(p); int *p = nullptr; int n = 10; p = (int *)realloc(p, sizeof(int) * n); // Поскольку p есть nullptr, просто выделяем память if (!p) { printf("Not enough memory\n"); return -1; } ... n = 20; p = (int *)realloc(p, sizeof(int) * n); // Меняем размер ранее выделенного участка памяти if (!p) { printf("Not enough memory\n"); return -1; } ... free(p);

1.2. Операции new и delete

В языке C++ были введены новые операции для выделения и освобождения динамической памяти: new и delete. Операция new пытается выделить достаточно памяти в куче для размещения новых данных и в случае успеха возвращает адрес выделенного участка памяти. Для выделения памяти необходимо указать нужный тип и, при необходимости выделить память под массив, размер массива в квадратных скобках. Память, выделенная при помощи операции new, должна быть освобождена при помощи операции delete. Память, выделенная под массив, должна освобождаться при помощи delete[]. int *p; p = new int; // Выделение памяти для одной переменной типа int delete p; // Освобождение выделенной памяти p = new int [10]; // Выделение памяти для массива из 10 элементов типа int delete [] p; // Освобождение выделенной памяти

При выделении памяти под переменную можно использовать прямую или универсальную инициализацию. При выделении памяти под массив имеет смысл использовать универсальную инициализацию для инициализации элементов массива значениями по умолчанию. Указывать какие-то конкретные значения нет смысла, т.к. неизвестно количество элементов массива. При выделении памяти под объекты класса вызывается конструктор умолчания. Если класс не имеет конструктора умолчания, то выделение памяти невозможно. При освобождении памяти вызывается деструктор. int *p1 = new int (5); // Прямая инициализация int *p2 = new int {5}; // Универсальная инициализация int *p3 = new int [10] {}; // Универсальная инициализация элементов массива значениями по умолчанию

При использовании операций new и delete также необходимо проверять, была ли выделена память. Обычно в случае ошибки операция new генерирует исключение bad_alloc, но можно изменить её поведение так, чтобы в случае ошибки возвращалось значение 0. #include <new> // Для использования bad_alloc и nothrow int *p; try { p = new int; } catch (std::bad_alloc) { printf("Not enough memory\n"); return -1; } ... delete p; p = new(std::nothrow) int[10]; // Теперь исключение не генерируется if (!p) { printf("Not enough memory\n"); return -1; } ... delete [] p;

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

1.3. Операция new с размещением

Выделение памяти из кучи с помощью операции new постепенно сводит на нет преимущества языка C++ в скорости. Чем интенсивнее мы пользуемся операцией new, тем сложнее становится приложению, поскольку память кончается, фрагментируется и всячески стремится утекать. Эта участь уже постигла удобные, но неявно опасные для производительности контейнеры STL: vector, string, deque, map. Операция new позволяет указать готовую область памяти для размещения создаваемых объектов. Для этого после ключевого слова new надо указать адрес буфера, где будут размещаться объекты. Однако, в этом случае размер памяти для размещения объектов будет ограничен размером буфера. Ещё одна проблема – поскольку после размещения операция delete не используется, то деструкторы автоматически не вызывают, надо делать это явно. const int size = 1024; char buffer[size]; int *pi = new (buffer) int [size / sizeof(int)]; double *pd = new (buffer) double [size / sizeof(double)]; X *px = new (buffer) X [size / sizeof(X)]; for (int i = 0; i < size / sizeof(X); i++) px[i].~X(); // Явный вызов деструктора

2. Примеры

Пример 1. Обработка одномерного массива

#pragma once #define _CRT_SECURE_NO_WARNINGS #include <cstdio> #include <malloc.h> #include <locale.h> enum {OK, BAD_ALLOC, FILE_NOT_OPENED}; int Input(double **x, int *n, char *fname); double Sum(double *x, int n); double Product(double *x, int n); int main(int argc, char* argv[]) { double *a; int n; setlocale(LC_ALL, "rus"); if (argc < 2) { printf("Недостаточно параметров!"); return 1; } int res = Input(&a, &n, argv[1]); if (res == BAD_ALLOC) { printf("Недостаточно памяти\n"); return -1; } if (res == FILE_NOT_OPENED) { printf("Невозмножно открыть файл\n"); return -1; } printf("Сумма = %lf\nПроизведение = %lf\n", Sum(a, n), Product(a, n)); free(a); // Освобождение выделенной памяти } int Input(double **x, int *n, char *fname) { FILE *f; if ((f = fopen(fname, "r")) == NULL) return FILE_NOT_OPENED; fscanf(f, "%d", n); // Используем один из двух вариантов выделения памяти *x = (double *)malloc(*n * sizeof(double)); *x = (double *)calloc(*n, sizeof(double)); if (!*x) return BAD_ALLOC; for (int i = 0; i < *n; i++) fscanf(f, "%lf", *x + i); fclose(f); return OK; } double Sum(double *x, int n) { double s = 0; // Обычный способ обработки массива for (int i = 0; i < n; i++) s += x[i]; return s; } double Product(double *x, int n) { double pr = 1; // Использование указателя для доступа к элементам массива for (double *p = x; p < x + n; p++) pr *= *p; return pr; }

Пример 2. Обработка двумерного массива (1 способ)

#pragma once #include <fstream> #include <iostream> #include <new> using namespace std; void Input(double **x, int *m, int *n, char *fname); double Max(double *x, int m, int n); int main(int argc, char * argv[]) { double *a; int m, n; setlocale(LC_ALL, "rus"); if (argc < 2) { cout << "Недостаточно параметров!"; return 1; } try { Input(&a, &m, &n, argv[1]); } catch (bad_alloc) { cout << "Недостаточно памяти\n"; return -1; } catch (exception e) { cout << e.what() << '\n'; return -1; } cout << Max(a, m, n) << '\n'; delete [] a; } void Input(double **x, int *m, int *n, char *fname) { ifstream f(fname); if (!f.is_open()) throw exception("Невозможно открыть файл"); f >> *m >> *n; *x = new double[*m * *n]; for (int i = 0; i < *m; i++) for (int j = 0; j < *n; j++) f >> (*x)[i * *n + j]; f.close(); } double Max(double *x, int m, int n) { double max = *x; for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (x[i * n + j] > max) max = x[i * n + j]; return max; }

Пример 3. Обработка двумерного массива (2 способ)

#pragma once #include <fstream> #include <iostream> #include <new> using namespace std; double** Input(int &m, int &n, char *fname); double Max(double **x, int m, int n); int main(int argc, char * argv[]) { double **a; int m, n; setlocale(LC_ALL, "rus"); if (argc < 2) { cout << "Недостаточно параметров!"; return 1; } try { a = Input(m, n, argv[1]); } catch (bad_alloc) { cout << "Недостаточно памяти\n"; return -1; } catch (exception e) { cout << e.what() << '\n'; return -1; } cout << Max(a, m, n) << '\n'; for (int i = 0; i < m; i++) delete [] a[i]; delete [] a; } double** Input(int &m, int &n, char *fname) { ifstream f(fname); double **x; if (!f.is_open()) throw exception("Невозможно открыть файл"); f >> m >> n; x = new double* [m]; for (int i = 0; i < m; i++) x[i] = new double [n]; for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) f >> x[i][j]; f.close(); return x; } double Max(double **x, int m, int n) { double max = x[0][0]; for (int i = 0; i < m; i++) for (int j = 0; j < n; j++) if (x[i][j] > max) max = x[i][j]; return max; }