CFA LogoCFA Logo Computer
Новости Статьи Магазин Драйвера Контакты
Новости
RSS канал новостей
В конце марта компания ASRock анонсировала фирменную линейку графических ускорителей Phantom Gaming. ...
Компания Huawei продолжает заниматься расширением фирменной линейки смартфонов Y Series. Очередное ...
Компания Antec в своем очередном пресс-релизе анонсировала поставки фирменной серии блоков питания ...
Компания Thermalright отчиталась о готовности нового высокопроизводительного процессорного кулера ...
Компания Biostar сообщает в официальном пресс-релизе о готовности флагманской материнской платы ...
Самое интересное
Программаторы 25 SPI FLASH Адаптеры Optibay HDD Caddy Драйвера nVidia GeForce Драйвера AMD Radeon HD Игры на DVD Сравнение видеокарт Сравнение процессоров

АРХИВ СТАТЕЙ ЖУРНАЛА «МОЙ КОМПЬЮТЕР» ЗА 2003 ГОД

Мысли о Паскале

Владислав ДЕМЬЯНИШИН nitromanit@mail.ru

Продолжение, начало см. в МК №46, 51-52, 4, 6-7, 10, 12-13, 16-18, 22, 24, 29, 34, 41, 46, 4, 6, 17, 21 (165, 170-171, 175, 177-178, 181, 183-184, 187-189, 193, 195, 200, 205, 212, 217, 227, 229, 240, 244).

Распределение памяти для локальных переменных

В системе Turbo Pascal существует три вида памяти: сегмент данных, стек и хип. Сегмент данных не может превышать 64 Кб, в нем размещаются все глобальные переменные и типизированные константы. Размер сегмента данных устанавливает сам компилятор при компиляции программы исходя из суммарного размера памяти, занимаемой глобальными переменными программы, в т.ч. глобальными переменными используемых модулей. Если общий размер всех переменных, описанных в глобальном блоке программы и типизированных констант, превышает 64 Кб, компилятор выдаст сообщение Error 96: Too many variables («слишком много переменных»).

О хипе (heap) я расскажу позднее. Стек — это область данных, размер которой не должен превышать тех же 64 Кб и может устанавливаться директивой $M. Минимально допустимый размер стека — 1 Кб, а по умолчанию он равен 16 Кб. Устанавливая размер стека, следует учитывать то, что модуль SYSTEM требует под свои нужды целых 522 байта стека, следовательно, если установлен размер стека 1 Кб, то из них реально доступны только 1024–522=502 байта.

В предыдущих статьях я уже упоминал о том, что локальные переменные размещаются в стеке, и что нужно быть осторожным при использовании рекурсивных алгоритмов. Что же происходит при вызове подпрограммы? Если подпрограмма при вызове должна получить параметры, то они помещается в стек. Затем выполняется машинная команда вызова подпрограммы (Call). Эта команда помещает в стек адрес машинной команды (4 байта = сегмент : смещение, если длинный вызов Far; или 2 байта = смещение в пределах текущего сегмента памяти, если вызов ближний —Near), следующей за командой вызова, чтобы при возвращении из подпрограммы извлечь из стека адрес команды, которой предшествовал вызов на процедуру, и продолжить выполнение программы. Сразу после выполнения машинной команды вызова подпрограммы начинается выполнение команд этой подпрограммы, но предварительно отводится область стека необходимого размера для локальных переменных и, если установлена директива {$S+} (включена по умолчанию), выполняется системная процедура проверки переполнения стека. Если же эта директива отключена ({$S–}), то никаких проверок производиться не будет, что даст небольшой прирост производительности программы, но может привести к краху системы, при котором машина сможет отреагировать лишь на нажатие всеми любимой кнопки Reset. Поэтому, если все же есть необходимость такой рискованной оптимизации, совсем не обязательно отключать проверку стека для кода всей программы. Сделайте это только для процедур, которые необходимо оптимизировать — их можно поместить в конструкцию директив {$S–}…{$S+}. После завершения выполнения кода подпрограммы перед возвращением управления производится освобождение отведенной области стека. Потом из стека извлекается адрес команды, следующей за вызовом подпрограммы, которой следует передать управление. В зависимости от типа вызова процедуры извлекается адрес длиной 4 байта (при вызове Far) или 2 байта (Near). Управление передается команде, следующей за командой вызова.

С вызовом подпрограммы, кажется, все понятно. Теперь подробно рассмотрим вопрос, касающийся отведения памяти стека под параметры и локальные переменные. При описании интерфейса подпрограммы следует учитывать тот факт, что все машинные команды, осуществляющие работу со стеком (занесение и извлечение данных) являются 16-разрядными, следовательно, могут оперировать сразу двумя байтами. Таким образом, если процедура или функция получает параметр типа word или integer, то в стеке под такой параметр будет отведено 2 байта, что вполне логично. Если один из параметров имеет тип longint, single или pointer, передан по ссылке или является бестиповым, то в стеке отводится под этот параметр 4 байта, что тоже вполне логично. Что касается параметров, имеющих размер более 4 байт, то под них в стеке выделяется область, размер которой округлен до большего числа, кратного двум, т.е. если вы пожелали передать процедуре параметр-значение в виде массива размером 10001 байт (array [0..10000] of byte), то размер отводимой области будет равен 10002 байтам. Такие затраты памяти можно считать вполне допустимыми. Но есть один нюанс, касающийся параметров типа byte, char и shortint. Ввиду того, что в стек нельзя поместить однобайтное значение, под каждый параметр размером 1 байт в стеке отводится 2 байта. Т.е. если подпрограмма получает два параметра типа byte, в стеке вместе они будут занимать 4 байта, т.е. по 2 байта каждый. Пока это все, что касается параметров.

Теперь насчет отведения области стека под локальные переменные. Все, что касается переменных, имеющих размер 2 и более байт, выполняется в соответствии со сказанным выше. Что же касается однобайтных переменных, например, типа byte, то если таких в описании Var указано подряд несколько, в стеке будет отведено по одному байту на каждую переменную. Если же в описании Var описана только одна переменная типа byte или дополнительно к списку таких переменных еще одна переменная описана отдельно, например:

то под такую переменную (в данном случае D) будет отведено 2 байта, что тоже логично, но неэкономично. А под три переменные A, B и C будет отведено 4 байта, так как A и B будут размещены как одно слово стека, а для C пары нет, и она займет еще 2 байта. Поэтому в данном случае целесообразнее описать переменные так:

что приведет к эффективному расходованию стека, так как теперь на каждую из переменных A, B, C и D будет в стеке отведено по одному байту.

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

Процедурные типы

Очень важным и удобным расширением языка Turbo Pascal является возможность описания процедурного типа. Процедурный тип — это не что иное как обыкновенный указатель, который предназначен для хранения адреса подпрограммы, а не адреса области некоторых данных. Т.е. такая переменная будет занимать 4 байта и содержать адрес в виде смещения и сегмента, указывающий на начало кода подпрограммы. Можно описать переменную процедурного типа и присвоить ей адрес некоторой процедуры, чтобы затем, пользуясь идентификатором этой переменной, вызывать подпрограмму, адрес которой она содержит. Например:

В данном примере объявлен тип TMyFunc и переменная MyFunc этого типа, которой можно присвоить адрес функции с соответствующим интерфейсом, указанным в описании типа TMyFunc, т.е. функция должна иметь два входных параметра с типом Real и возвращать результат типа Real. Имена входных параметров могут быть произвольными и могут не совпадать по имени с указанными в описании типа. Этому условию соответствуют функции Imul и Idiv. Таким образом, конструкция MyFunc:=Idiv интерпретируется компилятором не как присвоение результата функции Idiv переменной MyFunc, а как присвоение адреса функции Idiv указателю MyFunc. Только после такой инициализации процедурного указателя MyFunc его можно использовать, например, в конструкции writeln(MyFunc(10,20)), что будет эквивалентно конструкции writeln(Idiv(10,20)). Аналогично можно поступать и с функцией Imul. При этом адрес подпрограммы можно передавать из одной переменной процедурного типа в другую —MyFunc2:=MyFunc — и после этого применять конструкцию writeln(MyFunc2(10,20)). Корректна будет также конструкция MyFunc3:=MyFunc, так как по количеству и типу входных параметров, а также типу возвращаемого результата процедурные типы TMyFunc и TMyFunc2, от которых образованы переменные MyFunc3 и MyFunc, ничем не отличаются, а значит, являются совместимыми по присваиванию.

На примере простого (нетипизированного) указателя P хочу продемонстрировать приведение типа указателя к процедурному типу и таким образом объяснить смысл процедурных типов. В конструкции P:=@Imul происходит определение адреса функции Imul посредством операции взятия адреса @ и занесение его в нетипизированный указатель P. Такая конструкция вполне корректна, как если бы она выглядела так:

Но так как указатель P бестиповый, то его нельзя применить как процедурный указатель writeln(MyFunc(10,20)), но можно использовать так:

что соответствует правилам приведения типа. (см. «Преобразование типов. Совместимость типов. Явные преобразования», МК №6—7(177—178) за 18.02.02). Допустимы и конструкции MyFunc:=TMyFunc(P), TMyFunc(P):=MyFunc и @MyFunc:=P.

Аналогично может быть объявлена переменная указателя на процедуру без параметров MyProc, которой может быть присвоен адрес процедуры MyHalt при помощи конструкции MyProc:=MyHalt.

Для того чтобы подпрограмма могла использоваться в конструкциях присвоения переменной процедурного типа, необходимо, чтобы такая подпрограмма была оформлена с дальним вызовом при помощи служебного слова Far или была заключена в конструкцию директив {$F+}…{$F–}. Иначе компилятор выдаст сообщение Error 143: Invalid procedure or function reference («неправильная ссылка на подпрограмму»).

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

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

должна быть объявлена с дальним вызовом;

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

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

не может быть описана как обработчик прерывания при помощи служебного слова interrupt и не должна быть inline-подпрограммой.

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

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

Другое дело, когда условный оператор имеет вид:

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

В данном примере оператор @MyFunc преобразует MyFunc в нетипизированный указатель, содержащий адрес, а оператор @Idiv возвращает адрес подпрограммы.

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

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

(Продолжение следует )

Рекомендуем ещё прочитать:






Данную страницу никто не комментировал. Вы можете стать первым.

Ваше имя:
Ваша почта:

RSS
Комментарий:
Введите символы или вычислите пример: *
captcha
Обновить





Хостинг на серверах в Украине, США и Германии. © sector.biz.ua 2006-2015 design by Vadim Popov