Delphi динамическая память.

Delphi 7. Занятие 2_2.

Работа с динамической памятью.

Каждой программе выделяется своё адресное пространство. В нём хранятся коды команд, ячейки для хранения значений переменных, область стека. Область для хранения значений переменных выделяется при компиляции программы и не меняется в процессе работы программы. Такая память называется статической.

В 32-х разрядных приложениях адресное пространство ограничено значением 232-1 ~ 4 Гб (4 294 967 255 байт).

Предположим, что половина памяти отведена под переменные. Длина машинных команд может иметь максимум 15 байт. Тогда в оставшейся половине может разместиться примерно 143 000 000 машинных команд.

Будем считать (очень грубо), что на выполнение одного операнда (сложение, умножение и так далее) требуется 10 команд. Тогда оказывается доступно 14 300 00 операндов.

Если в одной строке (операторе) присутствует в среднем 10 операндов, то в программе может быть примерно 1 340 000 строк (операторов).

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

Тогда в запасе у программиста есть ещё 4 Гб памяти, называемой динамической. В отличие от статической, она распределяется в процессе работы программы.

Тип char.

Мы уже неоднократно использовали различные типы данных. Например, тип char. В переменной такого типа может храниться один символ. Всего под хранение символа отведен 1 байт, то есть можно закодировать 28-1=255 символа. В первой половине таблицы (127 символов) хранятся специальные символы языка, буквы латинского алфавита и цифры. Во второй половине — буквы национального алфавита.

Такой набор символов называется кодовой таблицей символов, можно найти в справочных материалах.

Для примера, латинской букве A соответствует код $41 в шестнадцатеричном исчислении, 65 в десятичном или 01 00 00 01 в двоичном.

Тип Integer.

Для хранения целых чисел мы использовали тип Integer. По него отводится 4 байта, то есть 32 бита. Так как старший бит кодирует знак (0 — число положительное, 1 — отрицательное), то под само число остаётся 31 бит, в которых может храниться число, меньшее 231-1 =2 147 483 647.

Тип double и extended.

Для работы с вещественными (десятичными) числами чаще всего применяют тип double, под который отводится 8 байт.

Но в хранении вещественных чисел есть одна особенность. Пусть у нас есть число (999 999 999 999. 999). В памяти компьютера оно будет представлено в виде (0.999 999 999 999 999 * 1012). то есть вещественное число хранится в двух частях. Первая часть 6 байт хранит мантису (наши девятки), а вторая — порядок (степень числа 10).

Для типа double мантиса размещается в 52 битах, то максимальное число, которое можно хранить 251-1 =4 503 599 627 370 495. Если вывести это число в Edit (преобразовав его в строку функцией floatToStr(х) ), то мы увидим
4, 503 599 627 370 5*1015 , то есть вывелась десятичная дробь, имеющая 14 знаков. Это произошло потому, что был отброшен 16-ый знак. Но так как это 5, то следующий знак увеличен на 1 и получилась единица переноса, поэтому вместо 4 мы увидели 5.

Если вывести число 888 888 888 888 888, занимающее 15 позиций, то оно выведется именно как 888 888 888 888 888. Если его умножить на 2, то позиций в числе станет 16, и число выведется в экспоненциальном виде
1,77 777 777 777 778E15, хотя на самом деле это число
1,77 777 777 777 7776E15 .Таким образом все разряды, большие пятнадцати, отбрасываются, а последний разряд округляется.

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

Пусть у этих чисел отличается только последний знак (например у одного числа он 6, а у другого 7). Но в итоге оба числа окажутся равными, так как последний знак будет отброшен и произведено округление.

Подобные ситуации трудно диагностировать. Поэтому при работе с большими числами надо быть очень внимательным!

Есть ещё один тип для работы с десятичными числами, для хранения которых отводится 10 байт — это тип Extended. У него 19 значащих цифр, в отличие от 15 у double.

Тип Currency.

Под этот тип тоже отводится 10 байт. Но если тип double может быть примерно до 1,7*10308 , тип Extended 1,1*104932 , то тип Currency имеет только 19 значащих цифр без экспоненты (без степени числа 10).

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

Тип String.

По сути, это одномерный открытый массив символов, длина которого ограничена только доступной памятью.

Идентификаторы переменных.

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

Поэтому, если 255 первых символов будут одинаковыми, а 256-ой символ у этих переменных отличается, они всё равно будут восприняты как одинаковые и произойдет ошибка компиляции, так как в пределах видимости не может не может быть двух одинаковых идентификаторов переменных.

При первом проходе компилятора создаётся таблица, сопоставляющая набор символов (имя переменной) и адрес, отведённый для хранения значения этой переменной.

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

Таким образом происходит преобразование мнемонических (символьных) имен в адреса переменных.

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

Параметры процедур.

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

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

Стек — это область памяти фиксированного объёма (65 535 байт) типа «очередь», организованная таким образом, что она растёт как лёд в воде, который сверху периодически поливают водой и он замерзает слой за слоем, а нижняя кромка льда опускается всё ниже и ниже, пока не достигнет дна.

Таким образом, последнее записанное значение оказывается на вершине стека, адрес которой зафиксирован.

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

Процедура считывает из стека такое количество значений, которое указано в её шапке. Значения последовательно считываются из стека, начиная с вершины, и присваиваются формальным параметрам процедуры.

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

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

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

Если же процедура должна работать с большим количеством данных, например с массивом, то в процедуру передаётся ссылка на первый элемент массива. В процедуре такие формальные переменные помечены ключевым словом «var».

