Память стековая и динамически распределяемая
Если вы помните, на занятии 5 приводились условное разделение памяти на пять областей:
• область глобальных переменных;
• свободная, или динамически распределяемая память;
• регистровая память (регистры);
• сегменты программы;
• стековая память.
Локальные переменные и параметры функций размещаются в стековой памяти. Программный код хранится в сегментах, глобальные переменные — в области глобальных переменных. Регистровая память предназначена для хранения внутренних служебных данных программы, таких как адрес вершины стека или адрес команды. Остальная часть памяти составляет так называемую свободную память — область памяти, динамически распределяемую между различными объектами.
Особенностью локальных переменных является то, что после выхода из функции, в которой они были объявлены, память, выделенная для их хранения, освобождается, а значения переменных уничтожаются.
Глобальные переменные позволяют частично решить эту проблему ценой неограниченного доступа к ним из любой точки программы, что значительно усложняет восприятие текста программы. Использование динамической памяти полностью решает обе проблемы.
Чтобы понять, что же такое динамическая память, попытайтесь представить область памяти, разделенную на множество пронумерованных ячеек, в которых записана информация. В отличие от стека переменных, ячейкам свободной памяти нельзя присвоить имя. Доступ к ним осуществляется посредством указателя, хранящего адрес нужной ячейки.
Чтобы лучше понять изложенное выше, рассмотрим пример. Допустим, вам дали номер телефона службы заказов товара по почте. Придя домой, вы занесли этот номер в память вашего телефона, а листок бумаги, на котором он был записан, выбросили. Нажимая на кнопку телефона, вы соединяетесь со службой заказа. Для вас не имеет значения номер и адрес этой службы, поскольку вы уже получили доступ к интересующей вас информации. Служба заказов в данном случае является моделью динамической памяти. Вы не знаете, где именно находится нужная вам информация, но знаете, как ее получить. Для обращения к значению используется его адрес, роль которого играет телефонный номер. Причем помнить адрес (или номер) не обязательно — достаточно лишь записать его значение в указатель (или телефон). После этого, используя указатель, можно извлечь нужное значение, даже не зная место его расположения.
Что касается стека переменных, то по завершении работы функции он очищается. В результате все локальные переменные оказываются вне области видимости и их значения уничтожаются. В отличие от стека, динамическая память не очищается до завершения работы программы, поэтому в таком случае освобождением памяти должен заниматься программист.
Важным преимуществом динамической памяти является то, что выделенная в ней облаять памяти не может использоваться до тех пор, пока явно не будет освобождена. Поэтому, если память в динамической области выделяется во время работы функции, ее можно будет использовать даже после завершения работы.
Еще одним преимуществом динамического выделения памяти перед использованием глобальных переменных является то, что доступ к данным можно получить только из функций, в которых есть доступ к указателю, хранящему нужный адрес. Такой способ доступа позволяет жестко контролировать характер манипулирования данными, а также избегать нежелательного или случайного их изменения.
Для работы с данными описанным способом прежде всего нужно создать указатель на ячейки динамической области памяти. О том, как это сделать, читайте в следующем разделе.
Оператор new
Для выделения памяти в области динамического распределения используется ключевое слово new. После new следует указать тип объекта, который будет размещаться в памяти. Это необходимо для определения размера области памяти, требуемой для хранения объекта. Написав, например, new unsigned short int, мы выделим два байта памяти, а строка new long динамически выделит четыре байта.
В качестве результата оператор new возвращает адрес выделенного фрагмента памяти. Этот адрес должен присваиваться указателю. Например, для выделения памяти в области динамического обмена переменной типа unsigned short можно использовать такую запись:
unsigned short int * pPointer; pPointer = new unsigned short int;
Или выполнить те же действия, но в одной сороке:
unsigned short int * pPointer = new unsigned short int;
В каждом случае указатель pPointer будет указывать на ячейку памяти в области динамического обмена, содержащую значение типа unsigned short. Теперь pPointer можно использовать как любой другой указатель на переменную этого типа. Чтобы занести в выделенную область памяти какое-нибудь значение, напишите такую строку:
*pPointer = 72;
Эта строка означает следующее: "записать число 72 в память по адресу, хранящемуся в pPointer".
Ввиду того что память является ограниченным ресурсом, попытка выделения памяти оператором new может оказаться неудачной. В этом случае возникнет исключительная ситуация, которая рассматривается на занятии 20.
Оператор delete
Когда память, выделенная под переменную, больше не нужна, ее следует освободить. Делается это с помощью оператора delete, после которого записывается имя указателя. Оператор delete освобождает область памяти, определенную указателем. Необходимо помнить, что указатель, в отличие от области памяти, на которую он указывает, является локальной переменной. Поэтому после выхода из функции, в которой он был объявлен, этот указатель станет недоступным. Однако область памяти, выделенная оператором new, на которую сослался указатель, при этом не освобождается. В результате часть памяти окажется недоступной. Программисты называют такую ситуацию утечкой памяти. Такое название полностью соответствует действительности, поскольку до завершения работы программы эту память использовать нельзя, она как бы "вытекает" из вашего компьютера.
Чтобы освободить выделенную память, используйте ключевое слово delete, например:
delete pPointer;
На самом деле при этом происходит не удаление указателя, а освобождение области памяти по адресу, записанному в нем. При освобождении выделенной памяти с самим указателем ничего не происходит и ему можно присвоить другой адрес. Листинг 8.4 показывает, как выделить память для динамической переменной, использовать ее, а затем освободить.
Предупреждение: Когда оператор delete применяется к указателю, происходит освобождение области динамической памяти, на которую этот указатель ссылается. Повторное применение оператора delete к этому же указателю приведет к зависанию программы. Рекомендуется при освобождении области динамической памяти присваивать связанному с ней указателю нулевое значение. Вызов оператора delete для нулевого указателя пройдет совершенно безболезненно для программы, например:
Animal *pDog = new Animal;
delete pDog; // освобождение динамической памяти
pDog = 0 // присвоение указателю нулевого значения
// ...
delete pDog; // бессмысленная, но совершенно безвредная строка
Листинг 8.4. Выделение, использование и освобождение динамической памяти
1; // Листинг 8, 4,
2; // Выделение, использование и освобождение динамической памяти 3;
4: #include <iostream.h>
5: int main()
6: {
7: int localVariable = 5;
8: int * pLocal= &localVariable;
9: int * pHeap = new int;
10: рНеар = 7;
11: cout << "localVariable: " << localVariable << "\n";
12: cout << "*pLocal: " << *pLocal << "\n";
13: cout << "*pHeap; " << *pHeap << "\n";
14: delete рНеар;
15: рНеар = new int;
16: *pHeap = 9;
17: cout << "*pHeap: " << *pHeap << "\n";
18: delete рНеар;
19: return 0;
20: }
Результат:
localVariable: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9
Анализ: В строке 7 объявляется и инициализируется локальная переменная localVariable. Затем объявляется указатель, которому присваивается адрес этой переменной (строка 8). В строке 9 выделяется память для переменной типа int и адрес выделенной области помещается в указатель рНеар. Записав по адресу, содержащемуся в рНеар, значение 7, можно удостовериться в том, что память была выделена корректно (строка 10). Если бы память под переменную не была выделена, то при выполнении этой строки появилось бы сообщение об ошибке.
Чтобы не перегружать примеры излишней информацией, мы опускаем всякого рода проверки. Однако во избежание аварийного завершения программы при решении реальных задач такой контроль обязательно должен выполняться.
В строке 10, после выделения памяти, по адресу в указателе записывается значение 7. Затем в строках 11 и 12 значения локальной переменной и указателя pLocal выводятся на экран. Вполне понятно, почему эти значения равны. Далее, в строке 13, выводится значение, записанное по адресу, хранящемуся в указателе рНеар. Таким образом, подтверждается, что значение, присвоенное в строке 10, действительно доступно для использования.
Освобождение области динамической памяти, выделенной в строке 9, осуществляется оператором delete в строке 14. Освобожденная память становится доступной для дальнейшего использования, и ее связь с указателем разрывается. После этого указатель рНеар может использоваться для хранения нового адреса. В строках 15 и 16 выполняется повторное выделение памяти и запись значения по соответствующему адресу. Затем в строке 17 это значение выводится на экран, после чего память освобождается.
Вообще говоря, строка 18 не является обязательной, так как после завершения работы программы вся выделенная в ней память автоматически освобождается. Однако явное освобождение памяти считается как бы правилом хорошего тона в программировании. Кроме того, это может оказаться полезным при редактировании программы.
Что такое утечка памяти
При невнимательной работе с указателями может возникнуть эффект так называемой утечки памяти. Это происходит, если указателю присваивается новое значение, а память, на которую он ссылался, не освобождается. Ниже показан пример такой ситуации.
1: unsigned short int * pPointer = new unsigned short int;
2: *pPointer = 72;
3: delete pPointer;
4: pPointer = new unsigned short int;
5: *pPointer = 84;
В строке 1 объявляется указатель и выделяется память для хранения переменной типа unsigned short int. В следующей строке в выделенную область записывается значение 72. Затем в строке 3 указателю присваивается адрес другой области памяти, в которую записывается число 84 (строка 4). После выполнения таких операций память, содержащая значение 72, оказывается недоступной, поскольку указателю на эту область было присвоено новое значение. В результате невозможно ни использовать, ни освободить зарезервированную память до завершения программы. Правильнее было бы написать следующее:
1: unsigned short int * pPointer = new unsigned short int;
2: *pPointer = 72;
3: pPointer = new unsigned short int;
4: *pPointer = 84;
В этом случае память, выделенная под переменную, корректно освобождается (строка 3).
Примечание: Каждый раз, когда в программе используется оператор new, за ним должен следовать оператор delete. Очень важно следить, какой указатель ссылается на выделенную область динамической памяти, и вовремя освобождать ее.