Delphi увеличить память для приложения

Обновлено: 03.07.2024

Алексей Изюмов
дата публикации 25-05-2004 13:44

Совсем недавно мне пришлось писать серверное приложение на Delphi, которое довольно активно работало с базой данных и большим количеством потоков (threads). При этом я столкнулся с таким явлением, как постоянное увеличение объёма памяти, запрошенной приложением у Windows. Почти неделю с бубном и заячьим хвостиком (другие средства уже не помогали) я пытался обнаружить утечку памяти, но тщетно. Причём глобальная переменная AllocMemSize показывала общий объём динамической кучи, не превышающий 20 Mb, а Диспетчер задач, тем не менее, через сутки непрерывной работы программы выдавал цифру больше двухсот (!) мегабайт. При этом явных запросов на выделение глобальной памяти у приложения не было (даже перекрыл метод TMemoryStream.Realloc, чтобы размещать потоки (streams) в динамической куче). К тому же, приложение не использовало пакетов (BPL) и было однокомпонентным, т.е. существовала только одна копия менеджера памяти.

Главной задачей, поставленной при создании надстройки была формирование пула удаляемых областей памяти, чтобы в динамической куче не образовывались дыры, которые впоследствии и привели бы к увеличению фрагментации. Для повышения эффективности пула были установлены фиксированные размеры блоков памяти, кратные степени двойки. Таким образом, с одной стороны, получилось некоторое неэффективное увеличение общего объёма памяти (перекрытие могло составить от 0 до 99.9%), а с другой стороны это позволило для каждого фиксированного размера вести собственный пул, что положительно сказалось на производительности. То есть, платим одной монетой (дополнительным расходом памяти, в среднем до 50%), получаем другую - производительность.

Дополнительно, все запросы на крохотные блоки памяти (размером 1..16 байт) приводились к 16 байтам (т.е. от ловли блох отказался).

  • при запросе на выделение памяти (GetMem) надстройка округляет размер блока до ближайшей большей степени двойки (16, 32, 64, . 64k), и смотрит, есть ли в пуле указатели на блок такого размера. Если пул для данного размера пуст, то запрос передаётся стандартному менеджеру кучи;
  • при освобождении блока памяти (FreeMem) надстройка пытается сохранить указатель на этот блок в пуле (в данной реализации установлено фиксированное ограничение на размер массива). Если в пуле ещё есть место, то указатель сохраняется, а стандартный менеджер не узнаёт о том, что блок освобождён (т.е. дыры не образуется)
  • при уменьшении размера существующего блока памяти (ReallocMem) ничего не происходит, т.к. блок меньшего размера вполне способен поместиться в области большего размера; при увеличении размера сначала идёт проверка, не вписывается ли новый размер в текущую область (которая была выделена с округлением до ближайшей степени двойки)? Если вписывается, то ничего не происходит, иначе (опять же через надстройку) выделяется блок памяти большего размера, а текущий блок, скорее всего, попадает в пул.

Со временем надстройка стремится к идеальному случаю работы, который заключается в том, что в определённый момент потребление памяти достигает пика, и все указатели на освобождаемые блоки попадают в пул. В этом случае все запросы на выделение/освобождение памяти будут обработаны надстройкой, и к стандартному менеджеру не попадёт ни один вызов. Как следствие, не будет причин для фрагментации памяти.

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

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

Модуль LFHeapMemory.pas, в котором реализована надстройка над менеджером кучи, желательно подключать первым в списке uses проекта, чтобы все запросы на выделение памяти прошли через надстройку. При инициализации модуль подменяет указатели на менеджер памяти, таким образом, работа надстройки является прозрачной и не требует переделки программы.

Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter.
Функция может не работать в некоторых версиях броузеров.

function AllocMem(Size: Cardinal): Pointer;
Выделяет в куче блок памяти заданного размера. Каждый байт выделенной памяти выставляется в ноль. Для освобождения памяти используется FreeMem.

