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

Как вы уже знаете, при объявлении переменной необходимо указать тип данных, а для массива дополнительно задать точное количество элементов. На основе этой информации при запуске программы автоматически выделяется необходимый объем памяти. После завершения программы память автоматически освобождается. Иными словами, объем памяти необходимо знать до выполнения программы. Во время выполнения программы создать новую переменную или увеличить размер существующего массива нельзя.

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

Выделение памяти под один объект

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

<Указатель> = new <Тип данных>(<Начальное значение>);

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

int *p = new int;     // Без начального значения
int *p = new int(10); // С начальным значением

При выделении памяти может возникнуть ситуация нехватки памяти. В случае ошибки оператор new возбуждает исключение bad_alloc (класс исключения объявлен в файле new). Обработать это исключение можно с помощью конструкции try...catch. Пример выделения памяти с обработкой исключения:

#include <new>
// ... Фрагмент опущен ...
int *p = nullptr; // Создаем указатель
try {
   p = new int;   // Выделяем память
}
catch (std::bad_alloc &ex) {
   // Обработка исключения
}

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

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

Возвратить ранее выделенную память операционной системе позволяет оператор delete. Оператор имеет следующий формат:

delete <Указатель>;

После использования оператора delete указатель по-прежнему будет содержать прежний адрес. Поэтому после использования оператора delete указатель принято обнулять. Пример выделения памяти под один объект приведен в листинге 3.14.

Листинг 3.14. Динамическое выделение памяти под один объект

#include <iostream>
#include <new>
#include <clocale>
#include <process.h>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
   int *p = nullptr;             // Создаем указатель
   try {
      p = new int;               // Выделяем память
   }
   catch (std::bad_alloc &ex) {
      std::cerr << "Не удалось выделить память" << std::endl;
      exit(1);                   // Выходим при ошибке
   }
   *p = 20;                      // Пользуемся памятью
   std::cout << *p << std::endl; // 20
   delete p;                     // Освобождаем память
   p = nullptr;                  // Обнуляем указатель
   return 0;
}

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

Выделение памяти под массив производится следующим образом:

<Указатель> = new <Тип данных>[<Количество элементов>];

Освободить выделенную память можно так:

delete [] <Указатель>;

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

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

#include <iostream>
#include <new>
#include <clocale>
#include <process.h>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
   const int ARR_SIZE = 3;      // Размер массива
   int *p = nullptr;            // Создаем указатель
   try {
      p = new int[ARR_SIZE];    // Выделяем память
   }
   catch (std::bad_alloc &ex) {
      std::cerr << "Не удалось выделить память" << std::endl;
      exit(1);                  // Выходим при ошибке
   }
   // Нумеруем элементы массива от 1 до 3
   for (int i = 0; i < ARR_SIZE; ++i) { // Пользуемся памятью
      p[i] = i + 1;
   }
   // Выводим значения
   for (int i = 0; i < ARR_SIZE; ++i) {
      std::cout << p[i] << std::endl;
   }
   delete [] p;                 // Освобождаем память
   p = nullptr;                 // Обнуляем указатель
   return 0;
}

Выделение памяти без возбуждения исключения

В ранних версиях C++ при нехватке памяти возвращался нулевой указатель. Такая же ситуация возникает в языке C при использовании функции malloc(). Чтобы оператор new, возвращал нулевой указатель, а не возбуждал исключение используется следующий синтаксис:

<Указатель> = new (std::nothrow) <Тип данных>(<Начальное значение>);
<Указатель> = new (std::nothrow) <Тип данных>[<Количество элементов>];

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

Листинг 3.16. Динамическое выделение памяти без возбуждения исключения

#include <iostream>
#include <new>
#include <clocale>
#include <process.h>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
   int *p = new (std::nothrow) int; // Выделяем память
   if (!p) {                        // Проверяем на корректность
      std::cerr << "Не удалось выделить память" << std::endl;
      exit(1);                      // Выходим при ошибке
   }
   *p = 20;                         // Пользуемся памятью
   std::cout << *p << std::endl;    // 20
   delete p;                        // Освобождаем память
   p = nullptr;                     // Обнуляем указатель
   return 0;
}

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

Язык C++ поддерживает также функции malloc(), calloc(), realloc() и free() , позволяющие управлять динамической памятью в языке C. Хотя эти функции можно использовать и в языке C++, тем не менее стоит отдать предпочтение оператору new и явной обработке исключения. Описание этих функций приведено в книге лишь для того, чтобы вы могли разобраться в чужом коде.

Функции malloc() и free()

Для выделения динамической памяти в языке C предназначена функция malloc(). Прототип функции:

#include <cstdlib> /* или #include <stdlib.h> */
void *malloc(size_t size);

Функция malloc() принимает в качестве параметра размер памяти в байтах и возвращает указатель, имеющий тип void *. Если память выделить не удалось, то функция возвращает нулевой указатель. Все элементы будут иметь произвольное значение, так называемый «мусор». В языке C указатель типа void * неявно приводится к другому типу, поэтому использовать явное приведение не нужно. В языке C++ перед присвоением значения указателю необходимо выполнить явное приведение к используемому типу. Кроме того, чтобы программа была машинонезависимой следует применять оператор sizeof для вычисления размера памяти, требуемого для определенного типа.

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

#include <cstdlib> /* или #include <stdlib.h> */
void free(void *memory);

Пример выделения памяти для одного объекта приведен в листинге 3.17.

Листинг 3.17. Динамическое выделение памяти для одного объекта

#include <iostream>
#include <cstdlib>
#include <clocale>
#include <process.h>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
    // Выделяем память
   int *p = (int *) std::malloc(sizeof(int));
   if (!p) {                     // Проверяем на корректность
      std::cerr << "Не удалось выделить память" << std::endl;
      exit(1);                   // Выходим при ошибке
   }
   *p = 20;                      // Пользуемся памятью
   std::cout << *p << std::endl; // 20
   std::free(p);                 // Освобождаем память
   p = nullptr;                  // Обнуляем указатель
   return 0;
}

Пример выделения памяти под массив приведен в листинге 3.18.

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

#include <iostream>
#include <cstdlib>
#include <clocale>
#include <process.h>

int main() {
   std::setlocale(LC_ALL, "Russian_Russia.1251");
   const int ARR_SIZE = 10;
   int *p = (int *) std::malloc(ARR_SIZE * sizeof(int));
   if (!p) {                     // Проверяем на корректность
      std::cerr << "Не удалось выделить память" << std::endl;
      exit(1);                   // Выходим при ошибке
   }
   // Нумеруем элементы массива от 1 до 10
   for (int i = 0; i < ARR_SIZE; ++i) { // Пользуемся памятью
      p[i] = i + 1;
   }
   // Выводим значения
   for (int i = 0; i < ARR_SIZE; ++i) {
      std::cout << p[i] << std::endl;
   }
   std::free(p);                        // Освобождаем память
   p = nullptr;                         // Обнуляем указатель
   return 0;
}

Функция calloc()

Вместо функции malloc() можно воспользоваться функцией calloc(). Прототип функции:

#include <cstdlib> /* или #include <stdlib.h> */
void *calloc(size_t count, size_t elem_size);

В первом параметре функция calloc() принимает количество элементов, а во втором — размер одного элемента. Если память выделить не удалось, то функция возвращает нулевой указатель. Все элементы будут иметь значение 0.

Используя функцию calloc(), следующую инструкцию из листинга 3.18:

int *p = (int *) std::malloc(ARR_SIZE * sizeof(int));

мы можем записать так:

int *p = (int *) std::calloc(ARR_SIZE, sizeof(int));

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

Листинг 3.19. Динамическое выделение памяти под двумерный массив

#include <iostream>
#include <cstdlib>
#include <process.h>

int main() {
   const int ROWS = 2;    // Количество строк
   const int COLUMNS = 4; // Количество столбцов
   int i = 0, j = 0;
   // Создаем массив указателей
   int **p = (int **) std::calloc(ROWS, sizeof(int*));
   if (!p) exit(1);                // Выходим при ошибке
   // Добавляем строки
   for (i = 0; i < ROWS; ++i) {
      p[i] = (int *) std::calloc(COLUMNS, sizeof(int));
      if (!p[i]) exit(1);          // Выходим при ошибке
   }
   // Нумеруем элементы массива
   int n = 1;
   for (i = 0; i < ROWS; ++i) {
      for (j = 0; j < COLUMNS; ++j) {
         p[i][j] = n++;
         // *(*(p + i) + j) = n++;
      }
   }
   // Выводим значения
   for (i = 0; i < ROWS; ++i) {
      for (j = 0; j < COLUMNS; ++j) {
         std::cout << p[i][j] << ' ';
         // std::cout << *(*(p + i) + j) << ' ';
      }
      std::cout << std::endl;
   }
   // Освобождаем память
   for (int i = 0; i < ROWS; ++i) {
      std::free(p[i]);
   }
   std::free(p);
   p = nullptr;                         // Обнуляем указатель
   return 0;
}

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

