До сих пор рассматривались случаи, когда информация в программе хранится в переменных и массивах, размер которых известен заранее (на этапе компиляции). Память под такие переменные выделяется заранее при сборке исполнимого файла программы. Если же размер объекта заранее неизвестен, то память нужно выделять непосредственно во время работы программы.
Типичный пример – массив, размер которого заранее неизвестен и должен быть задан пользователем. Одно из плохих решений такой проблемы – объявить массив большого размера (100 или 1000) в надежде, что пользователю не потребуется большее количество элементов. При этом значительная часть массива в большинстве случаев просто не будет использована, что повлечет неиспользование выделенной программе памяти. В таких случаях очень удобно выделять память динамически, во время работы программы.
Для выделения памяти в C++ используется операция new. Её синтаксис выглядит следующим образом:
переменная-указатель = new тип_данных(инициализатор);
Тип данных, на который ссылается указатель в левой части должен совпадать с типом, следующим за ключевым словом new.
Инициализатор – это необязательное инициализирующее выражение, которое может использоваться для всех типов, кроме массивов.
Операция new выделяет непрерывную область память, достаточную для хранения значения указанного типа и возвращает указатель на эту область. Память выделяется из так называемой кучи (heap). Это область памяти, которая выделена программе для хранения в ней динамически создаваемых объектов. Объем такой памяти ограничен и если программа при использовании операции new не сможет найти нужный объем свободной памяти, то она сгенерирует ошибку.
Пример использования new:
int *ip;
ip = new int;
*ip = 128;
Здесь операция new выделяет в куче 4 последовательных байта (размер int) и возвращает адрес первого из них, который записывается в указатель ip. После этого можно работать со значением, на которое указывает ip.
Предостережение! Если попытаться использовать значение, на которое ссылается указатель до выделения памяти, т.е. выполнить третью строку примера перед второй, то это может повлечь разрушение данных программы. Т.к. указатель ip не инициализирован адресом выделенной памяти, то в нём находится совершенно произвольное значение. И если попытаться получить доступ по этому адресу – последствия непредсказуемы.
Динамически выделенная память, после того, как с ней закончена работа, должна быть явно освобождена операцией delete.
delete переменная-указатель;
Операция delete освобождает область памяти, на которую ссылается переменная-указатель. При этом память возвращается в кучу и может быть снова выделена для других целей. Попытка обращения к значению, на которое ссылается указатель, после освобождения, приведёт к непредсказуемым последствиям. Поэтому довольно часто при использовании операции delete в переменную-указатель сразу же записывается нулевой указатель. Например:
int *ip = new int;
// работа с памятью, выделенной для ip
delete ip;
ip = NULL;
Кроме того, каждый раз при использовании ip проверяется, что он не содержит нулевой указатель, т.е. он не был освобожден. Делается это очень просто:
if (ip) // в ip – значение, отличное от нуля
cout << *ip;
Предостережение! Любая память, выделенная операцией new, должна быть обязательно освобождена с помощью delete. Иначе возникают так называемые утечки памяти. Одну из подобных ситуаций демонстрирует следующий пример.
int *ip = new int;
int i = 0;
ip = &i;
Здесь в ip записывается указатель на объект типа int, память под который выделяется динамически. А затем этот указатель перезаписывается другим адресом – ссылкой на значение переменной i. Указатель на динамически выделенную память теряется. Она будет считаться занятой, но использовать её нельзя и освободить – тоже. Если в программе интенсивно используется работа с динамической памятью и есть её утечки, то при активной работе программы в течение длительного времени она может занять всю свободную память системы и вызвать её отказ.