AllocMemCount

var AllocMemCount: Integer;
Содержит количество выделенных блок памяти. Эта переменная увеличивается каждый раз, когда пользователь запрашивает новый блок, и уменьшается, когда блок освобождается. Значения переменной используется для определения количества "оставшихся" блоков.

Так как переменная является глобальной и живёт в модуле System, её прямое использование не всегда безопасно. Модули, слинкованные статически, будут иметь разные экземпляры AllocMemCount. Статически слинкованными считаются приложения, не использующие пакеты времени выполнения (runtime packages). В следующей таблице обобщены сведения по использованию AllocMemCount в зависимости от типа приложения.

Тип приложенияДоступность AllocMemCount
EXEПриложения, не использующие пакеты и dll-и Delphi могут спокойно обращаться к данной глобальной переменной, т.к. для них существует только один её экземпляр.
EXE с пакетами без dllПриложения, использующие пакеты и не использующие dll-ки также могут спокойно работать с AllocMemCount. В этом случае все модули линкуются динамически, и существует только один экземпляр переменной, т.к. пакеты, в отличие от dll, умеют работать с глобальными переменными.
EXE со статически слинкованными dllЕсли приложение и используемые им dll-ки являются статически слинкованными с библиотекой выполнения (RTL), AllocMemCount никогда не следует использовать напрямую, т.к. и приложение, и dll-ки будут иметь собственные её экземпляры. Вместо этого следует использовать функцию GetAllocMemCount, живущую в BorlandMM, которая возвращает значение глобальной переменной AllocMemCount, объявленную в BorlandMM. Этот модуль отвечает за распределение памяти для всех модулей, в списке uses который первой указан модуль sharemem. Функция в данной ситуации используется потому, что глобальные переменные, объявленные в одной dll невидимы для другой.
EXE с пакетами и статически слинкованными dll-камиНе рекомендуется создавать смешанные приложения, использующие и пакеты, и статически слинкованные dll-ки. В этом случае следует с осторожностью работать с динамически выделяемой памятью, т.к. каждый модуль будет содержать собственный AllocMemCount , ссылающийся на память, выделенную и освобождённую именно данным модулем.

AllocMemSize

var AllocMemSize: Integer;
Содержит размер памяти, в байтах, всех блоков памяти, выделенных приложением. Фактически эта переменная показывает, сколько байтов памяти в данный момент использует приложение. Поскольку переменная является глобальной, то к ней относится всё, сказанное в отношении AllocMemCount.

GetHeapStatus
function GetHeapStatus: THeapStatus;
Возвращает текущее состояние диспетчера памяти.

type
THeapStatus = record
TotalAddrSpace: Cardinal;s
TotalUncommitted: Cardinal;
TotalCommitted: Cardinal;
TotalAllocated: Cardinal;
TotalFree: Cardinal;
FreeSmall: Cardinal;
FreeBig: Cardinal;
Unused: Cardinal;
Overhead: Cardinal;
HeapErrorCode: Cardinal;
end;
Если приложение не использует модуль ShareMem, то данные в записи TheapStatus относятся к глобальной куче (heap), в противном случае это могут быть данные о памяти, разделяемой несколькими процессами.
TotalAddrSpaceАдресное пространство, доступное вашей программе в байтах. Значение этого поля будет расти, по мере того, как увеличивается объём памяти, динамически выделяемый вашей программой.
TotalUncommittedПоказывает, сколько байтов из TotalAddrSpace не находятся в swap-файле.
TotalCommittedПоказывает, сколько байтов из TotalAddrSpace находятся в swap-файле. Соответственно, TotalCommited + TotalUncommited = TotalAddrSpace
TotalAllocatedСколько всего байтов памяти было динамически выделено вашей программой
TotalFreeСколько памяти (в байтах) доступно для выделения вашей программой. Если программа превышает это значение, и виртуальной памяти для этого достаточно, ОС автоматом увеличит адресное пространство для вашего приложения и соответственно увеличится значения TotalAddrSpace
FreeSmallДоступная, но неиспользуемая память (в байтах), находящаяся в "маленьких" блоках.
FreeBigДоступная, но неиспользуемая память (в байтах), находящаяся в "больших" блоках. Большие блоки могут формироваться из непрерывных последовательностей "маленьких".
UnusedПамять (в байтах) никогда не выделявшаяся (но доступная) вашей программой. Unused + FreeSmall + FreeBig = TotalFree.
OverheadСколько памяти (в байтах) необходимо менеджеру кучи, чтобы обслуживать все блоки, динамически выделяемые вашей программой.
HeapErrorCodeВнутренний статус кучи

Учтите, что TotalAddrSpace, TotalUncommitted и TotalCommitted относятся к памяти ОС, выделяемой для вашей программы, а TotalAllocated и TotalFree относятся к памяти кучи, используемой для динамического выделения памяти самой программой. Таким образом, для отслеживания того, как ваша программа использует динамическую память, используйте TotalAllocated и TotalFree. Константы для HeapErrorCode живут в MEMORY.INC (highly recommended для всех продвинутых и интересующихся). За компанию приведём и их.

КодКонстантаЗначение
0cHeapOkВсё отлично
1cReleaseErrОС вернула ошибку при попытке освободить память
2cDecommitErrОС вернула ошибку при попытке освободить память, выделенную в swap-файле
3cBadCommittedListСписок блоков, выделенных в swap-файле, выглядит подозрительно
4cBadFiller1Хреновый филлер. (Ставлю пиво тому, кто объяснит мне, что это значит). Судя по коду в MEMORY.INC, значения выставляются в функции FillerSizeBeforeGap, которая вызывается при различного рода коммитах (т.е. при сливании выделенной памяти в swap). И если что-то в этих сливаниях не срабатывает, функция взводит один из этих трёх флагов.
5cBadFiller2"-/-"
6cBadFiller3"-/-"
7cBadCurAllocЧто-то не так с текущей зоной выделения памяти

8cCantInitНе вышло инициализироваться
9cBadUsedBlockИспользуемый блок памяти нездоров
10cBadPrevBlockПредыдущий перед используемым блок нездоров
11cBadNextBlockСледующий после используемого блок нездоров
12cBadFreeListХреновый список свободных блоков. Судя по коду, речь идёт о нарушении последовательности свободных блоков в памяти
13cBadFreeBlockЧто-то не так со свободным блоком памяти
14cBadBalanceСписок свободных блоков не соответствует действительности

GetMemoryManager

procedure GetMemoryManager(var MemMgr: TMemoryManager);
Возвращает указатель на текущий диспетчер памяти. Структура TMemoryManager описана ниже.
TMemoryManager - структура данных

type
PMemoryManager = ^TMemoryManager;

TMemoryManager = record
GetMem: function(Size: Integer): Pointer;
FreeMem: function(P: Pointer): Integer;
ReallocMem: function(P: Pointer; Size: Integer): Pointer;
end;

Эта запись определяет, какие функции используются для выделения и освобождения памяти. Функция GetMem должна выделить блок памяти размером Size (Size никогда не может быть равным нулю) и вернуть на него указатель. Если она не может этого сделать, она должна вернуть nil. Функция FreeMem должна освободить память Size по адресу P. P никогда не должен быть равен nil. Если функция с этим справилась, она должна вернуть ноль. Функция ReallocMem должна перевыделить память Size для блока P. Здесь P не может быть nil и Size не может быть 0 (хотя при вызове ReallocMem не из диспетчера памяти, это вполне допускается). Функция должна выделить память, при необходимости, переместить блок на новое место и вернуть указатель на это место. Если выделение памяти невозможно, она должна вернуть nil.

HeapAllocFlags

var HeapAllocFlags: Word = 2;
Этими флагами руководствуется диспетчер памяти при работе с памятью. Они могут комбинироваться и принимать следующие значения (по умолчанию - GMEM_MOVEABLE):

ФлагЗначение
GMEM_FIXEDВыделяет фиксированную память. Т.к. ОС не может перемещать блоки памяти, то и нет нужды блокировать память (соответственно, не может комбинироваться с GMEM_MOVEABLE)
GMEM_MOVEABLEВыделяет перемещаемую память. В Win32 блоки не могут быть перемещены, Если они расположены в физической памяти, но могут перемещаться в пределах кучи.
GMEM_ZEROINITПри выделении памяти (например, функцией GetMem) все байты этой памяти будут выставлены в 0. (отличная черта)
GMEM_MODIFY Используется для изменения атрибутов уже выделенного блока памяти
GMEM_DDESHAREВведёны для совместимости с 16-разрядными версиями, но может использоваться для оптимизации DDE операций. Собственно, кроме как для таких операций эти флаги и не должны использоваться
GMEM_SHARE"-/-"
GPTRПредустановленный, соответствует GMEM_FIXED + GMEM_ZEROINIT
GHNDПредустановленный, соответствует GMEM_MOVEABLE + GMEM_ZEROINIT

IsMemoryManagerSet

function IsMemoryManagerSet:Boolean;
Возвращает TRUE, если кто-то успел похерить дефолтовый диспетчер памяти и воткнуть вместо него свой.

procedure ReallocMem(var P: Pointer; Size: Integer);
Перевыделяет память, ранее выделенную под P. Реальные действия процедуры зависят от значений P и Size.
P = nil, Size = 0: ничего не делается;
P = nil, Size <> 0: соответствует вызову P := GetMem (Size);
P <> nil, Size = 0: соответствует вызову FreeMem (P, Size) (с тем отличием, что FreeMem не будет обнулять указатель, а здесь он уже равен nil).
P <> nil, Size <> 0: перевыделяет для указателя P память размером Size. Текущие данные никак не затрагиваются, но если размер блока увеличивается, новая порция памяти будет содержать всякий мусор. Если новый блок "не влазит" на своё старое место, он перемещается на новое место в куче и значение P обновляется соответственно. Это важно: после вызова данной процедуры блок P может оказаться в памяти по совсем другому адресу!

SetMemoryManager

procedure SetMemoryManager(const MemMgr: TMemoryManager);
Устанавливает новый диспетчер памяти. Он будет использоваться при выделении и освобождении памяти процедурами GetMem, FreeMem, ReallocMem, New и Dispose, а также при работе конструкторов и деструкторов объектов и работе с динамическими строками и массивами.
SysFreeMem, SysGetMem, SysReallocMem
Используются при написании собственного диспетчера памяти. Другого смысла в них я не нашёл.

Думаете, очень сложно? Как бы не так. Вот пример из справочной системы самой Delphi: этот диспетчер будет запоминать количество выделений, освобождений и перевыделений памяти:

var
GetMemCount: Integer;
FreeMemCount: Integer;
ReallocMemCount: Integer;
OldMemMgr: TMemoryManager;

function NewGetMem(Size: Integer): Pointer;
begin
Inc(GetMemCount);
Result := OldMemMgr.GetMem(Size);
end;

function NewFreeMem(P: Pointer): Integer;
begin
Inc(FreeMemCount);
Result := OldMemMgr.FreeMem(P);
end;

function NewReallocMem(P: Pointer; Size: Integer): Pointer;
begin

Inc(ReallocMemCount);
Result := OldMemMgr.ReallocMem(P, Size);
end;

const
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);

procedure SetNewMemMgr;
begin
GetMemoryManager(OldMemMgr);
SetMemoryManager(NewMemMgr);
end;