Так как мы сохраняем в массиве указателей лишь адрес строки, а не саму строку, количество элементов в строке может быть произвольным. Это обстоятельство позволяет создавать так называемые «зубчатые» двумерные массивы.

Строки в памяти могут быть расположены в разных местах, что не позволяет эффективно получать доступ к элементам двумерного массива. Чтобы доступ к элементам сделать максимально быстрым, можно представить двумерный массив в виде одномерного массива (листинг 3.20).

Листинг 3.20. Представление двумерного массива в виде одномерного

#include <iostream>
#include <cstdlib>
#include <process.h>

int main() {
   const int ROWS = 2;    // Количество строк
   const int COLUMNS = 4; // Количество столбцов
   int i = 0, j = 0;
   int *p = (int *) std::calloc(ROWS * COLUMNS, sizeof(int));
   if (!p) exit(1);                // Выходим при ошибке
   // Нумеруем элементы массива
   int n = 1;
   for (i = 0; i < ROWS; ++i) {
      for (j = 0; j < COLUMNS; ++j) {
         *(p + i * COLUMNS + j) = n++;
      }
   }
   // Выводим значения
   for (i = 0; i < ROWS; ++i) {
      for (j = 0; j < COLUMNS; ++j) {
         std::cout << *(p + i * COLUMNS + j) << ' ';
      }
      std::cout << std::endl;
   }
   std::free(p);                        // Освобождаем память
   p = nullptr;                         // Обнуляем указатель
   return 0;
}

Так как в этом случае все элементы двумерного массива расположены в смежных ячейках, мы можем получить доступ к элементам с помощью указателя и адресной арифметики. Например, пронумеруем все элементы:

int *p2 = p; // Сохраняем адрес первого элемента
int n = 1;
int count = ROWS * COLUMNS;
for (i = 0; i < count; ++i) {
   *p2 = n++;
   ++p2;
}

Функция realloc()

Функция realloc() выполняет перераспределение памяти. Прототип функции:

#include <cstdlib> /* или #include <stdlib.h> */
void *realloc(void *memory, size_t newSize);

В первом параметре функция realloc() принимает указатель на ранее выделенную динамическую память, а во втором — новый требуемый размер в байтах. Функция выделит динамическую память длиной newSize, скопирует в нее элементы из старой области памяти, освободит старую память и вернет указатель на новую область памяти. Новые элементы будут иметь произвольные значения, так называемый «мусор». Если новая длина меньше старой длины, то лишние элементы будут удалены. Если память не может быть выделена, то функция вернет нулевой указатель, при этом старая область памяти не изменяется (в этом случае возможны утечки памяти, если значение присваивается прежнему указателю).

Если в первом параметре указать значение NULL, то будет выделена динамическая память и функция вернет указатель на нее. Если во втором параметре указано значение 0, то ранее выделенная динамическая память освобождается и функция вернет нулевой указатель.

Пример использования функции realloc() приведен в листинге 3.21.

Листинг 3.21. Функция realloc()

#include <iostream>
#include <cstdlib>
#include <process.h>

int main() {
   int arr_size = 5;
   int *p = (int *) std::malloc((size_t)arr_size * sizeof(int));
   if (!p) exit(1);                // Выходим при ошибке
   // Нумеруем элементы массива
   for (int i = 0; i < arr_size; ++i) {
      p[i] = i + 1;
   }
   // Увеличиваем количество элементов
   arr_size += 2;
   p = (int *) std::realloc(p, (size_t)arr_size * sizeof(int));
   // Здесь возможна утечка памяти, если realloc() вернет NULL
   if (!p) exit(1);                // Выходим при ошибке
   p[5] = 55;
   p[6] = 66;
   // Выводим значения
   for (int i = 0; i < arr_size; ++i) {
      std::cout << p[i] << ' ';
   } // 1 2 3 4 5 55 66
   std::free(p);                        // Освобождаем память
   p = nullptr;                         // Обнуляем указатель
   return 0;
}

В языке C++ вместо функций malloc(), calloc() и realloc() лучше использовать класс vector, который реализует динамический массив. Следить за размерами динамического массива нет необходимости, т. к. управление динамической памятью осуществляется автоматически:

// #include <vector>
std::vector<int> arr;
// Добавляем элементы в массив
for (int i = 0; i < 5; ++i) arr.push_back(i + 1);
// Увеличиваем количество элементов
arr.push_back(55);
arr.push_back(66);
// Выводим значения
for (int item : arr) {
   std::cout << item << ' ';
} // 1 2 3 4 5 55 66

Учебник C++ (Qt Creator и MinGW)
Учебник C++ (Qt Creator и MinGW) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов