В предыдущей главе вы узнали о размещении данных в оперативной памяти и познакомились с указателями, хранящими адреса переменных. Это была присказка… Ведь самый жгучий вопрос остался без ответа – зачем нужны эти указатели?
В программах для сортировки таблицы чемпионата и обработки классного журнала был заранее известен объёмом данных. Действительно, количество клубов в чемпионате известно любому болельщику. Чуть сложнее с классным журналом, – ведь ученики приходят и уходят. Но, взяв размер массива учеников с некоторым разумным запасом, мы решаем и эту проблему.
Но так будет не всегда. Есть немало задач, где предугадать объём данных нельзя даже приблизительно. Вот, к примеру, полицейская база данных по угнанным автомобилям, каков должен быть её размер? Тысяча или миллион элементов? В спокойной стране достаточно будет десятка, а там, где угоняют каждый третий автомобиль… Ох! Лучше не спрашивайте! Можно, конечно, объявить массив с солидным запасом, но это породит ещё две проблемы. Во-первых, большая часть массива вероятней всего будет пустовать, – разумно ли транжирить память попусту? Второй случай ещё злее: в какой-то момент не хватит даже этого запаса, и программа «рухнет».
Безупречное решение – выделять данным ровно столько памяти, сколько нужно. То есть, создавать переменные, когда нам надо, и уничтожать их, когда потребность в них отпадает. Отличная мысль! Двинемся в этом направлении!
Вернитесь к рисунку 51-й главы, где показано размещение программ в оперативной памяти. Большая часть этой памяти остается «не вспаханной», свободной. Куча – так принято её называть (по-английски – Heap). Операционная система распоряжается кучей по своему усмотрению, и все же большие куски этой памяти простаивают без дела. Нельзя ли программе временно одолжить частичку? Оказывается, можно! Надо лишь освоить работу с указателями. В предыдущей главе мы применяли указатели на переменные, не видя в том особой пользы. Другое дело – участки памяти в куче, у которых нет имени. Здесь указатели – единственное средство для доступа к этим залежам.
Поскольку кучей заведует операционная система, за памятью обращаются к ней. Для этого в Паскале предусмотрено несколько процедур и функций, две из которых – процедуры New и Dispose – мы и рассмотрим.
Для получения кусочка памяти из кучи, вызывают процедуру New (что значит «новый»). Этой процедуре нужен лишь один параметр – указатель некоторого типа. Процедура бронирует в куче кусок соответствующего размера, и адрес этого куска помещает в указатель. Так рождается новая переменная в куче. Рассмотрим пример.
var York : ^Integer; { указатель на целое }
begin
New(York); { выделено два байта из кучи, адрес в переменной York }
York^:=123;
Writeln(York^); { 123 }
end.
Здесь объявлен указатель на целое число по имени York. При выполнении процедуры New(York) из кучи выделяется 2 байта (для целого числа), и адрес этого кусочка попадает в указатель York. Чтобы воспользоваться выделенным участком как обычной переменной, указатель разыменовывают.
Спрашивается: откуда операционная система узнает объём запрашиваемой памяти (в данном случае 2 байта)? Об этом ей тихонько сообщит компилятор, которому известны размеры всех типов данных.
Поработав с выделенным участком памяти, со временем вы можете отказаться от него – за ненадобностью. Тогда следует освободить его и вернуть в кучу, – как ни велика память, она не беспредельна, а брать взаймы ещё придется.
Освобождение кусочка памяти выполняется процедурой Dispose – «освободить». Ей, как и процедуре выделения памяти, нужен лишь один параметр – указатель на ранее выделенный участок памяти. Обратимся снова к примеру.
var ps : ^string; { указатель на строку }
begin
New(ps); { выделено 256 байтов из кучи }
ps^:=’Hello !’;
Writeln(ps^); { Hello ! }
Dispose(ps); { возвращено в кучу 256 байтов }
end.
Здесь мы получили из кучи 256 байтов под строковую переменную, – вполне приличный кусок. А после – за ненадобностью – освободили эту память.
Переменные, порождаемые и исчезающие по мановению волшебника-программиста, называют динамическими. Но исполняет повеления программиста операционная система, – только она вправе хозяйничать в куче. Получив запрос на выделение памяти через процедуру New, система сообщит программе адрес выделенного участка и отметит его у себя как занятый. Теперь никто не посягнет на него, пока программа не освободит участок процедурой Dispose, или не завершится. По завершении программы все занятые ею участки памяти освобождаются системой автоматически.
Доступ к динамическим переменным возможен лишь через указатели, а они размещаются в секции данных или в стеке. Такие переменные (в обычном понимании, то есть с именами) называют статическими, поскольку их положение в памяти не изменяется, то есть, статично. В приведенных выше примерах статические указатели ссылались на динамические переменные.
Работа с динамическими переменными таит ряд тонкостей, к ним надо привыкнуть, впитать в себя. Рассмотрим типичные ошибки.
Не инициализированный указатель
var ps : ^string;
begin
ps^:=’Hello !’; { В указателе мусор – нельзя обращаться через него }
end.
Здесь память для переменной не выделена, и указатель содержит мусор, обращение через такой указатель к несуществующей переменной вызовет крах программы.
Обращение через пустой указатель
var ps : ^string;
begin
ps := nil;
ps^:=’Hello !’; { Указатель пуст – нельзя обращаться через него }
end.
Урок предыдущего примера справедлив и здесь: обращение к памяти через пустой указатель тоже вызовет крах программы.
Обращение к уничтоженной переменной (висячие ссылки)
var ps : ^string;
begin
New(ps); ps^:=’Hello !’; { Это нормально }
Dispose(ps);
ps^:=’Bye !’; { Здесь ошибка, – переменной уже нет! }
end.
После освобождения памяти, занимаемой динамической переменной, обращаться к ней уже нельзя. Вот другой пример такой ошибки:
var p1, p2 : ^string;
begin
New(p1);
p2 := p1; { адрес динамической переменной копируется в другой указатель }
Dispose(p2); { Переменная освобождается через указатель p2 }
p1^:=’Hello !’; { Это ошибка, – переменной уже нет! }
Dispose(p1); { Это тоже ошибка ! }
end.
Здесь все тоньше. Динамическая переменная создана через один указатель, и её адрес скопирован в другой. Теперь можно обращаться к переменной и освобождать её через любой из них. Но после освобождения все ссылки на переменную теряют силу! Обратиться к ней, или повторно освободить уже нельзя!
Утечка памяти
Эта ошибка преследует даже опытных программистов. Её трудно заметить, поскольку расплата наступает после исчерпания всей памяти, а этого можно и не дождаться. В действительности никакой утечки памяти не происходит, правильней назвать это захламлением памяти или потерей ссылки на переменную. Итак, смотрим:
var p1, p2 : ^string;
begin
New(p1); New(p2); { создаем две динамические переменные }
p2 := p1; { адрес первой переменной копируется во второй указатель }
Dispose(p1); { Первую переменную освободить можно… }
Dispose(p2); { … а вторую – уже нельзя! }
end.
Здесь вторая переменная существует и после того, как указатель p2 переключился на первую. Но связь с нею потеряна навсегда, эту переменную нельзя даже освободить! Так объём свободной памяти в куче уменьшается, что и породило выражение «утечка памяти».
К счастью, после завершения программы операционная система сама освободит всю занятую программой память, включая потерянные участки. Не тревожьтесь – память вашего компьютера не пострадает!
В последующих главах мы построим несколько «боевых» программ с применением динамических переменных.
• В задачах, где нельзя предсказать объём обрабатываемых данных, используют динамические переменные.
• Память для динамических переменных выделяется из кучи – области свободной памяти, которой «заведует» операционная система.
• У динамических переменных нет имен, поэтому доступ к ним возможен только через указатели.
• Для выделения памяти вызывают процедуру New, она помещает в указатель адрес созданной переменной.
• Когда надобность в динамической переменной отпадает, занятую ею память освобождают процедурой Dispose.
• Обращение с динамическими переменными требует аккуратности; остерегайтесь таких ошибок, как висячие ссылки и утечка памяти.
А) Сравните размеры переменных и указателей на них, воспользуйтесь для этого следующей программкой. Напишите нечто подобное для переменных других типов.
type pb = ^byte; pw = ^word; pe = ^extended;
var b : byte; w : word; e : extended;
begin
Writeln(SizeOf(b):5, SizeOf(pb):5);
Writeln(SizeOf(w):5, SizeOf(pw):5);
Writeln(SizeOf(e):5, SizeOf(pe):5);
Readln;
end.
Б) Найдите ошибки в следующей программе и объясните их.
var p1, p2 : ^integer;
begin
p1 := 10;
p2^:= 20;
New(p1);
p2:= p1;
p1^:= 'Привет!';
Dispose(p1);
Dispose(p2);
end.
Задачи на темы предыдущих глав
В) Ник обожал музыку. Но компьютерный музыкальный проигрыватель раздражал программиста, поскольку при случайном выборе мелодий повторял одни песни, напрочь забывая о других. Предположим, в списке 10 песен, но звучали только три из них: 3, 6, 5, 6, 3, 6, 5 и т.д.
Ник создал «справедливый» проигрыватель, выбирающий мелодии иначе. Все песни состояли в одном из двух списков: «белом» или «черном». Изначально все они были в «белом» списке, и очередная мелодия выбиралась из него случайно, а после проигрывания ставилась в конец «черного». Если в этот момент в «черном» списке состояла половина мелодий, то первая мелодия из «черного» списка возвращалась в «белый». Затем снова случайно выбиралась мелодия из «белого» списка. Так гарантировалось отсутствие повторов ранее проигранных песен в течение достаточно длительного времени. Создайте программу, генерирующую случайные числа (мелодии) в диапазоне от 1 до N представленным выше методом. Значение N не превышает 255.
Г) Распечатывая числовое множество, мы выводили все его элементы по одному, не заботясь об экономии бумаги или места на экране. Напишите экономную процедуру печати множества, учитывающую подряд идущие диапазоны чисел. Вот примеры желаемой распечатки:
1,5..255
0..200,210..255
0..255
2,5,7,10..20,30..40