Addr — Функция Delphi


Содержание

Addr — Функция Delphi

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

Не приветствуется поднятие старых тем. Если ваш вопрос перекликается со старой темой, то для вопроса лучше создать новую тему, а старую указать в первом сообщении с описанием взаимосвязи.
Внимание:
попытки открытия обсуждений реализации вредоносного ПО, включая различные интерпретации спам-ботов, наказывается предупреждением на 30 дней.
Повторная попытка — 60 дней. Последующие попытки бан.
Мат в разделе — бан на три месяца.
Полезные ссылки:
MSDN Library FAQ раздела Поиск по разделу Как правильно задавать вопросы

Выразить свое отношение к модераторам раздела можно здесь: Krid, Rouse_

Менеджер памяти Delphi

Автор: Андрей Мистик
Источник: RSDN Magazine #2

Опубликовано: 21.02.2003
Исправлено: 13.03.2005
Версия текста: 1.0

Введение

Многие стандартные типы данных, такие, как классы, интерфейсы, строки, динамические массивы, неявно работают с динамической памятью. Зачастую пользователь даже и не подозревает, сколько обращений к динамической памяти происходит в той или иной строчке кода. Размеры объектов в динамической памяти могут колебаться от нескольких байт (строки) до многих мегабайт (пользовательские вызовы функций GetMem и AllocMem, а также создание динамических массивов). Достаточно трудно представить себе, какое количество строк и объектов может находиться в памяти во время работы программы. Естественно, что в такой ситуации требуется наличие быстрого и экономичного менеджера памяти, какой и предоставляется Delphi. Приведу цитату Евгения Рошаля, который в описании новых возможностей своей программы Far (версия 1.70 beta 2, сборка 321 от 16.12.2000) писал: «Для компиляции FAR Manager использовался Borland C/C++ 5.02. MSVC 6 SP4 не оправдал ожиданий (FAR 1.70 beta 1) и добавил тормозов (работа с выделением памяти для мелких объектов)». Известно, что менеджеры памяти в Borland C/C++, Borland C++ Builder и Delphi имеют общие алгоритмы работы. В данной статье я постараюсь в общих чертах описать принципы работы менеджера памяти Delphi.

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

Вся реализация менеджера памяти (за исключением некоторых объявлений в интерфейсной части модуля System.pas) расположена в файле GetMem.inc, хранящемся в каталоге $(DELPHI)/Source/Rtl/Sys. Менеджер памяти Delphi полностью реализован на Object Pascal, без использования ассемблерных вставок. Заметим еще, что в Kylix нет своего менеджера памяти, поэтому вызовы GetMem, FreeMem и ReallocMem сводятся к соответствующим вызовом malloc, realloc и free из стандартной C-библиотеки.

Для понимания работы менеджера памяти Delphi вам понадобятся знание принципов работы виртуальной памяти в Windows, а также понимание принципов работы четырех функций: VirtualAlloc, VirtualFree, LocalAlloc и LocalFree. Эта информация выходит за рамки данной статьи, и может быть получена либо из книги Джеффри Рихтера «Windows для профессионалов, 4-е издание», либо из MSDN.

Прежде всего, условимся о смысле используемых терминов. Основная путаница возникает из-за того, что менеджер памяти Delphi сам использует услуги менеджера виртуальной памяти Windows. Блок памяти, свободный с точки зрения программы, может считаться выделенным с точки зрения Windows. Итак, фрагмент памяти будем называть свободным, если он целиком состоит из страниц свободной памяти в смысле Windows. Фрагмент памяти будем называть зарезервированным, если он целиком состоит из страниц памяти, зарезервированных приложением. Фрагмент памяти будем называть выделенным, если он состоит целиком из страниц памяти, под которые выделена физическая память. Соответственно, фрагмент памяти будем называть не выделенным, если он состоит из страниц, зарезервированных приложением, но под которые не выделено физической памяти. Фрагмент памяти будем называть используемым, если он был передан Delphi-программе. И, наконец, фрагмент памяти будем называть неиспользуемым, если под ним расположена физическая память, но в настоящее время он не используется приложением Delphi. Вся иерархия памяти представлена на рис. 1.

Структура менеджера памяти Delphi

Менеджер памяти Delphi состоит из четырех администраторов, а также отладочных и диагностических функций. Схема зависимости администраторов представлена на рисунок 2. Расскажем коротко о каждом из них.

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

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

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

Администратор используемой памяти является самым низкоуровневым администратором. Он непосредственно общается с самим приложением Delphi, удовлетворяя его запросы на выделение и освобождение памяти. Он хранит в себе информацию обо всей выделенной памяти.

Одной из особенностей работы всех администраторов (кроме администратора описателей блоков) является то обстоятельство, что они работают по принципу «можно больше, нельзя меньше». При запросе администратор может выделить больше запрашиваемого количества памяти, и уже администратор более низкого уровня должен решать, как ему использовать остаток. Так, скажем, при попытке зарезервировать область памяти размером 100 байт (обращение от администратора выделенной памяти к администратору зарезервированной памяти), все равно будет зарезервирован регион в 1 Мб, и администратор выделенной памяти сам должен будет решить, как ему поступать с остатком. Соответственно при попытке освобождения памяти администратор может освободить меньший фрагмент.

Сделаем также ряд замечаний по поводу схемы обработки ошибок менеджером памяти Delphi. Если функция возвращает выделенный блок памяти, то в случае ошибки возвращается блок со значением addr, равным nil. В случае ошибки при освобождении памяти устанавливается глобальная переменная heapErrorCode. Кроме того, ряд функций, возвращающих значение типа Boolean, сигнализируют о случившейся ошибке, возвращая значение False.

Опишем еще пару переменных – initialized, которая проверяет, инициализирован ли менеджер памяти, и heapLock – критическую секцию для обеспечения синхронизации при многопоточности. Отметим, что вход в критическую секцию происходит только в том случае, когда глобальная переменная IsMultiThread установлена в True. Это одна из причин, по которым потоки должны создаваться вызовом функции BeginThread, а не напрямую при помощи функции CreateThread.

Администратор описателей блоков

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

В листинге 1 приведены все объявления, относящиеся к данному администратору.

Обратим внимание на то, что менеджер памяти Delphi различает такие понятия, как блок (TBlock) и описатель блока (TBlockDesc). Блок задает фрагмент памяти и содержит в себе указатель на начало фрагмента и его длину. Блоки используются в основном для передачи параметров и при объявлении локальных переменных. Время жизни блока обычно ограничивается той процедурой или функцией, где он объявлен. Описатель блока – это элемент двунаправленного списка, который описывает некоторый фрагмент памяти.

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

Сразу отметим, что кольцевые двунаправленные списки, которые используются менеджером памяти Delphi, бывают двух типов. Основное различие типов – в способе выполнения операций вставки блока в список и удаления блока из списка. При добавлении блока в список первого типа, остальные блоки в списке остаются без изменений. Как следствие, удалить из такого списка мы можем лишь тот блок, который был вставлен в него ранее. При добавлении блока в список второго типа проверяется, не граничит ли добавляемый блок с остальными блоками в списке. Если это так, то происходит «слияние» блоков, т. е. все граничащие между собой блоки заменяются одним. Соответственно, удалить из такого списка можно любой блок памяти, лишь бы он содержался внутри одного из блоков, присутствующих в списке. В этом случае может произойти «расщепление» блока на два. Использование списков второго типа позволяет избежать фрагментации памяти.

Рассмотрим теперь динамическое выделение памяти под описатели. Все данные, относящиеся к администратору описателей блоков, хранятся в виде двух однонаправленных списков. В первом из них (blockDescBlockList) хранятся сами описатели группами по сто. Во втором списке (blockDescFreeList) хранятся свободные в настоящий момент описатели, а в качестве связи между ними используется поле next описателя. Каждый из описателей находится в одной из структур типа TBlockDescBlock списка blockDescBlockList, а если описатель свободен, то также и в списке blockDescFreeList. Типичное состояние администратора описателей блоков приведено на рисунке 3.

Кратко опишем функции администратора описателей блоков.

Данная функция создает новый описатель блока.

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

Данная функция динамически создает описатель для блока b и добавляет его (описатель) непосредственно перед элементом prev. В качестве элемента prev в текущей реализации менеджера памяти всегда используется указатель на корневой элемент списка первого типа. Таким образом, функция добавляет описатель блока в начало списка. В случае успеха функция возвращает True.

Данная процедура удаляет описатель блока из списка первого типа и освобождает этот описатель.

Данная функция добавляет указанный блок памяти в двунаправленный кольцевой список второго типа (см. выше). Возвращаемым значением является блок памяти, образовавшийся в результате слияния добавляемого блока с уже существующими. В случае ошибки значение addr в возвращаемом блоке установлено в nil.

Данная функция удаляет блок памяти из кольцевого двунаправленного списка второго типа. В случае успеха функция возвращает True.

Администратор зарезервированной памяти

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

Этой особенностью поведения менеджера виртуальной памяти Windows объясняется и присутствие в администраторе зарезервированной памяти функций Commit и Decommit, которые производят соответственно передачу и возврат физической памяти со страниц зарезервированного фрагмента, но не имеют ограничений, присущих функциям VirtualAlloc и VirtualFree. Но для корректного выполнения таких операций необходимо обладать информацией обо всех вызовах функции VirtualAlloc с целью резервирования адресного пространства. Наличие этих функций в администраторе зарезервированной памяти также позволило выделить все обращения к менеджеру виртуальной памяти Windows в один администратор.

В листинге 3 приведены все объявления, относящиеся к данному администратору.

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

  • cSpaceAlign, показывающая, по какой границе будут выравниваться резервируемые фрагменты памяти;
  • cSpaceMin, показывающая минимальный размер блока памяти, который будет зарезервирован;
  • cPageAlign, задающая размер страницы.

Коротко опишем назначение и особенности каждой из функций, входящих в состав администратора.

Резервирует регион памяти размером, как минимум, равным minSize байт. Возвращает блок памяти, который был фактически зарезервирован. В случае ошибки возвращает в поле addr блока памяти значение nil. Зарезервированная память имеет атрибут доступа PAGE_NOACCESS.

Резервирует регион памяти по указанному адресу addr размером, как минимум, равным minSize байт. Возвращает блок памяти, который был фактически зарезервирован. В случае ошибки возвращает в поле addr блока памяти значение nil. Зарезервированная память имеет атрибут доступа PAGE_READWRITE.

Данная функция обладает одной интересной особенностью, а именно, она может зарезервировать фрагмент памяти, который имеет размер меньше запрашиваемого. Подробнее эта особенность будет рассмотрена при описании функции GetCommittedAt.

Пытается освободить по указанному адресу addr не более чем maxSize байт. Возвращает блок памяти, который был фактически освобожден. Если память не освобождалась, функция возвратит в поле addr блока памяти значение nil. В случае ошибки устанавливает переменную heapErrorCode в значение cReleaseErr.

Функция выделяет фрагмент памяти по указанному адресу addr размером не менее minSize байт. В случае ошибки возвращает в поле addr блока памяти значение nil. Выделенная страница памяти будут иметь атрибуты доступа PAGE_READWRITE.

