Виды памяти, указатели и работа с ними в языке Delphi

В любой вычислительной системе важным ресурсом является память. Поэтому управление ею – одна из важных задач программиста. Различают два способа распределения памяти. Статическое – во время трансляции программы, что эффективно, поскольку в ходе ее выполнения на управление памятью не расходуются ресурсы системы. Альтернативный подход – динамическое управление памятью, которое осуществляется во время выполнения программы.

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

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

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

Для организации динамической памяти используется тип данных, называемый указателем или ссылочным типом данных. Значением указателя является адрес области памяти, содержащей переменную заранее определенного типа. В этом случае указатели называются типизированными. Для указателей область памяти выделяется статически, а для переменных, на которые они ссылаются, – динамически. Адреса задаются совокупностью двух 16-разрядных слов, которые называются сегментом и смещением. Сегмент – это участок памяти, имеющий длину 64 Кбайт и начинающийся с адреса, кратного 16. Смещение указывает, сколько байт от начала сегмента необходимо пропустить, чтобы обратиться к нужному адресу. В результате, абсолютный адрес образуется: сегмент*16+смещение.

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

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

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

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

В программе на языке высокого уровня указатели могут быть типизированными и нетипизированными. При объявлении типизированного указателя определяется и тип объекта в памяти, им адресуемого. Для объявления переменных ссылочного типа используется символ «^», после которого указывается тип динамической (базовой) переменной.

Type <имя_типа>=^ <базовый тип>;

Var <имя_переменной>: <имя_типа>; или

<имя_переменной>: ^ <базовый тип>:

Например:

Type ss = ^ Integer;

Var x, yt: ss; {*Указатели на переменные целого типа *}

z: ^Real; {*Указатель на переменную вещественного типа *}

Здесь переменные x и y представляют собой адреса областей памяти, в которых хранятся целые число, а z – адрес области памяти, в которой хранится вещественное число. Хотя физическая структура адреса не зависит от типа и значения данных, хранящихся по этому адресу, компилятор считает указатели x, y и z, имеющими разный тип, и в Delphi оператор:

x := z;

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

Нетипизированный указатель (тип pointer в Delphi) служит для представления адреса, по которому содержатся данные неизвестного типа.

Зарезервированное слово Nil обозначает константу ссылочного типа, которая ни на что не указывает.

Выделение оперативной памяти для динамической переменной базового типа осуществляется с помощью процедуры New(x), где x определен как соответствующий указатель.

Обращение к динамическим переменным выполняется по правилу: <имя переменной> ^.

Например, x^:= 15. Здесь в область памяти (два байта), адрес которой является значением указателя x, записывается число 15.

Процедура Dispose(x) освобождает память, занятую динамической переменной. При этом значение указателя x становится неопределенным.

Основными операциями, в которых участвуют указатели, являются присваивание, получение адреса, выборка.

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

Операция получения адреса – одноместная, ее операнд может иметь любой тип, результатом является типизированный (в соответствии с типом операнда) указатель, содержащий адрес объекта-операнда.

Операция выборки – одноместная, ее операндом является типизированный указатель, результат – данные, выбранные из памяти по адресу, заданному операндом. Тип результата определяется типом указателя-операнда.

Перечисленных операций, как правило, достаточно для решения задач прикладного программирования, поэтому набор операций над указателями, допустимых в языке Delphi, этим и ограничивается.

К указателю можно прибавить целое число или вычесть из него целое число. Поскольку память имеет линейную структуру, прибавление к адресу числа даст адрес области памяти, смещенной на это число байт (или других единиц измерения) относительно исходного адреса. Результат операций «указатель + целое», «указатель – целое» имеет тип «указатель».

Можно вычесть один указатель из другого (оба указателя-операнда при этом должны иметь одинаковый тип). Результат такого вычитания будет иметь тип целого числа со знаком. Его значение показывает, на сколько байт (или других единиц измерения) один адрес отстоит от другого в памяти.

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

Операции адресной арифметики выполняются только над типизированными указателями. Единицей измерения в адресной арифметике является размер объекта, который указателем адресуется. Так, если переменная x определена как указатель на целое число, то выражение x+ 1 даст адрес, больший не на 1, а на количество байт в целом числе. Вычитание указателей также дает в результате не количество байт, а количество объектов данного типа, помещающихся в памяти между двумя адресами. Это справедливо как для указателей на простые типы, так и для указателей на сложные объекты, размеры которых составляют десятки, сотни и более байт.

Ниже приведено несколько схем, иллюстрирующих работу с указателями.

В результате выполнения следующих операций присваивания p1^:=2; p2 ^:=4 в выделенные участки памяти будут записаны значения 2 и 4.

В результате выполнения оператора присваивания p1^:= p2 ^ в участок памяти, на который ссылается указатель p1, будет записано значение 4.

После выполнения оператора присваивания p2:=p1 оба указателя будут содержать адрес первого участка памяти.

Использование динамических величин предоставляет программисту ряд дополнительных возможностей:

- подключение динамической памяти позволяет увеличить объем обрабатываемых данных;

- если потребность в каких-то данных отпала до окончания работы программы, то занимаемую ими память можно освободить для другой информации;

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

При этом следует понимать, что работа с динамическими данными замедляет выполнение программы, поскольку доступ к информации происходит в два этапа: сначала ищется указатель, а затем по нему – данное.


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: