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

Этот раздел является переводом туториала C++ Language


   В предыдущих разделах  вся необходимая память определялась перед выполнением программы путем определения необходимых переменных. Но бывают случаи, когда потребности в памяти  могут быть определены только во время выполнения программы. Например, когда количество памяти зависит от ввода пользователя. В таких случаях, программа должна выделять память динамически, для чего в языке C++ имеются операторы new и delete.

Операторы new и new[ ]


   Динамическая память выделяется оператором new, за которым следует спецификатор типа данных и, если требуется последовательность из более, чем одного элемента, их количество в квадратных скобках [ ]. Он возвращает указатель на начало блока выделенной памяти. Его синтаксис:

pointer = new type
pointer = new type [число_элементов]

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

int * foo;
foo = new int [5];

   В этом случае система динамически выделяет пространство для пяти элементов типа int и возвращает указатель на первый элемент последовательности, который присваивается переменной foo (указатель). Следовательно, foo сейчас указывает на валидный блок памяти с пространством для пяти элементов типа int.

 

   Здесь foo - это указатель, и таким образом, доступ к первому элементу foo можно получить либо выражением foo[0], либо выражением *foo ( они равнозначны). Доступ ко второму элементу можно получить с помощью foo[1] или *( foo+1), и так далее...

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

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

C++ предоставляет два стандартных механизма для проверки успешности выделения памяти:

   Первый - это обработка исключений. Если память выделить не удалось, выбрасывается исключение типа bad_alloc. Если не перехватить исключение, приложение завершится с ошибкой. Этот метод используется по умолчанию для new и он используется для выражений типа:

foo = new int [5];  // если выделение памяти не удалось, выбрасывается исключение

Другой метод известен как nothrow. В этом случае вместо выбрасывания исключения или выхода из программы, new возвращает null pointer, и программа продолжает нормально выполняться (на практике оказывается, что исключение все равно может выбрасываться - прим. переводчика).

   Этот метод может быть описан путем использования специального объекта nothrow, определенного в заголовочном файле <new>, как аргумента для new:

int * foo;
foo = new (nothrow) int [5];
if (foo == nullptr) {
  // Ошибка выделения памяти. Выполнение соответствтующих действий.
}

   Этот метод обычно создает менее эффективный код, чем перехват исключений, потому что предусматривает явную проверку  значения указателя после каждого выделения памяти. Следовательно, механизм исключений обычно предпочтительнее хотя бы для критических участков кода. Большинство примеров кода будут использовать механизм nothrow ввиду его простоты.

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

#include <iostream>
using namespace std;
class A{ public: A(int num){ cout << "ctor: " << num << endl; data = new int(num); } ~A(){ cout << "dtor: " << *data << endl; delete data; } private: int* data; }; int main() { // Выделение памяти размером sizeof(A) байт char* ptr = new char[sizeof(A)]; A* pobj = new(ptr) A(1); // размещение объекта 1 по адресу ptr // работа с pobj pobj->~A(); //удаление pobj A* pobj2 = new(ptr) A(2); // размещение объекта 2 по адресу ptr // работа с pobj2 pobj2->~A(); // удаление pobj2 delete[] ptr; // освобождение памяти return 0; }

   В этом примере выделяется память по адресу ptr для хранения одного объекта типа A, затем, при помощи специального синтаксиса оператора new, объект pobj типа A размещается по адресу ptr. Объекты, размещенные в памяти подобным образом, требуют уничтожения путем явного вызова деструктора объекта. После того, как первый объект уничтожен, выделенная память все еще доступна для дальнейшего использования. Далее таким же образом и по тому же адресу размещается объект pobj2, после чего он также уничтожается аналогичным образом. Перед выходом из программы, выделенная память освобождается вызовом оператора delete[] (см. следующий параграф). Можно заметить, что память выделяется и освобождается единожды, однако имеется возможность повторного использования участка памяти разными объектами, что подтверждается выводом этой программы:

ctor: 1
dtor: 1
ctor: 2
dtor: 2

Операторы delete и delete[ ]


   В большинстве случаев, динамически выделенная память нужна в течение определенного периода времени. В тот момент, когда память больше не нужна, она может быть освобождена  для того, чтобы она стала доступной для новых запросов. Для этой цели существует оператор delete[ ]:

delete pointer;
delete[] pointer;

   Первое выражение освобождает память, выделенную при мощи new для одного элемента. Второе выражение освобождает память, выделенную для массива элементов с использованием new[ ].

Значение, переданное delete как аргумент, должно быть любым указателем на блок памяти, выделенной с помощью new, или null pointer (в этом случае delete не делает ничего).

// rememb-o-matic
#include <iostream>
#include <new>
using namespace std;

int main ()
{
  int i,n;
  int * p;
  cout << "Сколько чисел Вы хотите ввести? ";
  cin >> i;
  p= new (nothrow) int[i];
  if (p == nullptr)
    cout << "Ошибка: память не может быть выделена";
  else
  {
    for (n=0; n<i; n++)
    {
      cout << "Введите число: ";
      cin >> p[n];
    }
    cout << "Вы ввели: ";
    for (n=0; n<i; n++)
      cout << p[n] << ", ";
    delete[] p;
  }
  return 0;
}
Сколько чисел Вы хотите ввести? 5   
Введите число : 75
Введите число : 436
Введите число : 1067
Введите число : 8
Введите число : 32
Вы ввели: 75, 436, 1067, 8, 32,

   Заметьте, что значение в скобках оператора new это переменное значение, введенное пользователем( i), а не константное выражение:

p= new (nothrow) int[i];

   Всегда существует возможность того, что пользователь введет значение для i настолько большое, что система не сможет выделить достаточно памяти. Например, при попытке выделить память для биллиона элементов, система может не выделить достаточно памяти.

   Считается хорошей практикой всегда обрабатывать ошибки при выделении памяти обработкой соответствующего исключения или проверкой значения указателя.

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


   C++ включает операторы new и delete для динамического выделения памяти, однако это не относится к языку C. Вместо них используются библиотечные функции malloc, calloc, realloc и free, определенные в <cstdlib> или <stdlib.h> в языке C. Эти функции также доступны и в C++ и могут быть использованы для выделения и освобождения памяти.

Однако блоки памяти, выделенные этими функциями, не обязательно совместимы с теми, которые возвращает оператор new, поэтому не нужно их смешивать; каждый из них должен быть обработан собственным набором функций или операторов.