Функция пытается вернуть физическую память по указанному адресу addr, но не более чем maxSize байт. Результат функции – адрес возвращаемого блока физической памяти. Если физическая память не была возвращена, функция возвратит значение nil в поле addr блока памяти. В случае ошибки устанавливает переменную heapErrorCode в значение cDecommitErr.

Администратор выделенной памяти

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

В листинге 4 приведены все объявления, относящиеся к данному администратору.

Администратор выделенной памяти содержит один двунаправленный список decommittedRoot, в котором содержится информация обо всей зарезервированной, но не выделенной памяти. Первоначально список не содержит элементов. Память добавляется и удаляется из списка decommittedRoot при помощи вызовов MergeBlockAfter и RemoveBlock, таким образом, в этом списке не может содержаться двух смежных фрагментов памяти (т. е. это список второго типа). Кроме этого, в работе администратора используется одна константа – cCommitAlign, которая показывает, по какой границе будут выравниваться запросы на выделение памяти.

Расскажем немного о взаимодействии между администраторами зарезервированной памяти и выделенной памяти. Для всех фрагментов памяти, которые входят в список decommittedRoot, администратор выделенной памяти производит передачу физической памяти, пользуясь функцией Commit администратора зарезервированной памяти. Если же для удовлетворения запроса администратору зарезервированной памяти не хватает зарезервированной, но не выделенной памяти, он вызывает GetSpace. Соответственно администратор выделенной памяти возвращает физическую память при помощи вызова функции Decommit администратора зарезервированной памяти. После этого указанный фрагмент добавляется к списку decommittedRoot, и производится контрольная проверка на снятие резервирования с нового фрагмента памяти, образованного слиянием освобождаемого блока памяти с теми, которые уже находились в списке decommittedRoot.

Следует обратить внимание на одну особенность поведения функции GetCommittedAt администратора выделенной памяти. Данная функция в цикле вызывает функцию GetSpaceAt до тех пор, пока размер зарезервированного пространства не превысит запрашиваемый размер (см. особенности функции GetSpaceAt). Но при этом данному фрагменту памяти будет соответствовать несколько описателей в списке spaceRoot. Для чего это было сделано? Вспомним, что вызов функции VirtualAlloc для освобождения страницы (флаг MEM_RELEASE) должен в точности соответствовать вызову VirtualAlloc, который резервировал память. Таким образом, при очередном уменьшении размера блока (или при его освобождении), менеджер памяти будет в состоянии вернуть системе хотя бы часть зарезервированных фрагментов, в которых располагался этот блок памяти.

Коротко опишем каждую из функций администратора выделенной памяти.

Выделяет область памяти размером не меньше minSize байт. Возвращает блок памяти, который был фактически выделен. В случае ошибки возвращает в поле addr блока памяти значение nil.

Выделяет область памяти по указанному адресу addr размером не меньше minSize байт. Возвращает блок памяти, который был фактически выделен. В случае ошибки возвращает в поле addr блока памяти значение nil.

Пытается вернуть физическую память по указанному адресу addr, но не более чем maxSize байт. Возвращает блок памяти, с которого была возвращена физическая память. Если память не освобождалась, функция возвратит в поле addr блока памяти значение nil.

Администратор используемой памяти



Алгоритм работы этого администратора, с одной стороны, является самым запутанным из всех, но, с другой стороны, и самым интересным. Он хранит информацию обо всей выделенной памяти и непосредственно общается с самим приложением Delphi, удовлетворяя его запросы. Для начала опишем все структуры данных, которые используются в нем. Все константы, типы и переменные, которые используются администратором выделенной памяти, представлены в листинге 5.

В выделенных фрагментах памяти блоки идут один за другим. Всего различают три типа блоков. Это используемые блоки, неиспользуемые блоки и заполнители. Администратор памяти оперирует с блоками, размеры которых выровнены по границе cAlign байт. Размер блока хранится в самом блоке памяти. Поскольку в настоящее время cAlign равен четырем, то, очевидно, два младших бита несущественны и могут использоваться как флаги. Третий флаг возникает при использовании самого старшего бита, что ограничивает размеры выделяемых блоков двумя гигабайтами. Формат размера приведен на рисунке 4.

Размер блока также включает в себя всю служебную информацию, которая хранится в блоке (например, если мы выделяем 99 байт памяти, то полученный размер блока будет равен 104 байтам, из них 1 байт уйдет на выравнивание и 4 байта на значение размера и флаги).

Опишем назначение каждого из флагов. Флаг cThisUsedFlag показывает, что данный фрагмент памяти используется приложением Delphi. Флаг cPrevFreeFlag указывает, что предыдущий фрагмент памяти свободен. Флаг cFillerFlag указывает на то, что данный фрагмент памяти является заполнителем.

Используемые блоки – это фрагменты памяти, выделенные приложением Delphi. Они имеют формат, показанный на рисунке 5. Мы видим, что четыре байта памяти, расположенные по отрицательному смещению от указателя, который вернула функция GetMem, являются размером блока (не забывайте, что размер блока содержит еще и флаги), остальная же его часть целиком и полностью находится в ведении приложения. Служебная часть используемого блока памяти описана в структуре TUsed.

Неиспользуемые фрагменты имеют формат, показанный на рисунке 6. Несмотря на то, что сумма всех составляющих блока равна 16 байтам, минимальный размер блока может быть равен (!) 12 байтам. В этом случае поле size1 накладывается на поле size2, и никакого искажения информации не происходит. Поскольку любой используемый блок памяти может стать впоследствии неиспользуемым, минимальный размер блока, выделенного менеджером памяти Delphi, равен 12 (или, с точки зрения приложения, 8) байтам.

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

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

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

Служебная часть неиспользуемого блока памяти, расположенная в начале блока, описана в структуре TFree. Служебная часть неиспользуемого блока памяти, расположенная в конце блока, описана в структуре TUsed.

Теперь о заполнителях и щелях. Виртуальное адресное пространство, не имеющее под собой физической памяти, и граничащее с выделенными менеджером памяти фрагментами, называется щелью (gap). Заполнитель – это специфический блок памяти, который располагается либо сразу после, либо непосредственно перед щелью. Выделенный фрагмент памяти обязательно заканчивается заполнителем. Таким образом, это дает возможность смело адресоваться к блоку, который расположен непосредственно за текущим, если только текущий блок не является заполнителем. Размер заполнителя – от четырех до двенадцати байт. Заполнитель имеет формат, показанный на рисунке 7. Как и в случае со свободным блоком, значения size1 и size2 могут накладываться друг на друга. Значение флагов cFillerFlag и cUsedBlock для заполнителя установлены в 1, а значение флага cPrevFree зависит от того, является ли предшествующий блок используемым. Начало и конец заполнителя описываются структурой TUsed. Работа с заполнителями организована таким образом, что всякий раз после выделения блока памяти (если он граничит с уже использующимися) происходит стирание недействительных заполнителей и, при необходимости, создание новых.

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

Массив указателей smallTab хранит указатели на двунаправленные списки, состоящие из неиспользуемых блоков небольшого размера. Наибольший размер, информация о котором хранится в этом массиве, равен cSmallSize байт, что в настоящее время составляет 4 Кб. Заметим также, что элементы массива smallTab являются только указателями на элементы двунаправленного кольцевого списка, но, в отличие от таких структур, как spaceRoot, decommittedRoot и committedRoot, не являются корневыми элементами. В случае, когда свободных элементов указанного размера нет, соответствующий указатель в массиве smallTab равен nil. В качестве связей в двунаправленном списке используются поля prev и next структуры TFree в начале неиспользуемого блока. Место под массив smallTab выделяется в локальной куче процесса. Первоначально все указатели содержат значения nil.

Переменная avail является корневым элементом двунаправленного кольцевого списка, который содержит фрагменты памяти, большие чем cSmallSize. Первоначально этот список не содержит элементов. Переменная rover (переводится как «скиталец») указывает на блок памяти, который был в последний раз добавлен к списку avail. Первоначально переменная rover указывает на avail.

Переменные curAlloc и remBytes содержат соответственно указатель на текущий выделяемый фрагмент и размер этого фрагмента. Что это за фрагмент? Всякий раз, когда администратор используемой памяти обращается к администратору выделенной памяти за новой порцией памяти, образуется довольно большой фрагмент неиспользуемой памяти. Этот фрагмент оформляется как текущий выделяемый фрагмент. При удовлетворении запросов приложения, первым делом производится поиск свободного фрагмента в точности требуемого размера. Если таковой не найден, то будет произведена попытка выделить память из текущего выделяемого фрагмента. Текущий выделяемый фрагмент может иметь и нулевую длину, если он, например, расположен непосредственно перед щелью. Текущий выделяемый фрагмент не содержится ни в одном из перечисленных выше списков. Чтобы поместить его в один из таких списков, используется функция SysFreeMem (!), т. е. данный блок первоначально оформляется как используемый программой, а затем функция SysFreeMem помещает его в один из существующих списков. Первоначально curAlloc равен nil, а remBytes – нулю.

Теперь мы в состоянии взглянуть на менеджер памяти Delphi в целом. Типичное его состояние приведено на рисунке 8. Перед тем, как рассмотреть алгоритмы выделения, перераспределения и удаления памяти, опишем назначение каждой функции, входящей в администратор используемой памяти.

Инициализирует менеджер памяти. В случае успеха возвращает True.

Освобождает всю память, занимаемую или выделенную менеджером памяти Delphi.

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

Находит элемент списка committedRoot, содержащий указанный адрес addr. В случае ошибки устанавливает переменную heapErrorCode в cBadCommittedList.

Вызывается для того, чтобы создать заполнитель перед фрагментом, не имеющим под собой физической памяти. В качестве параметров передаются: указатель на начало последнего фрагмента в блоке выделенной памяти a и его размер size. В случае необходимости вставляет новый неиспользуемый блок памяти.

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

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

Вызывается для уничтожения заполнителя в случае, когда выделяемый фрагмент памяти граничит снизу с уже выделенным фрагментом. В качестве параметра a передается указатель на начало вновь выделяемого фрагмента памяти (либо, что то же самое, указатель на конец ранее выделенного фрагмента). Возвращает размер неиспользуемого фрагмента памяти в конце ранее выделенного фрагмента. В случае ошибки устанавливает значение heapErrorCode в одно из следующих значений: cBadFiller1, cBadFiller2, cBadFiller3.

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

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

Добавляет новый свободный блок памяти, начинающийся по адресу a и занимающий size байт, в какой-либо из существующих списков в зависимости от размера блока, его местоположения и т. д.

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

Добавляет новый блок памяти b в список committedRoot. В случае «слияния» блоков, все заполнители, оказавшиеся в середине блока, уничтожаются. В случае успешного завершения возвращает True.

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

Запрашивает у администратора выделенной памяти фрагмент памяти размером не менее minSize байт, начинающийся по адресу addr, после чего вызывает для него функцию MergeCommit. В случае успешного завершения возвращает True.

Производит поиск наименьшего блока памяти, имеющего размер больше, чем size байт, в массиве smallTab. В случае удачного завершения возвращает указатель на начало такого блока. Если такого блока не оказалось, возвращает nil.

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

Реализация функции GetMem, предоставляемая менеджером памяти Delphi. Передаваемый параметр size – размер запрашиваемого блока. Возвращает выделенный блок памяти. Подробнее о работе этой функции, а также функции TryHarder смотри в разделе «Алгоритм выделения памяти».

Реализация функции FreeMem, предоставляемая менеджером памяти Delphi. Передаваемый параметр p – указатель на освобождаемый блок памяти. В случае удачного освобождения памяти возвращает cHeapOk (нуль). В случае ошибки возвращает ее код. Подробнее о работе этой функции можно прочитать в разделе «Алгоритм освобождения памяти».

Выполняет попытку перераспределения памяти по указанному адресу p без переноса существующего блока. В случае удачного завершения (когда удалось изменить размер блока на newSize) возвращает True.

Реализация функции ReallocMem, предоставляемая менеджером памяти Delphi. Передаваемые параметры – запрашиваемый размер блока и указатель на фрагмент памяти. Возвращает указатель на новый перераспределенный фрагмент памяти (или nil, если перераспределить память не удалось). Подробнее о работе этой функции, а также о функции ResizeInPlace, рассказано в разделе «Алгоритм перераспределения памяти».

Алгоритмы


Алгоритм выделения памяти

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

  1. Пытаемся найти в массиве smallTab блок в точности нужного размера.
  2. Пытаемся выделить память из текущего выделяемого фрагмента.
  3. Пытаемся найти блок подходящего размера в начале списка avail.
  4. Просматриваем все оставшиеся элементы в списке avail, начиная с элемента, на который указывает rover.
  5. Просматриваем массив smallTab, и, если в нем находится блок большего размера, используем его для выделения памяти.
  6. Выделяем память.
  7. Пытаемся выделить память из текущего выделяемого фрагмента. Поскольку при выделении памяти она автоматически добавляется в текущий выделяемый фрагмент, эта попытка является последней, несмотря на то, что формально (по тексту программы) стоит переход на пункт 3. Цикл repeat..until в теле функции TryHarder используется как метка оператора goto и выполняется не более одного раза.

Таким образом, при выделении памяти средствами стандартного менеджера памяти Delphi:

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

Алгоритм освобождения памяти

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

Итак, освобожденный блок помещается в один из следующих списков:

  1. Если он граничит с текущим выделяемым фрагментом, то добавляется к нему.
  2. Если его размер меньше, чем cSmallSize, он помещается в соответствующий список из массива smallTab.
  3. Блок помещается в список avail. Указатель rover устанавливается на него.

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

Алгоритм перераспределения памяти

Алгоритм перераспределения памяти, как и следовало ожидать, работает по-разному при уменьшении и увеличении размера фрагмента. Разберем алгоритмы раздельно. Алгоритм, который используется при уменьшении размера блока, представлен на рисунке 11, а алгоритм, который используется при увеличении размера блока — на рисунке 12. Если при расширении фрагмента не удалось произвести изменения размера по месту, будет выделен новый фрагмент нужного размера, и в него будет скопировано содержимое старого блока.

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

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

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

Delphi Winsock почему sockaddr_in значения получают «искаженные» при назначении InAddr.S_addr

Я нашел функцию , чтобы проверить , если конкретный порт TCP открыт на удаленном компьютере. Это , кажется, работает хорошо, но когда я присвоить значение переменной client.sin_addr.S_addr другие значения появляются быть назначены с искаженными значениями, ну по крайней мере я не ожидал увидеть значения как то , что я вижу.

Запись клиента значения до назначения client.sin_addr.S_addr :


Запись клиента значения после назначения client.sin_addr.S_addr :

Обратите внимание , что 1745529024 правильное представление IP — адреса 192.168.10.104 .

Функция работает, как и ожидалось, насколько я могу видеть. Я просто не люблю эти искаженные ценности.

Это нормально, я должен беспокоиться, или просто игнорировать его?

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

Десятичное значение 1745529024 шестнадцатеричное 0x680AA8C0. Старший байт является 0x68, который , когда интерпретируется как ASCII — символы в нижнем регистре ч . Значение 0x0A является линия подачи, представлена в виде символа Delphi буквального , как #10 . И так далее.

Это вариант записи, как C союз. Вот:

Таким образом, S_addr содержит значение 1745529024 , а остальные два члена накладываются друг на друга на одной и той же памяти. В шестнадцатеричном, 1745529024 есть $680AA8C0 .

Теперь, что такое SunB ?

Таким образом, отладчик интерпретирует четыре байта S_addr как закодированные ANSI символов, следовательно , то , что вы видите в отладчике. Посмотрите $68 , $0A , $A8 и $C0 в вашей локальной таблице ANSI , и вы найдете четыре значения , что отладчик показал вам.

Аналогично для SunW которых является:

И эти два слова , которые составляют $680AA8C0 действительно $A8C0 = 43200 и $680A = 26634 .

В целом, ситуация нормальная, ничего страшного.

DelphiComponent.ru — бесплатно видеоуроки по Delphi, статьи, исходники

Указатели в Delphi

Указатель — это всего лишь специальная переменная. В отличие от обычных переменных, которые хранят значение определенного типа, указатель хранит ад­рес ячейки памяти. Название «указатель» обусловлено тем, что указатель не со­держит конкретного значения, а указывает на ячейку памяти, в которой хранится 1 нужное значение.

В Delphi существует два типа указателей: типизированные и нетипизированные (общие). Типизированные указатели можно использовать только с перемен­ными конкретного типа, в то время как нетипизированные указатели могут указы­вать на любые данные.

Закажи видеокурс по Delphi и получи 106 видеоуроков. Детальное объяснение каждого момента программирования. Кликни на картинку:

ЗАКАЗАТЬ

Для объявления типизированного указателя перед идентификатором типа дан­ных потребуется поместить символ ^:

Указатель P может использоваться для указания на любую ячейку памяти, ко­торая содержит целочисленное значение.

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

Чтобы инициализировать указатель, ему следует присвоить адрес ячейки па­мяти. Для чтения адреса переменной можно использовать операцию @ или функ­цию Addr:

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

После того, как указателю присвоен адрес, указатель можно использовать для изменения значения переменной, на которую он указывает. Для получения досту­па к переменной, на которую указывает указатель, потребуется разыменовать ука­затель, записав символ ^после имени указателя, как продемонстрировано в лис­тинге 9.1.

Листинг 9.1. Использование простого типизированного указателя

01. program Project1;

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

Вторым типом указателя в Delphi является Pointer. Pointer представляет собой тип нетипизированного указателя, который может использоваться для указания на переменную любого типа данных. Чтобы работать со значением, на которое указы­вает нетипизированньил указатель, вначале нетипизированный указатель нужно привести к другому типу указателя и выполнить его разыменование. Приведение к другому типу указателя не представляет особой сложности, поскольку в Delphi каж­дый тип данных уже обладает соответствующим типом указателя. Например, указа­телем на переменную типа Char является PChar, указателем на строку — PString. указателем на целочисленную переменную — PInteger и так далее.

В листинге 9.2 демонстрируется использование типа Pointer и его приведение к другим типам указателей.

Листинг 9.2. Использование нетилизированного указателя

Программирование на языке Delphi. Глава 2. Основы языка Delphi. Часть 4

Оглавление


Файлы


Понятие файла

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

Для файла существует понятие текущей позиции . Она показывает номер элемента, который будет прочитан или записан при очередном обращении к файлу. Чтение-запись каждого элемента продвигает текущую позицию на единицу вперед. Для большинства файлов можно менять текущую позицию чтения-записи, выполняя прямой доступ к его элементам.

В зависимости от типа элементов различают три вида файла:

  • файл из элементов фиксированного размера; элементами такого файла чаще всего являются записи;
  • файл из элементов переменного размера ( нетипизированный файл ); такой файл рассматривается просто как последовательность байтов;
  • текстовый файл ; элементами такого файла являются текстовые строки.

Для работы с файлом в программе объявляется файловая переменная . В файловой переменной запоминается имя файла, режим доступа (например, только чтение), другие атрибуты. В зависимости от вида файла файловая переменная описывается по-разному.

Для работы с файлом, состоящим из типовых элементов переменная объявляется с помощью словосочетания file of , после которого записывается тип элемента:

К моменту такого объявления тип TPerson должен быть уже описан (см. выше).

Объявление переменной для работы с нетипизированным файлом выполняется с помощью отдельного слова file :

Для работы с текстовым файлом переменная описывается с типом TextFile:

Работа с файлами

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

Приступая к работе с файлом, нужно первым делом вызвать процедуру AssignFile, чтобы файловой переменной поставить в соответствие имя файла на диске:

В результате этого действия поля файловой переменной F инициализируются начальными значениями. При этом в поле имени файла заносится строка ‘MyFile.txt’.

Так как файла еще нет на диске, его нужно создать:

Теперь запишем в файл несколько строк текста. Это делается с помощью хорошо вам знакомых процедур Write и Writeln:

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

После работы файл должен быть закрыт:

Рассмотрим теперь, как прочитать содержимое текстового файла. После инициализации файловой переменной (AssignFile) файл открывается с помощью процедуры Reset:

Для чтения элементов используются процедуры Read и Readln, в которых первый параметр показывает, откуда происходит ввод данных. После работы файл закрывается. В качестве примера приведем программу, распечатывающую в своем окне содержимое текстового файла ‘MyFile.txt’:

Так как обычно размер файла заранее не известен, перед каждой операцией чтения вызывается функция Eof, которая возвращает True, если достигнут конец файла.

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

Стандартные подпрограммы управления файлами

Для обработки файлов в языке Delphi имеется специальный набор процедур и функций:

  • AssignFile (var F; FileName: string) — связывает файловую переменную F и файл, имя которого указано в FileName.
  • Reset (var F [: File; RecSize: Word ] ) — открывает существующий файл. Если открывается нетипизированный файл, то RecSize задает размер элемента файла.
  • Rewrite (var F [: File; RecSize: Word ] ) — создает и открывает новый файл.
  • Append (var F: TextFile) — открывает текстовый файл для добавления текста.
  • Read (F, V1 [, V2, . Vn ]) — начиная с текущей позиции, читает из типизированного файла подряд расположенные элементы в переменные V1, V2, . Vn.
  • Read (var F: TextFile; V1 [, V2, . Vn ] ) — начиная с текущей позиции, читает из текстового файла символы или строки в переменные V1, V2, . Vn.
  • Write (F, V1 [, V2, . Vn ]) — начиная с текущей позиции, записывает в типизированный файл значения V1, V2, . Vn.
  • Write (var F: TextFile; V1 [, V2, . Vn ] ) — начиная с текущей позиции указателя чтения-записи, записывает в текстовый файл значения V1, V2, . Vn.
  • CloseFile (var F) — закрывает ранее открытый файл.
  • Rename (var F; NewName: string) — переименовывает неоткрытый файл F любого типа. Новое имя задается в NewName.
  • Erase (var F) — удаляет неоткрытый внешний файл любого типа, заданный переменной F.
  • Seek (var F; NumRec: Longint) — устанавливает позицию чтения-записи на элемент с номером NumRec; F — типизированный или нетипизированный файл.
  • SetTextBuf (var F: TextFile; var Buf [; Size: Word]) — назначает текстовому файлу F новый буфер ввода-вывода Buf объема Size.
  • SetLineBreakStyle (var T: Text; Style: TTextLineBreakStyle) — устанавливает способ переноса строк в файле (одиночный символ #10 или пара символов #13#10).
  • Flush (var F: TextFile) — записывает во внешний файл все символы, переданные в буфер для записи.
  • Truncate (var F) — урезает файл, уничтожая все его элементы, начиная с текущей позиции.
  • IOResult : Integer — возвращает код, характеризующий результат (была ошибка или нет) последней операции ввода-вывода.
  • FilePos (var F): Longint — возвращает для файла F текущую файловую позицию (номер элемента, на которую она установлена, считая от нуля). Не используется с текстовыми файлами.
  • FileSize (var F): Longint — возвращает число компонент в файле F. Не используется с текстовыми файлами.
  • Eoln (var F: Text): Boolean — возвращает булевское значение True, если текущая позиция чтения-записи находится на маркере конца строки. Если параметр F не указан, функция применяется к стандартному устройству ввода с именем Input.
  • Eof (var F): Boolean — возвращает булевское значение True, если текущая позиция чтения-записи находится сразу за последним элементом, и False в противном случае.
  • SeekEoln (var F: Text): Boolean — возвращает True при достижении маркера конца строки. Все пробелы и знаки табуляции, предшествующие маркеру, пропускаются.
  • SeekEof (var F: Text): Boolean — возвращает значение True при достижении маркера конца файла. Все пробелы и знаки табуляции, предшествующие маркеру, пропускаются.

Для работы с нетипизированными файлами используются процедуры BlockRead и BlockWrite. Единица обмена

  • BlockRead (var F: File; var Buf; Count: Word [; Result: Word] ) — считывает из файла F определенное число блоков в память, начиная с первого байта переменной Buf. Параметр Buf представляет любую переменную, используемую для накопления информации из файла F. Параметр Count задает число считываемых блоков. Параметр Result является необязательным и содержит после вызова процедуры число действительно считанных записей. Использование параметра Result подсказывает, что число считанных блоков может быть меньше, чем задано параметром Count.
  • BlockWrite (var F: File; var Buf; Count: Word [; Result: Word]) — предназначена для быстрой передачи в файл F определенного числа блоков из переменной Buf. Все параметры процедуры BlockWrite аналогичны параметрам процедуры BlockRead.
  • ChDir (const S: string) — устанавливает текущий каталог.
  • CreateDir (const Dir: string): Boolean — создает новый каталог на диске.
  • MkDir (const S: string) — аналог функции CreateDir. Отличие в том, что в случае ошибки при создании каталога функция MkDir создает исключительную ситуацию.
  • DeleteFile (const FileName: string): Boolean — удаляет файл с диска.
  • DirectoryExists (const Directory: string): Boolean — проверяет, существует ли заданный каталог на диске.
  • FileAge (const FileName: string): Integer — возвращает дату и время файла в числовом системно-зависимом формате.
  • FileExists (const FileName: string): Boolean — проверяет, существует ли на диске файл с заданным именем.
  • FileIsReadOnly (const FileName: string): Boolean — проверяет, что заданный файл можно только читать.
  • FileSearch (const Name, DirList: string): string — осуществляет поиск заданого файла в указанных каталогах. Список каталогов задается параметром DirList; каталоги разделяются точкой с запятой для операционной системы Windows и запятой для операционной системы Linux. Функция возвращает полный путь к файлу.
  • FileSetReadOnly (const FileName: string; ReadOnly: Boolean): Boolean — делает файл доступным только для чтения.
  • FindFirst/FindNext/FindClose
  • ForceDirectories (Dir: string): Boolean — создает новый каталог на диске. Позволяет одним вызовом создать все каталоги пути, заданного параметром Dir.
  • GetCurrentDir : string — возвращает текущий каталог.
  • SetCurrentDir (const Dir: string): Boolean — устанавливает текущий каталог. Если это сделать невозможно, функция возвращет значение False.
  • RemoveDir (const Dir: string): Boolean — удаляет каталог с диска; каталог должен быть пустым. Если удалить каталог невозможно, функция возвращет значение False.
  • RenameFile (const OldName, NewName: string): Boolean — изменяет имя файла. Если это сделать невозможно, функция возвращет значение False.
  • ChangeFileExt (const FileName, Extension: string): string — возвращает имя файла с измененным расширением.
  • ExcludeTrailingPathDelimiter (const S: string): string — отбрасывает символ-разделитель каталогов (символ ‘/’ — для Linux и ‘\’ — для Windows), если он присутствует в конце строки.
  • IncludeTrailingPathDelimiter (const S: string): string — добавляет символ-разделитель каталогов (символ ‘/’ — для Linux и ‘\’ — для Windows), если он отсутствует в конце строки.
  • ExpandFileName (const FileName: string): string — возвращает полное имя файла (с абсолютным путем) по неполному имени.
  • ExpandUNCFileName (const FileName: string): string — возвращает полное сетевое имя файла (с абсолютным сетевым путем) по неполному имени. Для операционной системы Linux эта функция эквивалентна функции ExpandFileName.
  • ExpandFileNameCase (const FileName: string; out MatchFound: TFilenameCaseMatch): string — возвращает полное имя файла (с абсолютным путем) по неполному имени, допуская несовпадения заглавных и строчных букв в имени файла для тех файловых систем, которые этого не допускают (например, файловая система ОС Linux).
  • ExtractFileDir (const FileName: string): string — выделяет путь из полного имени файла; путь не содержит в конце символ-разделитель каталогов.
  • ExtractFilePath (const FileName: string): string — выделяет путь из полного имени файла; путь содержит в конце символ-разделитель каталогов.
  • ExtractRelativePath (const BaseName, DestName: string): string — возвращает относительный путь к файлу DestName, отсчитанный от каталога BaseName. Путь BaseName должен заканчиваться символом-разделителем каталогов.
  • ExtractFileDrive (const FileName: string): string — выделяет имя диска (или сетевого каталога) из имени файла. Для операционной системы Linux функция возвращает пустую строку.
  • ExtractFileExt (const FileName: string): string — выделяет расширение файла из его имени.
  • ExtractFileName (const FileName: string): string — выделяет имя файла, отбрасывая путь к нему.
  • IsPathDelimiter (const S: string; Index: Integer): Boolean — проверяет, является ли символ S[Index] разделителем каталогов.
  • MatchesMask (const Filename, Mask: string): Boolean — проверяет, удовлетворяет ли имя файла заданной маске.


Указатели


Понятие указателя

Все переменные, с которыми мы имели дело, известны уже на этапе компиляции. Однако во многих задачах нужны переменные, которые по мере необходимости можно создавать и удалять во время выполнения программы. С этой целью в языке Delphi организована поддержка так называемых указателей, для которых введен специальный тип данных Pointer.

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

Переменная P занимает 4 байта и может содержать адрес любого участка памяти, указывая на байты со значениями любых типов данных: Integer, Real, string, record, array и других. Чтобы инициализировать переменную P, присвоим ей адрес переменной N. Это можно сделать двумя эквивалентными способами:

В дальнейшем мы будем использовать более краткий и удобный второй способ.

Если некоторая переменная P содержит адрес другой переменной N, то говорят, что P указывает на N. Графически это обозначается стрелкой, проведенной из P в N (рисунок 12 выполнен в предположении, что N имеет значение 10):

Рисунок 12. Графическое изображение указателя P на переменную N

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

Символ ^ , записанный после имени указателя, называется оператором доступа по адресу . В данном примере переменной, расположенной по адресу, хранящемуся в P, присваивается значение 10. Так как в переменную P мы предварительно занесли адрес N, данное присваивание приводит к такому же результату, что и

Однако в примере с указателем мы умышленно допустили одну ошибку. Дело в том, что переменная типа Pointer может содержать адреса переменных любого типа, не только Integer. Из-за сильной типизации языка Delphi перед присваиванием мы должны были бы преобразовать выражение P^ к типу Integer:

Согласитесь, такая запись не совсем удобна. Для того, чтобы сохранить простоту и избежать постоянных преобразований к типу, указатель P следует объявить так:

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

PInteger — это указательный тип данных . Чтобы отличать указательные типы данных от других типов, будем назначать им идентификаторы, начинающиеся с буквы P (от слова Pointer). Объявление указательного типа данных является единственным способом введения указателей на составные переменные, такие как массивы, записи, множества и другие. Например, объявление типа данных для создания указателя на некоторую запись TPerson может выглядеть так:

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

Динамическое распределение памяти

После объявления в секции var указатель содержит неопределенное значение. Поэтому переменные-указатели, как и обычные переменные, перед использованием нужно инициализировать. Отсутствие инициализации указателей является наиболее распространенной ошибкой среди новичков. Причем если использование обычных неинициализированных переменных приводит просто к неправильным результатам, то использование неинициализированных указателей обычно приводит к ошибке «Access violation» (доступ к неверному адресу памяти) и принудительному завершению приложения.

Один из способов инициализации указателя состоит в присваивании ему адреса некоторой переменной соответствующего типа. Этот способ мы уже рассмотрели. Второй способ состоит в динамическом выделении участка памяти под переменную соответствующего типа и присваивании указателю его адреса. Работа с динамическими переменными и есть основное назначение указателей. Размещение динамических переменных производится в специальной области памяти, которая называется Heap (куча). Ее размер равен размеру свободной памяти компьютера.

Для размещения динамической переменной вызывается стандартная процедура

Она выделяет требуемый по размеру участок памяти и заносит его адрес в переменную-указатель P. В следующем примере создаются 4 динамических переменных, адреса которых присваиваются переменным-указателям P1, P2, P3 и P4:

Далее по адресам в указателях P1, P2, P3 и P4 можно записать значения:

В таком контексте динамические переменные P1^, P2^, P3^ и P4^ ничем не отличаются от обычных переменных соответствующих типов. Операции над динамическими переменными аналогичны подобным операциям над обычными переменными. Например, следующие операторы могут быть успешно откомпилированы и выполнены:

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

Например, в приведенной выше программе явно не хватает следующих строк:

После выполнения данных утверждений указатели P1, P2, P3 и P4 опять перестанут быть связаны с конкретными адресами памяти. В них будут случайные значения, как и до обращения к процедуре New. Не стоит делать попытки присвоить значения переменным P1^, P2^, P3^ и P4^, ибо в противном случае это может привести к нарушению нормальной работы программы.

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

Поэтому следует четко придерживаться последовательности действий при работе с динамическими переменными:

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

Операции над указателями

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

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

Использование одинаковых значений в разных указателях открывает некоторые интересные возможности. Так после оператора P3 := P1 изменение значения переменной P3^ будет равносильно изменению значения P1^.

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

Указатели можно сравнивать. Так уж сложилось, что понятие больше-меньше в адресации памяти разных моделей компьютеров может иметь противоположный смысл. Из-за этого операции сравнения указателей ограничены двумя: сравнение на равенство или неравенство.

Чаще всего операции сравнения указателей используются для проверки того, связан ли указатель с динамической переменной. Если еще нет, то ему следует присвоить значение nil (зарезервированное слово):

Установка P1 в nil однозначно говорит о том, что указателю не выделена динамическая память. Если всем объявленным указателям присвоить значение nil, то внутри программы можно легко выполнить тестирование наподобие этого:

Процедуры GetMem и FreeMem

Для динамического распределения памяти служат еще две тесно взаимосвязанные процедуры: GetMem и FreeMem. Подобно New и Dispose, они во время вызова выделяют и освобождают память для одной динамической переменной:

  • GetMem (var P: Pointer; Size: Integer) — создает в динамической памяти новую динамическую переменную c заданным размером Size и присваивает ее адрес указателю P. Переменная-указатель P может указывать на данные любого типа.
  • FreeMem (var P: Pointer [; Size: Integer] ) — освобождает динамическую переменную. Если в программе используется этот способ распределения памяти, то вызовы GetMem и FreeMem должны соответствовать друг другу. Обращения к GetMem и FreeMem могут полностью соответствовать вызовам New и Dispose.

Следующий отрывок программы даст тот же самый результат:

С помощью процедуры GetMem одной переменной-указателю можно выделить разное количество памяти в зависимости от потребностей. В этом состоит ее основное отличие от процедуры New.

В данном случае для указателя P4 выделяется меньше памяти, чем может уместиться в переменной типа ShortString, и программист сам должен обеспечить невыход строки за пределы выделенного участка.

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

ReallocMem (var P: Pointer; Size: Integer) — освобождает блок памяти по значению указателя P и выделяет для указателя новый блок памяти заданного размера Size. Указатель P может иметь значение nil , а параметр Size — значение 0, что влияет на работу процедуры:

  • если P = nil и Size = 0, процедура ничего не делает;
  • если P = nil и Size <> 0, процедура выделяет новый блок памяти заданного размера, что соответствует вызову процедуры GetMem.
  • если P <> nil и Size = 0, процедура освобождает блок памяти, адресуемый указателем P и устанавливает указатель в значение nil . Это соответствует вызову процедуры FreeMem, с той лишь разницей, что FreeMem не очищает указатель;
  • если P <> nil и Size <> 0, процедура перевыделяет память для указателя P. Размер нового блока определяется значением Size. Данные из прежнего блока копируются в новый блок. Если новый блок больше прежнего, то приращенный участок остается неинициализированным и содержит случайные данные.

Представление строк в памяти

В некоторых случаях динамическая память неявно используется программой, например для хранения строк. Длина строки может варьироваться от нескольких символов до миллионов и даже миллиардов (теоретический предел равен 2 ГБ). Тем не менее, работа со строками в программе осуществляется так же просто, как работа с переменными простых типов данных. Это возможно потому, что компилятор автоматически генерирует код для выделения и освобождения динамической памяти, в которой хранятся символы строки. Но что стоит за такой простотой? Не идет ли она в ущерб эффективности? С полной уверенностью можем ответить, что эффективность программы не только не снижается, но даже повышается.

Физически переменная строкового типа представляет собой указатель на область динамической памяти, в которой размещаются символы. Например, переменная S на самом деле представляет собой указатель и занимает всего четыре байта памяти (SizeOf(S) = 4):

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

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

Такой подход весьма эффективен как с точки зрения производительности, так и с точки зрения экономного использования оперативной памяти. Его главная проблема состоит в том, чтобы обеспечить удаление блока памяти, содержащего символы строки, когда все адресующие его строковые переменные прекращают свое существование. Эта проблема эффективно решается с помощью механизма подсчета количества ссылок (reference counting). Для понимания его работы рассмотрим формат хранения строк в памяти подробнее.

Пусть в программе объявлены две строковые переменные:

И пусть в программе существует оператор, присваивающий переменной S1 значение некоторой функции:

Для хранения символов строки S1 по окончании ввода будет выделен блок динамической памяти. Формат этого блока после ввода значения ‘Hello’ показан на рисунке 13:

Рисунок 13. Представление строковых переменных в памяти

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

Если в программе встречается оператор присваивания значения одной строковой переменной другой строковой переменной,

то, как мы уже сказали, копия строки в памяти не создается. Копируется только адрес, хранящийся в строковой переменной, и на единицу увеличивается количество ссылок на строку (рисунок 14).


Рисунок 14. Результат копирования строковой переменной S1 в строковую переменную S2

При присваивании переменной S1 нового значения (например, пустой строки):

количество ссылок на предыдущее значение уменьшается на единицу (рисунок 15).

Рисунок 15. Результат присваивания строковой переменной S1 нового значения (пустой строки)

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

Интересно, а что происходит при изменении символов строки, с которой связано несколько строковых переменных? Правила семантики языка требуют, чтобы две строковые переменные были логически независимы, и изменение одной из них не влияло на другую. Это достигается с помощью механизма копирования при записи (copy-on-write).

Например, в результате выполнения операторов

получим следующую картину в памяти (рисунок 16):

Рисунок 16. Результат изменения символа в строке S1

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

Все, что было сказано выше о представлении в памяти строк, относится только к строкам формата AnsiString. Строки формата WideString тоже хранятся в динамической памяти, но для них не поддерживаются механизм подсчета количества ссылок и механизм копирования по записи. Операция присваивания строковых переменных формата WideString означает выделение нового блока динамической памяти и полное копирование в него всех символов исходной строки. Что же касается коротких строк, то они целиком хранятся по месту объявления: или в области данных программы (если это глобальные переменные), или на стеке (если это локальные переменные). Динамическая память вообще не используется для хранения коротких строк.

Динамические массивы

Одним из мощнейших средств языка Delphi являются динамические массивы. Их основное отличие от обычных массивов заключается в том, что они хранятся в динамической памяти. Этим и обусловлено их название. Чтобы понять, зачем они нужны, рассмотрим пример:

Задать размер массива A в зависимости от введенного пользователем значения невозможно, поскольку в качестве границ массива необходимо указать константные значения. А введенное пользователем значение никак не может претендовать на роль константы. Иными словами, следующее объявление будет ошибочным:

На этапе написания программы невозможно предугадать, какие именно объемы данных захочет обрабатывать пользователь. Тем не менее, Вам придется ответить на два важных вопроса:

  • На какое количество элементов объявить массив?
  • Что делать, если пользователю все-таки понадобится большее количество элементов?

Вы можете поступить следующим образом. В качестве верхней границы массива установить максимально возможное (с вашей точки зрения) количество элементов, а реально использовать только часть массива. Если пользователю потребуется большее количество элементов, чем зарезервировано Вами, то ему можно попросту вежливо отказать. Например:

Такое решение проблемы является неоптимальным. Если пользователю необходимо всего 10 элементов, программа работает без проблем, но всегда использует объем памяти, необходимый для хранения 100 элементов. Память, отведенная под остальные 90 элементов, не будет использоваться ни Вашей программой, ни другими программами (по принципу «сам не гам и другому не дам»). А теперь представьте, что все программы поступают таким же образом. Эффективность использования оперативной памяти резко снижается.

Динамические массивы позволяют решить рассмотренную проблему наилучшим образом. Размер динамического массива можно изменять во время работы программы.

Динамический массив объявляется без указания границ:

Переменная DynArray представляет собой ссылку на размещаемые в динамической памяти элементы массива. Изначально память под массив не резервируется, количество элементов в массиве равно нулю, а значение переменной DynArray равно nil.

Работа с динамическими массивами напоминает работу с длинными строками. В частности, создание динамического массива (выделение памяти для его элементов) осуществляется той же процедурой, которой устанавливается длина строк — SetLength.

Изменение размера динамического массива производится этой же процедурой:

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

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

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

Определение количества элементов производится с помощью функции Length:

Элементы динамического массива всегда индексируются от нуля. Доступ к ним ничем не отличается от доступа к элементам обычных статических массивов:

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

Освобождение памяти, выделенной для элементов динамического массива, осуществляется установкой длины в значение 0 или присваиванием переменной-массиву значения nil (оба варианта эквивалентны):

Однако Вам вовсе необязательно по окончании использования динамического массива освобождать выделенную память, поскольку она освобождается автоматически при выходе из области действия переменной-массива (удобно, не правда ли!). Данная возможность обеспечивается уже известным Вам механизмом подсчета количества ссылок.

Также, как и при работе со строками, при присваивании одного динамического массива другому, копия уже существующего массива не создается.

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

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

Для работы с динамическими массивами вы можете использовать знакомую по строкам функцию Copy. Она возвращает часть массива в виде нового динамического массива.

Не смотря на сильное сходство динамических массивов со строками, у них имеется одно существенное отличие: отсутствие механизма копирования при записи (copy-on-write).

Нуль-терминированные строки

Кроме стандартных строк ShortString и AnsiString, в языке Delphi поддерживаются нуль-терминированные строки языка C, используемые процедурами и функциями Windows. Нуль-терминированная строка представляет собой индексированный от нуля массив ASCII-символов, заканчивающийся нулевым символом #0. Для поддержки нуль-терминированных строк в языке Delphi введены три указательных типа данных:

Типы PAnsiChar и PWideChar являются фундаментальными и на самом деле используются редко. PChar — это обобщенный тип данных, в основном именно он используется для описания нуль-терминированных строк.

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

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

переменная S3 получит адрес уже существующей строки ‘Object Pascal’.

Для удобной работы с нуль-терминированными строками в языке Delphi предусмотрена директива $EXTENDEDSYNTAX . Если она включена ( ON ), то появляются следующие дополнительные возможности:

  • массив символов, в котором нижний индекс равен 0, совместим с типом PChar;
  • строковые константы совместимы с типом PChar.
  • указатели типа PChar могут участвовать в операциях сложения и вычитания с целыми числами; допустимо также вычитание (но не сложение!) указателей.

В режиме расширенного синтаксиса допустимы, например, следующие операторы:

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

Переменные с непостоянным типом значений


Тип данных Variant

В среде Delphi определен стандартный тип данных Variant, с помощью которого объявляются переменные с непостоянным типом значений. Такие переменные могут принимать значения разных типов данных в зависимости от типа выражения, в котором используются. Следующий пример хорошо демонстрирует мощь переменных с непостоянным типом значений:

Значения переменных с типом Variant

Переменные с непостоянным типом содержат целые, вещественные, строковые, булевские значения, дату и время, массивы и др. Кроме того, переменные с типом Variant принимают два специальных значения: Unassigned и Null.

Значение Unassigned показывает, что переменная является нетронутой, т.е. переменной еще не присвоено значение. Оно автоматически устанавливается в качестве начального значения любой переменной с типом Variant.

Значение Null показывает, что переменная имеет неопределенное значение. Если в выражении участвует переменная со значением Null, то результат всего выражения тоже равен Null.

Переменная с типом Variant занимает в памяти 16 байт. В них хранятся текущее значение переменной (или адрес значения в динамической памяти) и тип этого значения.

Тип значения выясняется с помощью функции

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

Код типа


Значение


Описание


varEmpty $0000 Переменная содержит значение Unassigned. varNull $0001 Переменная содержит значение Null. varSmallint $0002 Переменная содержит значение типа Smallint. varInteger $0003 Переменная содержит значение типа Integer. varSingle $0004 Переменная содержит значение типа Single. varDouble $0005 Переменная содержит значение типа Double. varCurrency $0006 Переменная содержит значение типа Currency. varDate $0007 Переменная содержит значение типа TDateTime. varOleStr $0008 Переменная содержит ссылку на строку формата Unicode в динамической памяти. varDispatch $0009 Переменная содержит ссылку на интерфейс IDispatch (интерфейсы рассмотрены в главе 6). varError $000A Переменная содержит системный код ошибки. varBoolean $000B Переменная содержит значение типа WordBool. varVariant $000C Элемент варьируемого массива содержит значение типа Variant (код varVariant используется только в сочетании с флагом varArray). varUnknown $000D Переменная содержит ссылку на интерфейс IUnknown (интерфейсы рассмотрены в главе 6). varShortint $0010 Переменная содержит значение типа Shortint varByte $0011 Переменная содержит значение типа Byte. varWord $0012 Переменная содержит значение типа Word varLongword $0013 Переменная содрежит значение типа Longword varInt64 $0014 Переменная содержит значение типа Int64 varStrArg $0048 Переменная содержит строку, совместимую со стандартом COM, принятым в операционной системе Windows. varString $0100 Переменная содержит ссылку на длинную строку. varAny $0101 Переменная содержит значение любого типа данных технологии CORBA Флаги varTypeMask $0FFF Маска для выяснения типа значения. varArray $2000 Переменная содержит массив значений. varByRef $4000 Переменная содержит ссылку на значение.

Таблица 10. Коды и флаги варьируемых переменных

позволяет вам преобразовать значение варьируемой переменной к нужному типу, например:

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

Delphi + ассемблер

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

Встроенный ассемблер

Пользователю предоставляется возможность делать вставки на встроенном ассемблере в исходный текст на языке Delphi.

К встроенному ассемблеру можно обратиться с помощью зарезервированного слова asm , за которым следуют команды ассемблера и слово end :

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

В языке Delphi имеется возможность не только делать ассемблерные вставки, но писать процедуры и функции полностью на ассемблере. В этом случае тело подпрограммы ограничивается словами asm и end (а не begin и end ), между которыми помещаются инструкции ассемблера. Перед словом asm могут располагаться объявления локальных констант, типов, и переменных. Например, вот как могут быть реализованы функции вычисления минимального и максимального значения из двух целых чисел:

Обращение к этим функциям имеет привычный вид:

Подключение внешних подпрограмм

Программисту предоставляется возможность подключать к программе или модулю отдельно скомпилированные процедуры и функции, написанные на языке ассемблера или C. Для этого используется директива компилятора $LINK и зарезервированное слово external. Директива <$LINK >указывает подключаемый объектный модуль, а external сообщает компилятору, что подпрограмма внешняя.

Предположим, что на ассемблере написаны и скомпилированы функции Min и Max, их объектный код находится в файле MINMAX.OBJ. Подключение функций Min и Max к программе на языке Delphi будет выглядеть так:

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

Итоги

Все, что вы изучили, называется языком Delphi. Мы надеемся, что вам понравились стройность и выразительная сила языка. Но это всего лишь основа. Теперь пора подняться на следующую ступень и изучить технику объектно-ориентированного программирования, без которого немыслимо стать профессиональным программистом. Именно этим вопросом в рамках применения объектов в среде Delphi мы и займемся в следующей главе.

2.1.11. Передача данных при использовании TCP

2.1.11. Передача данных при использовании TCP

При программировании TCP и UDP применяются одни и те же функции, но их поведение при этом различно. Для передачи данных с помощью TCP необходимо сначала установить соединение, и после этого возможен обмен данными только с тем адресом, с которым это соединение установлено. Функция sendto может использоваться для TCP-сокетов, но ее параметры, задающие адрес получателя, игнорируются, а данные отправляются на тот адрес, с которым соединен сокет. Поэтому при отправке данных через TCP обычно прибегают к функции send , которая дает тот же результат. По тем же причинам обычно используется recv , а не recvfrom .

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

Для начала взаимодействия клиент должен соединиться с сервером с помощью функции connect . Мы уже знакомы с этой функцией, но в случае TCP она выполняет несколько иные действия. В данном случае она устанавливает реальное соединение, поэтому ее действия начинаются с проверки того, существует ли по указанному адресу серверный сокет, находящийся в режиме ожидания подключения. Функция connect завершается успешно только тогда, когда соединение установлено, и серверная сторона выполнила все необходимые для этого действия. При вызове connect в TCP предварительный явный вызов функции bind также не обязателен.

В отличие от UDP, сокет в TCP нельзя отсоединить или соединить с другим адресом, если он уже соединен. Для нового соединения необходим новый сокет.

Мы уже говорили, что TCP является надежным протоколом, т. е. в том случае, если пакет не доставлен, отправляющая сторона уведомляется об этом.

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

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

Функция recv копирует пришедшие данные из входного буфера сокета в буфер, заданный параметром Buf , но не более len байтов. Скопированные данные удаляются из буфера сокета. При этом все полученные данные сливаются в один поток, поэтому получатель может самостоятельно выбирать, какой объем данных считывать за один раз. Если за один раз была скопирована только часть пришедшего пакета, оставшаяся часть не пропадает, а будет скопирована при следующем вызове recv . Функция recv возвращает число байтов, скопированных в буфер. Если на момент ее вызова входной буфер сокета пуст, она ждет, когда там что-то появится, затем копирует полученные данные и лишь после этого возвращает управление вызвавшей ее программе. Если recv возвращает 0, это значит, что удаленный сокет корректно завершил соединение. Если соединение завершено некорректно (например, из-за обрыва кабеля или сбоя удаленного компьютера), функция завершается с ошибкой (т. е. возвращает SOCKET_ERROR ).

Теперь рассмотрим, какие действия при использовании TCP должен выполнить сервер. Как мы уже говорили, сервер должен перевести сокет в режим ожидания соединения. Это делается с помощью функции listen , имеющей следующий прототип:

function listen(s: TSocket; backlog: Integer): Integer;

Параметр s задает сокет, который переводится в режим ожидания подключения. Этот сокет должен быть привязан к адресу, т. е. функция bind должна быть вызвана для него явно. Для сокета, находящегося в режиме ожидания, создается очередь подключений. Размер этой очереди определяется параметром backlog , если он равен SOMAXCONN , очередь будет иметь максимально возможный размер. В MSDN отмечается, что узнать максимально допустимый размер очереди стандартными средствами нельзя. Функция возвращает ноль при успешном завершении и SOCKET_ERROR — в случае ошибки.

Когда клиент вызывает функцию connect , и по указанному в ней адресу имеется сокет, находящийся в режиме ожидания подключения, то информация о клиенте помещается в очередь подключений этого сокета. Успешное завершение connect говорит о том, что на стороне сервера подключение добавлено в очередь. Однако для того, чтобы соединение было действительно установлено, сервер должен выполнить еще некоторые действия: извлечь из очереди соединений информацию о соединении и создать сокет для его обслуживания. Эти операции выполняются с помощью функции accept , имеющей следующий прототип:

function accept(s: TSocket; addr: PSockAddr; addrlen: PInteger): TSocket;

Параметр s задает сокет, который находится в режиме ожидания соединения и из очереди которого извлекается информация о соединении. Выходной параметр addr позволяет получить адрес клиента, установившего соединение. Здесь должен быть передан указатель на буфер, в который этот адрес будет помещен. Параметр addrlen содержит указатель на переменную, в которой хранится длина этого буфера: до вызова функции эта переменная должна содержать фактическую длину буфера, задаваемого параметром addr , после вызова — количество байтов буфера, реально понадобившихся для хранения адреса клиента. Очевидно, что в случае TCP и входное, и выходное значение этой переменной должно быть равно SizeOf(TSockAddr) . Эти параметры передаются как указатели, а не как параметры-переменные, что было бы более естественно для Delphi, потому что библиотека сокетов допускает для этих указателей нулевые значения, если сервер не интересует адрес клиента. В данном случае разработчики модуля WinSock сохранили полную функциональность, предоставляемую библиотекой.

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

Исходный сокет, определяемый параметром s , остается в режиме прослушивания. Если сервер поддерживает одновременное соединение с несколькими клиентами, то функция accept может быть вызвана многократно. Каждый раз при этом будет создаваться новый сокет, обслуживающий одно конкретное соединение: протокол TCP и библиотека сокетов гарантируют, что данные, посланные клиентами, попадут в буферы соответствующих сокетов и не будут перемешаны.

Для получения целостной картины кратко повторим все сказанное. Для установления соединения сервер должен, во-первых, создать сокет с помощью функции socket , а во-вторых, привязать его к адресу с помощью функции bind . Далее сокет должен быть переведен в режим ожидания с помощью listen , а потом с помощью функции accept создается новый сокет, обслуживающий соединение, установленное клиентом. После этого сервер может обмениваться данными с клиентом. Клиент же должен создать сокет, при необходимости привязки к конкретному порту вызвать bind , и затем вызвать connect для установления соединения. После успешного завершения этой функции клиент может обмениваться данными с сервером. Это иллюстрируют листинги 2.11 и 2.12.

Delphi winsock Почему значения sockaddr_in становятся «искаженными» при назначении InAddr.S_addr

Я нашел функцию, чтобы проверить, открыт ли какой-либо порт tcp на удаленном ПК. Кажется, что все работает нормально, но когда я присваиваю значение переменной client.sin_addr.S_addr , другим значениям присваиваются искаженные значения, ну, по крайней мере, я не ожидал увидеть такие значения, как то, что я вижу.

Значения записи клиента перед назначением client.sin_addr.S_addr :

Значения записи клиента после назначения client.sin_addr.S_addr :

Обратите внимание, что 1745529024 является правильным представлением IP-адреса 192.168.10.104 .

Функция работает как ожидалось, насколько я могу видеть. Мне просто не нравятся эти искаженные значения.

Является ли это нормальным, я должен беспокоиться или просто игнорировать его?

Это вариантная запись, подобная C-соединению. Вот он:

Итак, S_addr содержит значение 1745529024 , а остальные два члена накладываются на одну и ту же память. В hex, 1745529024 есть $680AA8C0 .

Теперь, что такое SunB ?

Таким образом, отладчик интерпретирует четыре байта S_addr как кодированные ANSI символы, следовательно, то, что вы видите в отладчике. Посмотрите $68 , $0A , $A8 и $C0 в своей локальной таблице ANSI, и вы найдете четыре значения, которые показал вам отладчик.

Аналогично для SunW , который есть:

И два слова, составляющие $680AA8C0 , действительно $A8C0 = 43200 и $680A = 26634 .

Таким образом, ситуация нормальная, о чем не о чем беспокоиться.

Функции Delphi

Стандартные функции Delphi:

Для проведения всевозможных математических вычислений и многочисленных преобразований язык программирования Delphi содержит библиотеки стандартных процедур и функций. Давайте подробнее рассмотрим стандартные функции Delphi.

Между значением и именем функции существует зависимость. Поэтому всякая функция может быть представлена как операнд некоторого выражения (к примеру, в инструкции присваивания). Для возведения числа в n-ую степень достаточно записать

откуда ln — функция, вычисляющая натуральный логарифм числа exp(x), exp — функция, вычисляющая экспоненту в степени x, x — число, n-ую степень которого надо найти, а n — степень числа x. Каждая функция обладает следующими характеристиками: тип значений, тип параметров.

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

Математические функции Delphi:

Библиотеки языка Delphi включаются в себя и множество математических функций:

Величину угла при использовании тригонометрических функций необходимо выражать в радианах. Чтобы преобразовать угол из градусов в радианы, используйте следующую формулу:

где a выражает угол в градусах; 3.1415926 означает число pi. На месте константы 3.1415926 с дробной частью для достижения большей точности чаще всего пользуются стандартной именованной константой pi. Тогда выражения для угла в пересчете в радианы будет выглядеть следующим образом:


Функции преобразования Delphi:

Наиболее частое использование функций преобразования связано с инструкциями, которые обеспечивают ввод/вывод какой-либо информации. Например, для вывода значения переменной c типом real в поле вывода диалогового окна (компонент Label), нужно провести преобразование числа в строку символов, которая собственно изображает данное число. Это можно достичь, применяя функцию FloatToStr, которая заменяет значение выражения (оно указано как параметр функции) его строковым представлением.

Пример.

В приведенном примере значение переменной m будете выведено в поле Label. В таблице ниже Вам будут представлены основные функции преобразования Delphi:

Применение функций Delphi:

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

Примеры.

Структура функции Delphi

Как организована инструкция функции в языке Delphi? В любом языке программирования на первом этапе описания функции указывается ее заголовок. Далее за заголовком программист описывает раздел объявления констант const (если таковы имеются), затем занимается описанием раздела объявления типов type, далее следует раздел объявления переменных var и, наконец, раздел инструкций.

В приведенном примере в заголовке функции вначале указывается зарезервированное слово function, а следом идет имя функции. Далее в скобках программист перечисляет список параметров, и вслед за ним, используя символ «:», указывает тип значения функции. В конце каждого заголовка стоит символ «;». После заголовка следуют раздел констант, раздел типов, раздел переменных. Внутри раздела инструкций кроме констант и переменных, описанных соответственно в разделах const и var, может находится переменная result.

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

Addr — Функция Delphi

От Delphi 4 к Delphi 5

Во многих задачах используются переменные, которые создаются и уничтожаются во время выполнения программы. С этой целью в Object Pascal организована поддержка указателей, для которых введен специальный тип Pointer.

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

Указатели объявляются точно так же, как обычные переменные:

P: Pointer; <объявление переменной указателя >

K: Integer; <объявление переменной целого типа >

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

P := Addr(К); <инициализация переменной с помощью функции Addr>

Инициализацию можно провести вторым способом:

P^ :=@K; <инициализация с помощью оператора @>

Обычно используется более краткий и удобный второй способ. Таким образом, если некоторая переменная Р содержит адрес другой переменной К, то говорят, что Р указывает на К (рисунок 1) . Вы можете изменить значение переменной К, не прибегая к идентификатору K.

Можно изменить значение содержимого указателя, например, вместо предварительно определенного значения в Р адреса К, присвоим значение 10.

Если вы запишете:

то это будет ошибкой.

Из-за сильной типизации языка Object Pascal перед присваиванием, необходимо преобразовать выражение P^ к целому типу Integer. Объявим указатель P следующим образом:

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

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

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

Модифицируем предыдущий пример (рисунок 2):

Значение PInteger — указательный тип данных. Чтобы отличить указательные типы от других, лучше назначать им идентификаторы, начинающиеся с буквы P.

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

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

Переменная Р типа PScreen является указателем и может содержать адрес любой переменной типа TScreen.

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

После объявления в секции var указатель содержит неопределенное значение. Переменные-указатели, как и обычные переменные, перед использованием нужно инициализировать. Использование неинициализированных переменных приводит к неправильным результатам, а использование неинициализированных указателей приводит к ошибке Acces violation (доступ к неверному адресу памяти) и принудительному завершению приложения.

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

Второй способ состоит в динамическом выделении участка памяти под переменную соответствующего типа и присваивании указателю его адреса. Основное назначение указателей — работа с динамическими переменными.

Размещение динамических переменных производится в специальной области памяти, которая называется Heap (куча). Размер кучи равен размеру свободной памяти компьютера (рис 2.)

Для размещения динамической переменной (рисунок 3) вызывается стандартная процедура:

New(var P: Pointer);

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

List, P: PListEntry;

P^.Text := ‘Hello world’;

Новая процедура создает новую динамическую переменную и устанавливает переменную указателя, чтобы указать на это. P является переменной любого типа указателя. Размер размещенного блока памяти соответствует размеру типа, на который P указывает. Вновь созданная переменная может ссылаться на P^. Если там недостаточно памяти пригодной для размещения динамической переменной, то генерируется исключение EOutOfMemory .

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

P^ := ‘Мой любимый язык Delphi. ‘;

После выполнения процедуры Dispose указатель P перестает быть связан с конкретным адресом памяти. В нем будет случайное значение, как и до обращения к процедуре New. Не стоит делать попытки присвоить значение переменной Р, в противном случае это может привести к нарушению нормальной работы программы. Поэтому необходимо четко придерживаться последовательности работы с динамическими переменными:

создание динамической переменной;

выполнение с переменной необходимых действий;

разрушение динамической переменной.

Рассмотрим пример, показывающий работу указателей .

2 X, Y: Integer; // X и Y переменные целого типа

3 P: ^Integer; // P указатель на данные целого типа

5 X := 17; // присвоение значения 17 для X

6 P := @X; // определение адреса X в P

7 Y := P^; /разыменование Р; помещение результата в Y

Строка 2 объявляет X и Y как переменные целого типа. Строка 3 объявляет P как указатель данных целого типа; это означает, что P может указать на позицию X или Y. Строка 5 определяет величину X, и строка 6 назначает адрес X (обозначается @X) в P. Наконец, строка 7 извлекает величину в позиции, указанной на P (обозначается ^P), и присваивает это значение Y. После того, как выполнится программный код, X и Y будут иметь одну и ту же величину 17.


Если вы добавите на форму два компонента Edit1, Edit2 и напишете следующий код

то вы можете убедиться, что значения X и Y одинаковы и равны 17.

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

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

Наш пример может показаться подобным тому, что мы окольным путем копировали целую величину одной переменной, присвоили это значение другой величине, в то время когда это можно произвести обычным оператором присваивания. Но указатели полезны по различным причинам. Вначале, поняв назначение указателя, вы можете лучше понимать язык программирования Object Pascal . Любой тип данных, который требует большие объемы, динамически распределяемой памяти, использует указатели. Кроме того, некоторые программы требуют обязательного использования указателей.

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

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

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

В Object Pascal имеется ряд предопределенных типов указателей. Это, прежде всего, типы указателей на строки: PAnsiChar и PWideChar , представляющие собой соответственно указатели на значения AnsiChar и WideCha r. Имеется также родовой тип PChar , определяющий указатель на Char (т.е. пока указывающий на AnsiChar ). Эти указатели используются при работе со строками с нулевым символом в конце.

PAnsiString, Pstring — типы переменной, на которую указывает указатель AnsiString.

PByteArray тип переменной, на которую указывает указатель ByteArray (объявлен в модуле SysUtils ). Используется для доступа к динамически размещаемым массивам.

PCurrency тип переменной, на которую указывает указатель Currency.

PExtended тип переменной, на которую указывает указатель Extended.

POleVariant тип переменной, на которую указывает указатель OleVariant.

PShortString тип переменной, на которую указывает указатель ShortString.

PTextBuf тип переменной, на которую указывает указатель TextBuf (объявлен в модуле SysUtils ). Внутренний тип буфера в записи файлов TTextRec .

PVarRec тип переменной, на которую указывает указатель TVarRec (объявлен в модуле System).

PVariant тип переменной, на которую указывает указатель Variant.

PWideString тип переменной, на которую указывает указатель WideString.

PWordArray т ип переменной, на которую указывает указатель TWordArray (объявлен в модуле SysUtils ). Используется для доступа к динамически размещаемым массивам 2-байтных величин.

Объявление своего типизированного указателя на любой тип имеет вид:

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

Чтобы получить доступ к данным, на которые указывает типизированный указатель, надо применить операцию его разыменования. Она записывается с помощью символа ^ , помещаемого после указателя.

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

Указатели PChar и PWideChar.

При работе со строками с нулевым символом в конце часто используют специальные типы указателей: PChar и PWideChar, которые являются указателями соответственно на массивы с элементами типов Char и WideChar с нулевым символом в конце.

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

Это эквивалентно операторам

const SHello: array[0…7] of Char = ‘Привет!’#0;

согласитесь, первая запись гораздо короче.

Совместимость указателей типов PChar и PWideChar со строковыми константами позволяет в вызовах функций и процедур с параметрами соответствующих типов использовать просто строки символов.

Например, если некоторая процедура F требует параметр типа PChar, то обратиться к ней можно так:

Указатели типов PChar и PWideChar могут индексироваться точно так же, как строки и массивы символов. Например, P[0] — это первый символ строки, на которую указывает P.

В выражениях, операторах присваивания и при передаче параметров в функции и процедуры можно смешивать длинные строки (AnsiString) и строки с нулевым символом в конце типа PChar. Но иногда требуется явное приведение типа PChar к типу длинной строки. Например, операция склеивания (конкатенации) строк требует, чтобы хотя бы один ее операнд был типа строки.

Если же требуется склеить два объекта типа PChar, то это надо сделать с помощью приведения типа:

S := string(P1) + string(P2);

Когда вы объявляете константу указатель, вы должны проинициализировать эту величину. Есть три пути, чтобы сделать это: с @ оператором, с нулем и (если константа будет типом PChar) с литералом строки. Например, имеется глобальная переменная целого типа, вы можете объявить константу следующим образом:

const PI: ^Integer = @I;

Компилятор может решить эту проблему, поскольку глобальные переменные являются частью кодового сегмента. Так, функции и глобальные константы объявляются следующим образом:

const PF: Pointer = @MyFunction;

Поскольку литералы строки определены как глобальные константы, вы можете проинициализировать PChar константу с литералом строки:

const WarningStr: PChar = ‘Warning!’;

Процедуры GetMem и FreeMem.

GetMem(var P: Pointer [;Size: Integer]) — создает в динамической памяти новую динамическую переменную с заданным размером Size и присваивает ее адрес указателю P.

Переменная указателя P может указывать на данные любого типа.

FreeMem(var P: Pointer [; Size: Integer]) — освобождает динамическую переменную.

Если в программе используется этот способ распределения памяти, то вызовы GetMem и FreeMem должны соответствовать друг другу.

GetMem(P4, SizeOf(ShortString)); <выделить блок памяти для P4>

FreeMem(P4); <освободить блок памяти >

С помощью процедуры GetMem одной переменной-указателю можно выделить разное количество памяти в зависимости от потребностей, в этом состоит ее основное отличие от процедуры New.

В данном случае для указателя P4 выделяется меньше памяти, чем может уместиться в переменной типа ShortString, и программист сам должен предотвратить выход строки за пределы выделенного участка.

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

Для этого предназначена следующая процедура:

ReallocMem(var P: Pointer; Size: Integer) — освобождает блок памяти по значению указателя Р и выделяет для указателя новый блок памяти заданного размера Size.

Указатель P может иметь значение nil, а параметр Size — значение 0, что влияет на работу процедуры:

если P = nil и Size = 0, процедура ничего не делает;

если P = nil и Size <> 0, процедура выделяет новый блок памяти заданного размера, что соответствует вызову процедуры GetMem;

если P <> nil и Size = 0, процедура освобождает блок памяти, адресуемый указателем P, и устанавливает указатель в значение nil; это соответствует вызову процедуры FreeMem, с той лишь разницей, что FreeMem не очищает указатель;

если P <> nil и Size <> 0, процедура переопределяет память для указателя P; размер нового блока определяется значением Size. Данные из прежнего блока копируются в новый блок; если новый блок больше прежнего, то приращенный участок остается неинициализированным и содержит случайные данные.

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


    1. Запустите Delphi.
    2. Сохраните файл модуля под именем ImageView_pas, а файл проекта под именем ImageView. dpr.
    3. Поместите на форму компонент Image со страницы Additional палитры компонентов.
    4. Поместите на форму компонент OpenDialog со страницы Dialogs палитры компонентов.
    5. Поместите на форму компонент BitButton со страницы Additional палитры компонентов. Вы уже знаете, как придать данной кнопке более красивый вид, можете проделать это. Для обработчика события щелчка по кнопке OnClick напишите следующий программный код:

    procedure TForm1.BitBtn1Click(Sender: TObject);

    if OpenDialog1.Execute then

    Image1.Top:= Form1.ClientRect.Top + (Form1.ClientHeight — Image1.Height)

    Image1.Left:= Form1.ClientRect.Left + (Form1.ClientWidth — Image1.Width)

  1. Для того чтобы изображения разных размеров были в окне полностью видимы, необходимо установить для компонента Image1 свойство AutoSize в True. А приведенные выше операторы позволят размещать изображение по центру формы. В этом программном коде размеры клиентской области устанавливаются несколько больше размеров компонента Image1, которые в свою очередь адаптируются к размеру изображения благодаря свойству AutoSize.
  2. Для того чтобы просматривать только необходимые вам изображения, используя свойство Filter компонента OpenDialog, определите следующие значения. Например, запишите в разделе Filter Mame *.bmp;*.ico;*.wmf, в разделе Filter запишите *.bmp;*.ico;*.wmf (рисунок 4). На рисунке 5 показана процедура поиска необходимого изображения. На рисунке 6 показан результат работы программы ImageView.

Если у вас установлена версия Delphi 5, то на странице палитры компонентов Dialogs имеется компонент OpenPictureDialog, который вызывает окно открытия и предварительного просмотра изображений. Если вы посмотрите свойство Filter этого компонента (рисунок 7), то вы увидите, что за вас поработали и определили, какие файлы вы можете просматривать. Добавьте на форму компонент Image и Button и напишите следующий программный код:

procedure TForm1.Button1Click(Sender: TObject);

if OpenPictureDialog1.Execute then

Осуществляя поиск необходимого изображения, в Delphi 5 диалоговое окно открытия файлов содержит окно предварительного просмотра изображений.

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

procedure TForm1.Image1Progress(Sender: TObject; Stage: TProgressStage;

PercentDone: Byte; RedrawNow: Boolean; const R: TRect;

const Msg: String);

Параметр Stage содержит состояние процесса загрузки (psStarting — начало, psRunning — идет загрузка, psEnding — процесс завершен ). Параметр PercentDone приблизительно указывает процент выполненной работы.

С помощью параметра RedrawNow Windows сообщает, нужно ли сейчас выполнить прорисовку части изображения. Этот параметр имеет смысл, только если свойство IncrementalDisplay компонента содержит True. Параметр R – прямоугольник, нуждающийся в прорисовке. Параметр Msg содержит одно или более слов, уточняющих состояние процесса.

Обычно в обработчике события по сигналу psStarting создается индикатор процесса типа TProgressBar, по сигналам psRunning изменяется позиция индикатора, а в момент psEnding индикатор уничтожается.

Событие OnProgress создается только при загрузке некоторых типов изображений, например, подготовленных в формате JPEG (Joint Photographic Except Group — объединенная группа фотографических экспертов). Пример использования события OnProgress вы можете найти в папке Borland \ Delphi \ Help \ Examples \ JEPG. На рисунке 8 представлен результат демонстрационной работы программы Ipegproi.

  1. Марко Канту. Delphi 2 для Windows 95/NT. Москва. ООО «Малип». 1997г.
  2. Джон Матчо. Дэвид Р. Фолкнер. Delphi. Москва. БИНОМ. 1995г.
  3. Эндрю Возневич. Delphi. Освой самостоятельно. Москва. Восточная книжная компания. 1996г.
  4. К. Сурков, Д. Сурков, А. Вальвачев. Программирование в среде Delphi 2.0.

ООО «Попурри» 1997 г.

  • В.В.Фаронов. Delphi 5. Учебный курс. Москва. Издательство Нолидж. 2000г.
  • А. Я. Архангельский. Программирование в Delphi 5. Москва. ЗАО «Издательство Бином». 2000г.
  • Упаковка сложных типов данных в Delphi

    В этой статье я хотел бы рассказать и показать особенности использования
    «уплотнения сложных типов данных» в Delphi.

    Все когда-нибудь имели дело с записями. Записи используются во многих случаях
    – для создания файловых справочников, удобной группировки и представления
    данных, в системном программировании и т.д. Например, открыв файл Windows.pas
    (стандартный модуль, подключается в разделе uses), можно найти что-то типа
    такого описания:

    _POINTL = packed record
    x: Longint;
    y: Longint;
    end;

    TISHMisc = packed record
    case Integer of //Здесь используется запись с вариантами
    0: (PhysicalAddress: DWORD);
    1: (VirtualSize: DWORD);
    end;

    Что же здесь обозначает ключевое слово packed? Слово Packed говорит Delphi
    минимизировать память. Так что же получается, без этого слова у нас структура
    занимает памяти больше?

    Вот пример записи:

    TRecord = Record
    pole1 : byte;
    pole2 : string[4];
    pole3 : integer;
    pole4 : Int64;
    end;

    Давайте подсчитаем ее размер: pole1 – 1 байт, pole2 – 5 байт (надеюсь, не
    забыли 4 байта под символы и 1 под размер), pole3 – 4 байта, pole4 – 8 байт.
    Если их сложить получиться 18 байт. Давайте проверим:

    size1 := sizeof(TRecord);
    ShowMessage(‘Размер обычной записи = ’+IntToStr(size1));

    Удивлены? А теперь добавьте слово packed перед словом Record… Размер 18 как и
    подсчитали ранее. Так куда же у нас пропали целые 6 байт?! А если у нас будет
    массив из 10 000 000 таких записей, например в каком-нибудь справочнике, мы
    потеряем около 57 Мегабайт?

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

    procedure TForm1.Button1Click(Sender: TObject);
    type
    // обычная запись
    TRecord = Record
    pole1 : byte;
    pole2 : string[4];
    pole3 : integer;
    pole4 : Int64;
    end;
    // упакованная записи
    TPackedRecord = Packed record
    pole1 : byte;
    pole2 : string[4];
    pole3 : integer;
    pole4 : Int64;
    end;

    var
    Rec : TRecord;
    packedRec : TPackedRecord;
    size1 : integer;
    begin
    ShowMessageFmt(‘Adress = %P’,[Addr(Rec)]); //Вывод адреса нашей записи
    size1 := sizeof(Rec); //Узнаем размер
    // присваиваем некоторые данные нашей записи
    Rec.pole1 := $FF;
    Rec.pole2 :=’hack’;
    Rec.pole3 := $AAAAAAAA;
    Rec.pole4 := $1122334455667788;
    ShowMessage(‘Размер структуры = ‘ + IntToStr(size1)); //выводим размер
    end;

    Функцию ShowMessageFmt используем для того, чтобы вывести адрес нашей
    структуры. Обычный ShowMessage не поможет, т.к. нам надо вывести указатель (тип
    Pointer). Этот адрес меняется, так что у вас может быть другой. Функция Addr –
    это получение адреса, возвращаемый тип Pointer. Вот ее результат:

    Запомним этот адрес. Теперь поставим breakpoint на последнем ShowMessage –
    нажмем F5 на этой сроке.

    Теперь запустим. Программа выдаст сообщение с адресом, а затем остановиться
    на предпоследней строке. Нажимаем Ctrl + Alt + C. Появится окно дизассемблера.
    Щелкаем правой кнопкой мыши по левой-нижней области (dump памяти) и выбираем
    «Идти к адресу».

    Вводим наш адрес записи (например так: $0012F568), который получили с помощью
    ShowMessageFmt, и попадаем на адрес начала нашей структуры.

    Помним, что pole1 мы присвоили значения $FF (т.е. =255), pole2 =’hack’, pole3
    = $AAAAAAAA (4 байта, т.е. =2863311530) и т.д. Посмотрим на рисунок, на нем я
    отобразил структуру байтов и как они расположены в памяти.

    Как видим, после данных pole2 идут байты выравнивания, которые выравнивают
    границу памяти до 8 байт. И поэтому Pole3 начинается с адреса кратному 8. То же
    самое видим и с Pole4 – сначала выравнивается граница (байты 0E 00 00 00), а
    затем идут сами данные (хочу заметить, что в памяти данные хранятся в обратном
    порядке, что видно в Pole4).

    А теперь проверим упакованную запись. Переменную Rec заменяем в коде на
    packedRec, т.е. используем packed record. И проделываем все то же самое:

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

    Возможно возник вопрос: почему в первом примере данные выравнивались по
    границе 8 байт? Все дело в настройках компилятора. Если вы откроете Project –
    Option – Compiler (опции компилятора), то там будет списочек Record field
    alignment, в котором можно выбрать нужное выравнивание (8,4,2 или 1 байт). Не
    забываем перекомпоновать проект после изменений. Кстати выравнивание по 1 байту
    и есть упаковка данных.

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

    Кстати, в том же файле Windows.pas можно найти не только упаковка записей, но
    и массивов, вот пример:

    CprMask: packed array[0..3] of DWORD;

    Попробуйте сами проанализировать упаковку этого массива.

    Илон Маск рекомендует:  Как узнать сеpийный номеp, тип ide винта
    Понравилась статья? Поделиться с друзьями:
    Кодинг, CSS и SQL