Пожалуйста, выделяйте текст программы тегом [сode=pas] . [/сode] . Для этого используйте кнопку [code=pas] в форме ответа или комбобокс, если нужно вставить код на языке, отличном от Дельфи/Паскаля.

'> как при запуке проги выделить 1гиг памяти? , нужен гиг памяти пользователя.

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

Зачем?
В процессе работы она что, сама не может брать, сколько захочет? Смущает фраза "чтобы они были зарезервированы только для неё". Речь идёт о физической памяти? А если столько нету?

Ок, для работы программа берёт память:
1). Выделяется менеджером памяти (из кучи).
2). Как-то выделяется руками из заранее подготовленного участка.

Я не вижу разницы между этими двумя случаями. Какое-такое принципиальное отличие во втором случае, что вы прямо уверены, что там не будет фрагментации?

Большом количестве чего? Здесь нет телепатов.


Ну-ка, поделитесь с общественностью, как это вы проверили?

Зачем нам куча? Просто выделяем виртуальную память через функцию VirtualAlloс. Гранулярность 64 КБ. Никакой фрагментации и другого бреда .

p:=VirtualAlloc(0,size,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE);

вот только зачем это?

Можно выделить до 2 ГБ памяти, но только есть ограничения. Не помню точно какие, но одно точно помню (в Win32) : объём памяти выделенный всеми процессами в сумме, не должен превышать 4 ГБ (т.е. чтобы размер файла подкачки не превышал 4 ГБ).

Не помню точно какие, но одно точно помню (в Win32) : объём памяти выделенный всеми процессами в сумме, не должен превышать 4 ГБ (т.е. чтобы размер файла подкачки не превышал 4 ГБ).

С чего вы это взяли? Нет таких ограничений. Для начала - суммарная виртуальная память это всё же RAM + pagefile, а не просто pagefile. Кроме того, в конце-концов есть же AWE. Или вы про это?
Кстати, при желании можно и в обычном пользовательском режиме 3Gb использовать: Summary of the recent spate of /3GB articles.

Просто выделяем виртуальную память через функцию VirtualAlloс
Чем это отличается от GetMem(P, 700*1024*1024); или SetLength(S, 700*1024*1024)? Проблема с фрагментацией как была, так и осталась. А что, собственно, за проблемы с фрагментацией?
Если мы работаем в защищенном режиме, то мы всегда работатем через таблицу страниц и процессор на аппаратном уровне производит трансляцию адресов, поэтому на уровне прикладной задачи память непрерывна, а если физически она расположена в разных частях, то на скорости доступа это никак не сказывается.
Если же мы работаем в реальном режиме. А что, кто-то еще работает в реальном режиме?
Речь идёт не о фрагментации физической памяти, а о фрагментации адресного пространства процесса.
Представьте, что вы выделили 2 гига памяти кусками по 1 Mb. Теперь вы освобождаете каждый нечётный кусок. Получается, что вы освободили 1 Gb памяти, и занято у вас тоже 1 Gb (т.е. 50/50). Причём карта памяти похожа на решето - куски занятой памяти равномерно перемешаны с кусками свободной памяти. А теперь вы хотите выделить 2 Mb памяти. Несмотря на то, что суммарно у вас ещё есть целый 1 Gb свободной памяти, вы не можете это сделать, т.к. максимальный размер непрерывного свободного участка у вас = 1 Mb. Зато у вас 1024 таких участка
Это и есть фрагментация. Суть её в том, что из-за интернисвного цикла выделения/освобождения памяти адресное пространство процесса всё больше начинает напоминать решето. Именно по этой причине практически невозможно использовать в программе максимум памяти, доступной режиму пользователя - всегда будут зазоры.
При этом нет разницы, выделяет ли вам память ОС, менеджер памяти или вы выделяете их из зарезервированного участка памяти, как это хочет сделать автор вопроса.
Разница может быть только в алгоритме выделения. Не имеет значения место выделения. Имеет значение как, в какой последовательности вы работаете с памятью.
Например, в отличие от менеджера памяти старых Delphi, менеджер памяти в новых Delphi (FastMM) группирует данные по размеру: мелкие - с мелкими, средние - со средними, а для крупных он вообще вроде запрашивает у ОС отдельные блоки памяти. Это способствует уменьшению фрагментации памяти. Другой пример - нет необходимости выделять память под каждый из кучи однотипных объектов. Лучше загнать их в массив. Ещё пример? Что ж, например, в XP и выше есть куча с низкой фрагментацией. Потом. В нашем же примере - если бы мы заранее сперва выделили память для всех чётных кусков, а затем - для нечётных, то после освобождения памяти у нас получилось бы начало адресного пространства - один целый блок занятой памяти в 1 Gb, а вторая половина - один свободный блок памяти в 1 Gb. Т.е. 0% фрагментации. объём памяти выделенный всеми процессами в сумме, не должен превышать 4 ГБ (т.е. чтобы размер файла подкачки не превышал 4 ГБ).

да, да, да забыл. это ещё не считая промэпированную память. но там ещё есть ограничения. в книге Руссиновича и Соломна всё подробно об этом написано, не помню точно.

Для начала - суммарная виртуальная память это всё же RAM + pagefile, а не просто pagefile

нет, но суммарная виртуальная память это только pagefile, потому что "а вдруг придётся выгрузить всю память в своп", хотя в режиме ядра есть ещё и невыгружаемая память. Вернее даже если физической памяти хватает, файл подкачки всё равно будет занимать место, но при этом он не будет использоваться. Например, у меня XP 1 Гиг памяти, её хватате и избытком, всё приложения сейчас в сумме расходуют примерно 400 метров, но файл подкачки имеет размер 512 метром, но при этом почти не используется (в настройках файл подкачки 512-2048). И когда гооврят что файл подкачки должен иметь размер не меньше, чем размер физической памяти, исходят из утверждения "а вдруг придётся выгрузить всю память в своп". опять же, в книге Руссиновича и Соломна всё подробно об этом написано, не помню точно.

Чем это отличается от GetMem(P, 700*1024*1024); или SetLength
(S, 700*1024*1024)? Проблема с фрагментацией как была, так и осталась.

А как ты объяснишь что гранулярность у них 8 байт, в то время как у VirtualAlloc 64КБ? Функции GetMem и SetLength работают с кучей а не с виртуальнйо памятью, и они вызывают функции HeapAlloc.

нет, это уже другая история, и толк от AWE есть только тогда когда ядро винды PAE и размер физической памяти больше чем размер виртуальной памяти, т.е. когда физическая память больше чем 2 ГБ.

А по поводу менеджера куч Delphi, то там разработчики сделали так: в начале выделяется большой кусок памяти под кучу, и функции GetMem и SetLength это аналоги функции HeapAlloc, т.е. выделяют память вручную из кучи. Не понимаю зачем они так сделали когда можно воспользоваться теми функциями, которые предоставляет сама система.

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

В 32-х разрядных приложениях адресное пространство ограничено значением 2 32 -1

4 Гб (4 294 967 255 байт).

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

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

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

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

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

Тип char.

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

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

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

Тип Integer.

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

Тип double и extended.

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

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

Для типа double мантиса размещается в 52 битах, то максимальное число, которое можно хранить 2 51 -1 =4 503 599 627 370 495. Если вывести это число в Edit (преобразовав его в строку функцией floatToStr(х) ), то мы увидим
4, 503 599 627 370 5*10 15 , то есть вывелась десятичная дробь, имеющая 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*10 308 , тип Extended 1,1*10 4932 , то тип 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;

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

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

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

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

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

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

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 указатели различного типа используются для организации памяти в куче, а также инструменты работы с ними посредством процедур и функций.

Читайте также: