Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учебное пособие 700383.doc
Скачиваний:
15
Добавлен:
01.05.2022
Размер:
4.33 Mб
Скачать

9.6.3. Макроязыки и препроцессоры

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

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

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

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

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

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

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

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

Рассмотрим пример на языке С. Если описана функция

int f1 (int a) { return a + а; }

и аналогичная ей макрокоманда

#define f2(a) ((a) + (а))

то результаты их вызова не всегда будут одинаковы.

Действительно, вызовы j = f1(i) и j = f2(i) (где i и j - некоторые целочисленные переменные) приведут к одному и тому же результату. Но вызовы j = f1(++i) и j = f2(++i) дадут разные значения переменной j. Дело в том, что поскольку f2 - это макроопределение, то во втором случае будет выполнена текстовая подстановка, которая приведет к последовательности операторов j = ( (++i) + (++i)). Вид­но, что в этой последовательности операция ++i будет выполнена дважды, в отличие от вызова функции f1(++i), где она выполняется только один раз.