В этом случае надо быть очень осторожным, так как работа будет проводиться с самим массивом, а не с его копией!

delphi указатели.

Динамическая память. Типы указателей.

При работе программы часто возникает необходимость хранить в памяти полученные результаты (например, результаты расчётов), количество которых заранее не известно.

Имеется возможность при наличии свободного пространства памяти распределять её для хранения таких данных. Как указывалось ранее, в 32-х разрядной версии Delphi возможно адресовать ~ 4Гб памяти. Это пространство называется «куча».

Для размещения данных в куче используют особые переменные, называемые «указатель».

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

Указатели бывают двух типов.

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

Поэтому такие указатели называются «типизированными».

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

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

Объявление типизированных указателей.

Типизированный указатель объявляется следующим образом:

type имя_типа=^базовый_тип (или пользовательский тип)

var имя_переменной_указателя: имя_типа;

Или непосредственно var имя_переменной_указателя:^базовый_тип (или пользовательский тип).

Например, объявить указатель на данные целого типа:

type tPInt=^Integer;
var vPInt: tPInt;

или

var vPInt:^Integer;

Delphi работа с памятью.

Но объявленная переменная-указатель пока не содержит в себе ссылки (адреса). Там находится случайная информация. Чтобы привязать к указателю конкретный адрес в куче, переменную надо инициализировать процедурой: new():

new(vPInt);

Delphi работа с памятью

Теперь в указатель vPInt записан конкретный адрес и по этому адресу можно разместить целое значение (разименовать указатель).

Делается это с помощью нотации:

vPInt^:=12345;

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

dispose(vPInt);

Теперь связь с кучей потеряна и область памяти, занимаемая ранен данными, оказывается свободной. Далее этот указатель использовать в программе нельзя.

Только что объявленный, но ещё не инициализированный указатель можно приравнять предопределённому «пустому» значению nil:

vPInt:=nil;

Это не позволит прочитать «мусор», если вдруг в указателе случайно оказался реальный адрес. Ещё хуже, если этот указатель указывает на область кучи, содержащей данные. Таким образом можно разрушить хранимые значения.

Однако и после «уничтожения» указателя его также можно приравнять nil.

Проверка значения указателя на nil не позволит дважды инициализировать указатель, что приведёт к ошибке времени выполнения и программа остановится.

Кроме того, если объявленный, но не инициализированный указатель сразу пометить как nil, топроверка указателя на nil позволит записывать данные только в инициализированный указатель.

Например:

if vPInt=nil then new(vPInt);
или if vPInt <> nil then vPInt^:=12345;

Процедуру dispose(vPInt); надо всегда применять, как только надобность в данных отпадает (во избежании утечки памяти).

Определение адреса и размера переменной.

Иногда необходимо знать адрес начала расположения переменной. Это делается с помощью функции

addr(X)

возвращающей адрес.

Применению указанной функции эквивалентна нотация: @X.

Замечание. Если приравнять vPInt:=addr(X); (или vPInt:=@X), то vPInt^ становится эквивалентом X.

Если мы создаём указатель на сложную структуру, например «запись», то бывает полезно знать, сколько байт она занимает в памяти. Для этого применяют функцию:

sizeOf(vPInt);

Нетипизированные указатели.

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

Нетипизированный указатель объявляется следующим образом:

var p:pointer;

Далее выделяется блок памяти процедурой

getMem(p,N)

где N – количество требуемых байт памяти.

Освобождается память процедурой:

freeMem(p,N)

Особенности объявления типизированных указателей.

Типизированные указатели, как и простые переменные, можно приравнивать друг к другу. Например:

var p1,p2:^Integer; x:Integer;
p1:=@x;
p2:=p1;

Но здесь есть существенный нюанс. Дело в том, что приравнивать можно только переменные одного типа.

При объявлении переменных тип переменной назначается неявно и он является уникальным для группы переменных, перечисленных в одном объявлении (как показано в приведённом примере);

Но если сделать объявление переменных:

var p1:^Integer;
p2:^Integer;

то типы, присвоенные для p1 и p2 при компиляции, будут иметь разные имена. Поэтому

p1:=@x;
p2:=p1;

вызовет ошибку времени выполнения.

В то же время нотация:

type tPInt=^Integer;
var p1: tPInt; p2:tPInt;
p1:=@x;
p2:=p1;

ошибки не вызовет, так как переменным присвоен один и тот же тип.

Другой способ избежать коллизии — использовать нетипизированный указатель. Нотация:

var p1:^Integer; p2:^Integer; p:pointer;
p1:=@x;
p:=p1;
p2:=p;

вполне допустима.

Заключение.

Рассмотрены понятия динамической и статической памяти, а также как в delphi динамическая память распределяется. Более подробно рассмотрены типы данных Char, Integer, Double. Добавлены новые типы extended и сurrency. Рассмотрен механизм связи идентификатора и его адреса. Также рассмотрен механизм использования параметров процедур и функций. Даны примеры, как в delphi указатели различного типа используются для организации памяти в куче, а также инструменты работы с ними посредством процедур и функций.

Глоссарий

  • Статическая память.
  • Динамическая память, куча.
  • Кодовая таблица.
  • Тип Currency.
  • Куча.
  • Типизированный указатель.
  • Нетипизированный указатель.
  • Процедурыnew() dispose() addr() sizeOf() getMem() freeMem() @
  • Разименовать указатель.
  • Значениеnil.
Обновлено: 07.01.2021 — 13:11

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *