Результат будет одинаковым для большинства распространенных машин:
char 1, short 2, int 4, long 4, float 4, double 8, void* 4
однако возможны и другие значения. Некоторые 64-битовые машины покажут такие значения:
char 1, short 2, int 4, long 8, float 4, double 8, void* 8
а ранние компиляторы под PC показали бы такое:
char 1, short 2, int 2, long 4, float 4, double 8, void* 2
Во времена появления PC аппаратура поддерживала несколько видов указателей. Для того чтобы справиться с такой неразберихой, были придуманы модификаторы указателей far и near, ни один из которых не входит в стандарт, но до сих пор эти ключевые слова-призраки появляются во многих компиляторах. Если ваш компилятор может менять размеры базовых типов или если вы имеете доступ к машинам, поддерживающим другие размеры, постарайтесь скомпилировать и оттестировать вашу программу при таких новых для нее условиях.
Стандартный заголовочный файл stddef. h определяет ряд типов, которые могут помочь с переносимостью. Наиболее часто используемый из них — size_t, который представляет собой тип беззнакового целого, возвращаемого оператором sizeof. Значения этого типа возвращаются функциями типа strlen и во многих функциях, включая mall ос, используются в качестве аргументов.
Наученная чужим горьким опытом, Java четко определяет размеры всех своих базовых типов: byte — 8 битов, char и short — 16, int — 32 и long — 64 бита.
Мы не будем рассматривать набор проблем, связанных с вычислениями с плавающей точкой, потому что разговор об этом достоин отдельной книги. Скажем только, что большинство современных машин поддерживают стандарт IEEE на аппаратуру для плавающей точки, и, следовательно, для них свойства арифметики с плавающей точкой определены достаточно четко.
Порядок вычислений. В С и C++ порядок вычислений операндов выражений, побочных эффектов и аргументов функций не определен. Например, в присваивании
? n = (getcharQ « 8) getchar();
второй getcha r может быть вызван первым: порядок, в котором выражение записано, не обязательно соответствует порядку, в котором оно исполняется.
В этом фрагменте, вообще говоря,

В этом фрагменте, вообще говоря, лучше было бы использовать tfelif после каждого определения — тогда бы не было такого ненужного скопления tfendif в конце. Однако главная проблема вовсе не в этом, а в том, что, несмотря на все старания, код плохо переносим, потому что он ведет себя по-разному в разных системах, а для работы в каждой новой системе должен быть дополнен новым Sifdef. Одна-единственная строка с унифицированным текстом (но на самом деле столь же информативным) была бы гораздо удобнее, проще и переносимее:
char *astring = "convert to local text format";
Для этой строки никаких условий не нужно, она будет выглядеть одинаково во всех системах.
Смешивание управляющей логики времени компиляции (определяемой выражениями «if def) и времени исполнения приводит к еще более трудно воспринимаемому коду:
Даже будучи явно безопасной, условная

Даже будучи явно безопасной, условная компиляция может быть заменена более простым кодом. Например, tfifdef часто используют для управления отладочным кодом:
? ftifdef DEBUG
? printf(...);
? tfendif
однако обычное выражение if с константой в условии может делать то же самое:
Если DEBUG есть ноль, то

Если DEBUG есть ноль, то большинство компиляторов не сгенерируют для приведенного фрагмента никакого кода, но при этом они еще и проверят синтаксис. Секция с #if def, наоборот, может содержать синтаксические ошибки, которые сорвут компиляцию, как только соответствующее условие #if def окажется выполнено.
Иногда условия компиляции содержат большие блоки кода:
tfifdef notdef /* неопределенный символ */ ttendif
или
#if О
tfendif
В таком случае этот код лучше вынести в отдельные файлы, которые будут подключаться при компиляции при соблюдении определенных условий. К этой теме мы еще вернемся в следующем разделе.
Начиная адаптировать программу к новой среде, не делайте копии всей программы, а перерабатывайте исходный код. Скорее всего, вам придется вносить изменения в основное тело программы, и при редактировании копии вы через какое-то время получили бы новую, отличающуюся от исходной версию. Изо всех сил стремитесь к тому, чтобы у вас существовала единственная версия программы; при необходимости подстроиться под конкретную систему старайтесь вносить изменения таким образом, чтобы они работали во всех системах. Измените внутренние интерфейсы, если надо, но не нарушайте целостности кода; не пытайтесь решить проблему с помощью #if def. При таком подходе каждое изменение сделает вашу программу все более переносимой, а не более специализированной. Сужайте пересечение, а не расширяйте объединение.
Мы уже привели много доводов против использования условной компиляции, но не упоминали еще о главной проблеме: ее практически невозможно оттестировать. Каждое выражение tfif def разделяет всю программу на две по отдельности компилируемые программы, и определить, все ли возможные варианты программ были скомпилированы и проверены, очень сложно. Если в один блок tfifdef было внесено изменение, то может статься, что изменить надо и другие блоки, но проверить эти изменения можно будет только в той среде, которая вызовет эти tfifdef к исполнению. Точно так же, если мы добавляем блок #if def, то трудно изолировать это изменение, то есть определить, какие еще условия должны быть учтены и в каких еще местах должен быть изменен код. Наконец, если некий блок кода должен быть опущен в соответствии с условием, то компилятор его просто не видит, и проверить этот блок можно только в соответствующей конфигурации. Вот небольшой пример подобной проблемы — программа компилируется, если _МАС определено, и отказывается это делать в противном случае:
мы предпочитаем использовать только те

Итак, мы предпочитаем использовать только те возможности, которые присутствуют во всех средах, где будет исполняться программа. Мы всегда можем скомпилировать и протестировать весь код. Если что-то вызывает проблемы с переносимостью, мы переписываем этот кусок, а не добавляем условно компилируемый код; таким образом, переносимость все время улучшается.
Некоторые большие системы распространяются с конфигурационными скриптами, которые помогают приспособить код к локальной среде. Во время компиляции скрипт проверяет возможности среды: расположение заголовочных файлов и библиотек, порядок байтов внутри слов, размер типов, уже известные неверные реализации функций (таких на удивление много) и т. п. — и генерирует параметры настройки или make-файлы (makefile), которые описывают нужные настройки для данной ситуации. Эти скрипты могут быть большими и сложными, они являются важной частью дистрибутивного пакета и требуют постоянной поддержки. Иногда такие сложные способы оказываются полезны, но все же, чем переносимее будет ваш код и чем меньше #if def будет в нем использова-' но, тем проще и безопаснее будет происходить его настройка и установка.
При каких обстоятельствах компилятор проверяет

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

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

На 32- битовом старшеконечнике на экран будет выведено
11 22 33 44
на младшеконечнике —
44 33 22 11
а на PDP-11 (16-битовая машина, все еще встречающаяся во встроенных системах) результатом будет
22 11 44 33
На машинах с 64-битовым типом long мы можем рассмотреть константу большей длины и увидеть те же результаты.
Наша программа выглядит довольно глупо, но если нам надо послать целое число через побайтовый интерфейс, например по сети, то надо решить, какой байт посылать первым, а какой вторым, и в этом выборе суть проблемы со старше- и младшеконечниками. Другими словами, наша программа неявнр выполняет то, что выражение
fwrite(&x, sizeof(x), 1, stdout);
делает явным образом. Небезопасно писать (отправлять) int (или short, или long) на одном компьютере и читать это число как int на другом.
Например, если компьютер-передатчик пишет с помощью
unsigned short x;
fwrite(&x, sizeof(x), 1, stdout);
а компьютер-приемник производит чтение так:
unsigned short x;
fread(&x, sizeof(x), 1, stdin);
то, если эти компьютеры имеют разный порядок байтов, значение х будет воспроизведено неправильно. Например, если отправлено было число 0x1000, то прочитано оно будет как 0x0010.
Эта проблема часто решается посредством условной компиляции и перестановки байтов, то есть примерно так:
При пересылке большого количества двух-

При пересылке большого количества двух- и четырехбайтовых целых такой подход получается слишком громоздким. На практике получает- ; ся, что байты при пересылке не один раз подвергаются перестановке.
Если ситуация выглядит невесело для short, то для более длинных типов она еще хуже — для них существует больше способов перепутать байты. А если к этому добавить еще всяческие преобразования между членами структур, ограничения на выравнивание и абсолютно таинственный порядок байтов в старых машинах, то проблема покажется 4 просто неразрешимой.
Используйте при обмене данными фиксированный порядок байтов.
Решение проблемы все же существует. Записывайте байты в каноническом порядке, используя переносимый код:
и считывайте их обратно побайтово,

и считывайте их обратно побайтово, собирая первоначальные значения
с EOF его значение останется

Когда getchar возвратит EOF, в s[i] будет сохранено значение 255 (OxFF, результат преобразования -1 в unsigned char). Если s[i] беззнаковое, то при сравнении с EOF его значение останется 255, и, следовательно, проверка не пройдет.
Однако, даже если char является знаковым типом, код все равно некорректен. В этом случае сравнение с EOF будет проходить нормально, но при вводе вполне допустимого значения OxFF оно будет воспринято как EOF и цикл будет прерван. Так что вне зависимости от того, знаковый или беззнаковый у вас char, хранить значение, возвращаемое getcha r, вы должны в int, и тогда проверка на конец файла будет осуществляться нормально. Вот как должен выглядеть наш цикл в переносимом виде:
что можно сказать про побайтовую
Главное, что можно сказать про побайтовую обработку: она решает имеющуюся проблему для всех машин с 8-битовыми байтами, причем решает без участия flifdef. Мы еще вернемся к этой теме в следующей главе.
Итак, лучшим решением нередко оказывается преобразование информации в текстовый формат, который (не считая проблемы CRLF) является абсолютно переносимым: не существует никаких неопределенностей с его представлением. Однако и текстовый формат не является панацеей. Время и размер для некоторых программ могут быть критичны, кроме того, часть данных, особенно числа с плавающей точкой, могут потерять точность при округлениях в процессе передачи через printf и scanf. Если вам надо передавать числа с плавающей точкой, особо заботясь о точности, убедитесь, что у вас есть хорошая библиотека форматированного ввода-вывода; такие библиотеки существуют, просто ее может не оказаться конкретно в вашей системе. Особенно сложно представлять числа с плавающей точкой для переноса их в двоичном формате, но при должном внимании текст может эту проблему решить.
Существует один тонкий момент, связанный с использованием стан-| дартных функций для обработки двоичных файлов, — эти файлы необходимо открывать в двоичном режиме:
в определенной последовательности, побайтово, без

Этот подход можно перенести и на структуры: записывайте значе-'' ния членов структур в определенной последовательности, побайтово, без выравнивания. Неважно, какой порядок байтов вы изберете, — подойдет любой, лишь бы передатчик и приемник согласовали порядок байтов в передаче и количество байтов в каждом объекте. В следующей главе мы покажем несколько инструментов для упаковки и распаковки данных.
Побайтовая обработка может показаться довольно дорогим удовольствием, но в сравнении с вводом-выводом, при котором необходима упаковка и распаковка данных, потери времени окажутся незначительными. Представьте себе систему X Window, в которой клиент пишет данные с тем порядком байтов, что используется у него на машине, а сервер должен распаковывать все, что клиент посылает. На клиентском конце достигнута экономия в несколько инструкций, но сервер становится больше и сложнее, поскольку ему приходится обрабатывать одновременно данные с разными порядками байтов: ведь клиенты вполне могут иметь различные типы машин. В общем, сложностей куда больше. Кроме того, не забывайте, что это графическая оболочка, где расходы на распаковку байтов будут с лихвой перекрыты выполнением той графической операции, которую они содержат.
Система X Window воспринимает данные от клиента с любым порядком байтов: считается, что это забота сервера. А система Plan 9, наоборот, сама задает порядок байтов для отправки сообщений файл-серверу (или графическому серверу), а данные запаковываются и распаковываются неким универсальным кодом вроде приведенного выше. На практике определить временные затраты во время исполнения оказывается практически невозможно; можно только сказать, что на фоне ввода-вывода упаковка данных оказывается малозаметной.
Java — язык более высокого уровня, чем С и C++, в нем порядок байтов скрыт совсем. Библиотеки представляют интерфейс Serializable, который определяет, как элементы данных пакуются для передачи.
Однако, если вы пишете на С или C++, всю работу придется выполнять самостоятельно.
в большинстве систем Unix это

Если ' b' опущено, то в большинстве систем Unix это ни на что не повлиЯ яет, но в системах Windows первый встретившийся во вводе байт 1 Control-Z (восьмеричный 032, шестнадцатеричный 1А) прервет чтение! (такое происходило у нас с программой st rings из главы 5). В то же вре- j мя при использовании двоичного режима для чтения текстовых файлов; вам придется вставлять символы \ r во ввод и убирать их из вывода.
<
После передачи контрольная сумма не

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

Непонятно, произошел сбой в передаче или просто мы столкнулися разными версиями sum. Может быть и то, и другое.
Таким образом, sum являет собой яркий пример препятствия на пути! переносимости: программа, призванная помогать в копировании фай-] зв с одной машины на другую, имеет несколько несовместимых версий,! го делает ее абсолютно непригодной для использования.
Для выполнения изначально поставленной задачи первая версия sum] юлнелодходила: алгоритм, заложенный в ней, был не самым эффектив-1 JM, но приемлемым. Ее "улучшение", может, и сделало собственно ко-анду лучше, но зато использовать ее по назначению стало нельзя. И деля 'т, надо сказать, не в том, что получилось несколько разных по существу! зманд, а в том, что все эти команды имеют одно и то же имя. Как видите! юблема несовместимости версий может оказаться весьма серьезной.
Поддерживайте совместимость с существующими программами и данными. Когда выпускается новая версия программы, например текстового редактора, то она, как правило, умеет читать файлы, созданньЯ эедыдущей версией. При этом мы ожидаем, что из-за добавления новых возможностей формат должен измениться. Но зачастую новые версии оказываются не в состоянии обеспечить способ записи в предыдущем формате. Пользователи новых версий, даже если они не обращаются к добавленным возможностям, не могут применять свои файлы совместно с пользователями более старой версии, таким образом, обновлять программы приходится сразу всем. Независимо от того, чем определяются такие решения — технической необходимостью или маркетинговой политикой, — о таких случаях можно только сожалеть.
Совместимостью сверху вниз называется возможность программы соответствовать спецификациям своих более ранних версий. Если вы собираетесь изменить свою программу, убедитесь, что при этом вы не создадите противоречий со старыми версиями и связанными с ними данными. Тщательно документируйте изменения и продумайте способ восстановить первоначальные возможности. И главное, задумайтесь над тем, перевесят ли достоинства предлагаемых вами усовершенствований потери от непереносимости, которые при этом возникнут.
<
В языке Java вообще нет

В языке Java вообще нет спецификатора unsigned; порядковые типы данных являются знаковыми, а тип char (16-битовый) — беззнаковым.
Арифметический или логический сдвиг. Сдвиг вправо знаковых величин с помощью оператора » может быть арифметическим (при сдвиге распространяется копия знакового бита) или логическим (при сдвиге освободившиеся биты заполняются нулями). И здесь Java, наученная горьким опытом С и C++, резервирует » для арифметического сдвига вправо и предоставляет отдельный оператор >» для логического сдвига вправо.
Порядок байтов. Порядок байтов внутри short, int и long не определен; байт с младшим адресом может быть как наиболее значимым, так и наименее значимым. Этот вопрос зависит от аппаратуры, и мы подробно обсудим его несколько ниже в этой главе.
Выравнивание членов структуры или класса. Расположение элементов внутри структур, классов и объединений (union) не определено, утверждается лишь, что члены располагаются в порядке объявления. Например, в структуре
может находиться на расстоянии

адрес 1 может находиться на расстоянии 2, 4 или 8 байтов от начала структуры. Некоторые (немногие) машины позволяют целым храниться на нечетных границах, но большинство требует, чтобы п-байтовые элементарные типы данных хранились на и-байтовых границах, чтобы, например, double, которые имеют, как правило, длину в 8 байтов, хранились по адресам, кратным 8. В дополнение к этому создатели компиляторов могут вносить и свои ограничения — такие как принудительное выравнивание для повышения производительности.
Никогда не рассчитывайте на то, что элементы структуры занимают смежные области памяти. Ограничения на выравнивание вызывают появление "дыр" в структурах — так, st ruct X всегда будет содержать по крайней мере один байт неиспользуемого пространства. Из-за этих дыр размер структуры может быть больше, чем сумма размеров ее членов, причем этот размер может быть разным на разных машинах. Если вы хотите зарезервировать память под структуру, всегда запрашивайте sizeof (struct X) байтов, ноне sizeof (char) + sizeof(int).
Битовые поля. Битовые поля настолько зависят от конкретных машин, что никому не следует их использовать.
Все перечисленные опасные места можно миновать, следуя нескольким правилам. Не используйте побочные эффекты нигде, кроме как в идиоматических конструкциях типа
с EOF. Всегда используйте sizeof

Не сравнивайте char с EOF. Всегда используйте sizeof для вычисления размера типов и объектов. Никогда не сдвигайте вправо знаковые значения. Убедитесь, что тип данных достаточно велик для диапазона значений, которые вы собираетесь в нем хранить.
Попробуйте несколько компиляторов. Иногда вам может показаться, что вы решили все проблемы с переносимостью, однако компиляторы в состоянии увидеть проблемы, который не вы заметили, и вообще, раз-, ные компиляторы воспринимают вашу программу по-разному, и этим; можно воспользоваться. Включите все предупреждения компилятора. Попробуйте использовать разные компиляторы на одной машине и на разных машинах. Попытайтесь компилировать вашу С-программу на компиляторе C++.
Поскольку язык, воспринимаемый различными компиляторами, может несколько отличаться от стандарта, тот факт, что ваша программа компилируется одним компилятором, не дает гарантии даже того, чтс она корректна синтаксически. А вот если несколько компиляторов принимают ваш код, значит, все не так плохо. Мы компилировали каждув программу, приведенную в книге, на трех компиляторах С для трех различных операционных систем (Unix, Plan 9, Windows) и на паре компиляторов C++. При таком подходе были найдены десятки ошибок переносимости — никакое самое пристальное изучение программ чeлoвeкoм не смогло бы найти их все. Исправлялись же все ошибки тривиально.
Естественно, сами компиляторы тоже вызывают проблемы с переносимостью из-за разного толкования не описанных в стандарте случаев. Однако наш подход — писать программы, избегая применения деталей, которые могут варьироваться, позволяет надеяться на создание программ, работающих вне зависимости от внешних условий.
Даже из этого сравнительно простого

Даже из этого сравнительно простого примера видно, что заголовочные файлы и программы, структурированные подобным образом, получаются довольно запутанными и сопровождать их достаточно сложно. Возможно, проще использовать отдельный заголовочный файл для каждого компилятора или среды. При этом придется сопровождать множество отдельных файлов, но каждый из них будет предназначен только для использования в конкретной конфигурации, что уменьшит вероятность появления ошибок вроде включения функции st rdup в среду, строго поддерживающую стандарт ANSI С.
Заголовочные файлы также иногда "засоряют" пространство имен, определяя функции с именами, уже использующимися в программе. Например, наша функция оповещения об ошибках wep rintf изначально называлась wp rintf, однако мы выяснили, что в некоторых средах функция с таким именем определена в stdio. h (можно сказать, что сделано это в преддверии нового стандарта С). Для того чтобы скомпилировать программу в этих средах и защитить себя в будущем, нам пришлось изменить название своей функции. Если бы проблема состояла в некорректном компиляторе, а не в ожидаемом изменении спецификации, как в нашем случае, то ее можно было бы решить, переопределяя имя при подключении заголовочного файла:

Этот фрагмент изменяет все появления wprintf в заголовочном файле на stdio_wp rintf, так что теперь они не повлияют на нашу версию. Теперь мы можем использовать нашу wprintf, не изменив ее имени, правда, при этом неизбежно появится некая путаница, а также риск, что подключенная библиотека будет вызывать нашу wprintf, подра-
зумевая обращение к своей версии. Для одной функции проблемы, может, и невелики, но уже для нескольких лучше придумать более радикальное решение. Всегда комментируйте назначение конструкции; без крайней необходимости не ухудшайте ее добавлением условной компиляции. Если в некоторых средах определена wprintf, то стоит считать, что она определена во всех; тогда единственный разумный выход — переименовать ее, избавившись при этом от выражения tfifdef. Нередко проще не превозмогать трудность, а подстраиваться под нее; да это и безопаснее, вот почему мы решили переименовать свою функцию в weprintf.
Даже если вы следуете всем правилам и неясностей со средой не возникает, все равно вполне возможно появление ошибок. Так, можно ошибиться, предположив, что какая-нибудь ваша излюбленная возможность одинакова во всех системах. К примеру, ANSI С определяет шесть сигналов, которые можно поймать с помощью signal, в стандарте POSIX их определено 19, а большинство"систем Unix поддерживает 32 и более. Если вы хотите использовать сигнал, отличный от описанного в ANSI С, вам придется выбирать между функциональностью и переносимостью, так что сами решайте *ITO для вас важнее.
Существует болыпсте количество других стандартов, не являющихся частью определения языка: среди них можно назвать интерфейсы операционных систем и сетей, графические интерфейсы и тому подобные вещи. Некоторые стандарты распространяются на несколько систем — например POSIX; другие определены исключительно для одной системы, например различные API Microsoft Windows. Здесь можно еще раз повторить наши главные советы: ваша программа станет более переносимой, если вы выберете самые распространенные и устоявшиеся стандарты и будете пользоваться самыми важными и общепринятыми их свойствами.
<>
Защитный стиль приемлем, если он

Защитный стиль приемлем, если он применяется время от времени,; а не всегда. Возникает резонный вопрос: а для скольких еще функций из stdlib. h придется писать аналогичный код? В частности, если вы собираетесь использовать mall ос и real loc, то явно потребуется еще и free. А что, если тип unsigned int нетождественен size_t — правильному типу аргумента для malloc и realloc? Более того, откуда мы знаем, что STOC_HEADERS и _LIBC определены, и определены корректно? Можем ли мы быть уверенными в том, что не существует другого имени, которое потребует замены для другой среды? Любой условный код вроде этого неполон, а значит — непереносим, поскольку рано или поздно встретится система, не удовлетворяющая его условию, и тогда придется редактировать #ifdef. Если нам удастся решить задачу без помощи условной компиляции, мы избавимся и от проблем, связанных с дальнейшим поддержанием этого кода.
Итак, проблема, которая решается в рассмотренном примере, существует в реальности. Так как же нам решить ее раз и навсегда? На самом деле нам просто надо предположить, что стандартные заголовочные файлы присутствуют во всех системах всегда; если одного из них нет, то это уже не наши проблемы. Но мы можем решить и их; для данного случая достаточно вместе с программой поставить и заголовочный файл, который определяет malloc, realloc и free в точности так, как этого требует стандарт ANSI С. Такой файл всегда может быть включен полностью вместо "заплаток", и мы будем уверены, что нужный интерфейс обеспечен.
Избегайте условной компиляции. Условной компиляцией с помощью ttifdef и подобных ей директив препроцессора трудно управлять, поскольку информация оказывается рассеянной по всему коду:
как ваш компилятор обрабатывает код,
Выясните, как ваш компилятор обрабатывает код, содержащийся внутри условного блока типа
Напишите программу, которая бы удаляла
Напишите программу, которая бы удаляла из файла случайно образовавшиеся возвраты каретки. После этого напишите еще одну, которая бы добавляла их в файл, заменяя символы перевода строки комбинацией возврата каретки и перевода строки. Как вы будете тестировать эти программы?
<
Заголовочные файлы и библиотеки
Заголовочные файлы и библиотеки предоставляют возможности, расширяющие базовый язык. Например, ввод и вывод осуществляются с помощью библиотек stdio в С, lost ream в C + + и Java, io в Java. Строго говоря, эти элементы не являются частью языка, но они определен! вместе с языком и представляют собой составную часть любой среды, поддерживающей этот язык. Однако, поскольку библиотеки покрывают широкий спектр возможностей и нередко имеют дело со специфическими вопросами устройства операционных систем, в их использовании может крыться причина плохой переносимости программы.
Используйте стандартные библиотеки. Здесь, как и при обсуждении самого языка, применимо то же самое общее правило: придерживайтесь стандарта, отдавая предпочтение старым, устоявшимся компонентам. В С определена стандартная библиотека функций ввода-вывода, операций со строками, проверок символов, выделения памяти под данные и ряда других задач. Если вы ограничите взаимодействие своей программы с операционной системой использованием этих функций, с хорошей долей уверенности можно считать, что программа будет вести себя одинаково при переходе от одной операционной системы к другой. Однако и здесь надо соблюдать осторожность: существуют различные реализации библиотек, и некоторые из них имеют отклонения от стандарта.
В ANSI С не определена функция копирования строк st rdup, хотя она имеется в большинстве сред программирования — даже в тех, что декларируют строгую приверженность стандарту. Может показаться заманчивым использовать данную функцию, но делать этого не следует: компилятор не предупредит вас, что функция не стандартная, а в дальнейшем программу будет не перенести в среду, этой функции не имеющую. Проблемы подобного рода — основной источник головной боли при использовании библиотек, и единственное решение — придерживаться стандарта и тестировать свою программу на возможно большем количестве конфигураций.
Заголовочные файлы и описания пакетов описывают интерфейс со стандартными функциями. Один из недостатков многих заголовочных файлов состоит в том, что в них приводятся описания сразу для нескольких языков. Нередко можно встретить один файл вроде stdiо. h с описаниями одновременно для старого (до стандарта ANSI) С, ANSI С и даже C++ компиляторов. Такой файл получается очень громоздким — в нем много директив условной компиляции вроде #if и ffif def. Язык препроцессора не слишком гибок, поэтому такие файлы получаются довольно сложными для восприятия; иногда в них даже содержатся ошибки.
Ниже приведен фрагмент заголовочного файла одной из систем, причем он еще гораздо лучше многих, по крайней мере нормально отформатирован:
к которому надо стремиться, поскольку
Переносимый код — это идеал, к которому надо стремиться, поскольку только он способен сэкономить вам время при переносе программы из системы в систему или при изменении текущего окружения. Однако переносимость достается не бесплатно — вам придется быть особенно внимательными при написании кода, а также представлять себе детали всех потенциально пригодных систем.
Мы рассмотрели два подхода к обеспечению переносимости — объединение и пересечение. Объединение предусматривает создание ; версий, которые работают в каждой конкретной среде; акцент при этом делается на механизмы вроде условной компиляции. Недостатков у этого подхода много: требуется писать больше кода, и нередко этот код получается весьма сложным; трудно изменять версии, очень трудно тестировать.
Пересечение предусматривает написание возможно большей части кода так, чтобы он работал без каких-либо изменений или условий в любой системе. Обработка системных различий, от которых все равно никуда не денешься, должна быть вынесена в отдельные файлы, которые будут играть роль интерфейса между программой и конкретной системой. И у этого подхода есть свои недостатки, главным из которых является потенциальное ухудшение производительности и даже возможностей, но в общем и целом все недостатки окупаются открывающимися преимуществами.
Нотация
Форматирование данных
Между тем, что мы хотим сказать компьютеру ("реши мою проблему"), и тем, что нам приходится ему говорить для достижения нужного результата, всегда существует некоторый разрыв. Очевидно, что чем этот разрыв меньше, тем лучше. Хорошая нотация поможет нам сказать лменно то, что мы хотели, и препятствует ошибкам. Иногда хороший ;пособ записи освещает проблему в новом ракурсе, помогая в ее решении и подталкивая к новым открытиям.
Малые языки (little languages) — это нотации для узких областей применения. Эти языки не только предоставляют удобный интерфейс, но| л помогают организовать программу, в которой они реализуются. Хоро-лим примером является управляющая последовательность printf:
printf("%d %6.2f %-10.10s\n", f, s);
Здесь каждый знак процента обозначает место вставки значения сле-гующего аргумента printf ; за ним следуют необязательные флаги и раз- \ меры поля и, наконец, буква, которая указывает тип параметра. Такая нотация компактна, интуитивно понятна и легка в использовании; ее реализация достаточно проста и прямолинейна. Альтернативные возможности в C++ (lost ream) и Java ( j ava . io) выглядят гораздо менее привле-сательно, поскольку они не предоставляют специальной нотации, хотя могут расшириться типами, определяемыми пользователем, и обеспечив 5ают проверку типов.
Некоторые нестандартные реализации printf позволяют добавлять] ;вои приведения типов к встроенным. Это удобно, когда вы работаете] : другими типами данных, нуждающимися в преобразованиях при вы- р| зоде. Например, компилятор может использовать знак %1_ для обозначе- ; шя номера строки и имени файла; графическая система — использовать »Р для точки, a %R — для прямоугольника. Строка шифра из букв и номе- , юв — сведения о биржевых котировках, которая рассматривалась нами главе 4, относится к тому же типу: это компактный способ записи та-сих котировок.
Схожие примеры можно придумать и для С и C++. Представим себе, что нам нужно пересылать пакеты, содержащие различные комбинации типов данных, из одной системы в другую. Как мы видели в главе 8, самым чистым решением была бы передача данных в текстовом виде. Однако для стандартного сетевого протокола лучше иметь двоичный формат по причинам эффективности и размера. Как же нам написать код для обработки пакетов, чтобы он был переносим, эффективен и прост в эксплуатации?
Для того чтобы дальнейшее обсуждение было конкретным, представим себе, что нам надо пересылать пакеты из 8-битовых, 16-битовых и 32-битовых элементов данных из одной системы в другую. В стандарте ANSI С оговорено, что в char может храниться как минимум 8 битов, 16 битов может храниться в sho rt и 32 бита — в long, так что мы, не мудрствуя лукаво, будем использовать именно эти типы для представления наших данных. Типов пакетов может быть много: пакет первого типа содержит однобайтовый спецификатор типа, двухбайтовый счетчик, однобайтовое значение и четырехбайтовый элемент данных:
Интерпретаторы, компиляторы и виртуальные машины
Какой путь проходит программа от исходного кода до исполнения? Если язык достаточно прост, как в printf или в наших простейших регулярных выражениях, то исполняться может сам исходный код. Это несложно; таким образом можно запускать программу сразу же по написании.
Нужно искать компромисс между временем на подготовку к запуску и скоростью исполнения. Если язык не слишком прост, то для исполнения желательно преобразовать исходный код в некое приемлемое и эффективное внутреннее представление. На это начальное преобразование исходного кода тратится определенное время, но оно вполне окупается более быстрым исполнением. Программы, в которых преобразование и исполнение объединены в один процесс, читающий исходный текст, преобразующий его и его исполняющий, называются интерпретаторами (interpreter). Awk и Perl, как и большая часть других языков скриптов и языков специального назначения, являются именно интерпретаторами.
Третья возможность — генерировать инструкции для конкретного типа компьютера, на котором должна исполняться программа; этим занимаются компиляторы. Такой подход требует наибольших затрат времени на подготовку, однако в результате ведет и к наиболее быстрому последующему исполнению.
Существуют и другие комбинации. Одна из них — мы рассмотрим ее подробно в данном разделе — это компиляция программы в инструкции для воображаемого компьютера (виртуальной машины — virtual machine), который можно имитировать на любом реальном компьютере. Виртуальная машина сочетает в себе многие преимущества обычной интерпретации и компиляции.
Если язык прост, то какой-то особо большой обработки для определения структуры программы и преобразования ее во внутреннюю форму не требуется. Однако при наличии в языке каких-то сложных элементов — определений, вложенных структур, рекурсивно определяемых выражений, операторов с различным приоритетом и т. п. — провести синтаксический разбор исходного текста для определения структуры становится труднее.
Синтаксические анализаторы часто создаются с помощью специальных автоматических генераторов, называемых также компиляторами компиляторов (compiler-compiler), таких как уасс или bison. Подобные программы переводят описание языка, называемое его грамматикой, как правило, в программу на С или C++, которая, будучи однажды скомпилирована, переводит выражения языка во внутреннее представление. Конечно же, генерация синтаксического анализатора непосредственно из грамматики языка является еще одной впечатляющей демонстрацией мощи хорошей нотации.
Представление, создаваемое анализатором, — это обычно дерево, в котором внутренние вершины содержат операции, а листья — операнды. Выражение
а = max(b, с/2);
эжет быть преобразовано в такое синтаксическое дерево:
Использование макросов для генерации кода
Опустившись на пару уровней, можно говорить о макросах, которые шут код во время компиляции. На протяжении всей книги мы предостегали вас от использования макросов и условной компиляции, по-зльку этот стиль программирования вызывает множество проблем, щако же они все равно существуют, и иногда текстуальная подстановка — это именно то, что нужно для решения данной проблемы. Один из таких примеров - использование препроцессора C/C++ для компоновки программы с большим количеством повторяющихся частей.
Например, программа из главы 7, оценивающая скорость конструкций простейшего языка, использует препроцессор С для компоновки тестов, составляя из них последовательность стереотипных кодов. Суть теста — заключить фрагмент кода в цикл, который запускает таймер, выполняет этот фрагмент много раз, останавливает таймер и сообщает о результатах. Весь повторяющийся код заключен в пару макросов, а измеряемый код передается в качестве аргумента. Первичный макрос имеет такой вид:
Компиляция "на лету"
В предыдущем разделе мы говорили о программах, которые пишут программы. В каждом примере программы генерировались в виде исходного ада и, стало быть, для запуска должны были быть скомпилированы или нтерпретированы. Однако возможно сгенерировать код, который можно шускать сразу, создавая машинные инструкции, а не исходный текст, акой процесс известен как компиляция "налету" (on the fly) или "как раз :вовремя" (just in time); первый термин появился раньше, однако последний — включая его акроним JIT — более популярен.
Очевидно, что скомпилированный код по определению получается епереносимым — его можно запустить только на конкретном типе процессора, зато он может получиться весьма скоростным. Рассмотрим такое выражение:
max (b, c/2)
Здесь нужно вычислить с, поделить его на 2, сравнить результат с b и вы-рать большее из значений. Если мы будем вычислять это выражение, используя виртуальную машину, которую мы в общих чертах описали в начале этой главы, то хотелось бы избежать проверки деления на ноль в divop: поскольку 2 никогда не. будет нулем, такая проверка попросту бессмысленна. Однако ни в одном из проектов, придуманных нами для реализации этой виртуальной машины, нет возможности избежать этой проверки — во всех реализациях операции деления проверка делителя на ноль осуществляется в обязательном порядке.
Вот здесь-то нам и может помочь динамическая генерация кода. Если мы будем создавать непосредственно код для выражения, а не использовать предопределенные операции, мы сможем исключить проверку деления на ноль для делителей, которые заведомо отличны от нуля. На самом деле мы можем пойти еще дальше: если все выражение является константой, как, например, тах(3*3, 2/2), мы можем вычислить его единожды, при генерации кода, и заменять константой-значением, в данном случае числом 9. Если такое выражение используется в цикле, то мы экономим время на его вычисление при каждом проходе цикла. При достаточно большом числе повторений цикла мы с лихвой окупим время, потраченное на дополнительный разбор выражения при генерации кода.
Основная идея состоит в том. что способ записи позволяет нам вообще выразить суть проблемы, а компилятор для выбранного способа записи может специально оптимизировать код конкретного вычисления, Например, в виртуальной машине для регулярных выражений у нас, скорее всего, будет оператор для сравнения с символом:
Нотация
Из всех творений человека
самым удивительным является язык.
Джайлс Литтон Страчи. Слова и поэзия
Правильный выбор языка может решающим образом влиять на простоту написания программы. Поэтому в арсенале практикующего программиста находятся не только языки общего назначения вроде С и его родственников, но и программируемые оболочки, языки скриптов, а также большое количество языков, ориентированных на конкретные приложения.
Преимущества хорошей нотации — способа записи — появляются при переходе от традиционного программирования к узкоспециальным проблемным областям. Регулярные выражения позволяют использовать компактные (из-за этого подчас превращающиеся в тайнопись) описания классов строк. Язык HTML позволяет определять внешний вид интерактивных документов, нередко используя встроенные программы на других языках, вроде JavaScript. PostScript рассматривает целый документ — например эту книгу — как стилизованную программу. Электронные таблицы и текстовые процессоры часто содержат в себе языки программирования типа Visual Basic, они используются для вычисления выражений, доступа к информации, управления размещением данных в документе.
Если вы ловите себя на том, что приходится писать слишком много кода' для выполнения рутинных операций, или если у вас возникают проблемы с тем, чтобы в удобной форме описать весь процесс, знайте — скорее всего, вы выбрали неправильный язык. Отсутствие правильного языка можно считать хорошим поводом написать его самостоятельно. Придумать свой язык вовсе не означает создать преемника Java: просто нередко самые запутанные проблемы проясняются при выборе должной нотации. В связи с этим вспомните форматные строки семейства printf, которые дают нам компактный и выразительный способ для управления выводом.
В этой главе мы говорим о том, как способ записи может помочь в решении наших проблем, а также демонстрируем ряд приемов, которые вы мо-Ц «ете использовать для создания собственных специализированных языков. Мы даже заставим программу писать другую программу, такова экстремальное использование способа записи распространено гораздо пире и осуществляется гораздо проще, чем думают многие программисты.
<
Пакет второго типа может состоять

Пакет второго типа может состоять из одного короткого и двух длинных слов данных:

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

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

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

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

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

Если регулярное выражение пусто, это означает, что мы дошли до его конца и, следовательно, нашли соответствие. Если выражение оканчива- i ется символом $, оно имеет соответствие только в том случае, если текст также расположен в конце. Если выражение начинается с точки, то первый символ соответствующего текста может быть любым. В противном случае выражение начинается с какого-то обычного символа, который) в тексте соответствует только самому себе. Символы ~ и $, встречающиеся в середине регулярного выражения, рассматриваются как простые литеральные, а не метасимволы.
Отметьте, что matchhe re, убедившись в совпадении одного символа из; шаблона и подстроки, вызывает себя самое, таким образом, глубина ре] курсии может быть такой же, как и длина шаблона (при полном соответветствии).
Единственный сложный случай представляет собой выражение, начинающееся с символа и звездочки, например х*. В этом случае мы осуществляем вызов matchstar, где первым аргументом является операнд звездочки (то есть х), а следующими аргументами — шаблон после звездочки и сам текст.
Если регулярное выражение пусто, это

Если регулярное выражение пусто, это означает, что мы дошли до его конца и, следовательно, нашли соответствие. Если выражение оканчива- i ется символом $, оно имеет соответствие только в том случае, если текст также расположен в конце. Если выражение начинается с точки, то первый символ соответствующего текста может быть любым. В противном случае выражение начинается с какого-то обычного символа, который) в тексте соответствует только самому себе. Символы ~ и $, встречающиеся в середине регулярного выражения, рассматриваются как простые литеральные, а не метасимволы.
Отметьте, что matchhe re, убедившись в совпадении одного символа из; шаблона и подстроки, вызывает себя самое, таким образом, глубина ре] курсии может быть такой же, как и длина шаблона (при полном соответветствии).
Единственный сложный случай представляет собой выражение, начинающееся с символа и звездочки, например х*. В этом случае мы осуществляем вызов matchstar, где первым аргументом является операнд звездочки (то есть х), а следующими аргументами — шаблон после звездочки и сам текст.
за того, что регулярному выражению

Здесь мы опять используем do-while — из- за того, что регулярному выражению х* может соответствовать и ноль символов. Цикл проверяет, совпадает ли текст с остатком регулярного выражения, пытаясь сопоставить их, пока первый символ текста совпадает с операндом звездочки.
Наша реализация получилась довольно простенькой, однако она работает и занимает всего лишь три десятка строк кода — можно утверждать, что для того, чтобы ввести в обращение регулярные выражения, не нужно применять никаких сложных средств.
Скоро мы представим некоторые соображения по расширению возможностей нашего кода, а пока напишем свою версию g rep, использующую match. Вот как выглядит основная часть:
за того, что регулярному выражению

Здесь мы опять используем do-while — из- за того, что регулярному выражению х* может соответствовать и ноль символов. Цикл проверяет, совпадает ли текст с остатком регулярного выражения, пытаясь сопоставить их, пока первый символ текста совпадает с операндом звездочки.
Наша реализация получилась довольно простенькой, однако она работает и занимает всего лишь три десятка строк кода — можно утверждать, что для того, чтобы ввести в обращение регулярные выражения, не нужно применять никаких сложных средств.
Скоро мы представим некоторые соображения по расширению возможностей нашего кода, а пока напишем свою версию g rep, использующую match. Вот как выглядит основная часть:
при различных сбоях. Наша программа

По соглашению программы на С возвращают 0 при успешном заверше-и и ненулевое значение — при различных сбоях. Наша программа Ф, так же как и Unix-версия, считает выполнение успешным, если ла найдена строка, соответствующая шаблону. Поэтому она возвращает 0, если было найдено хотя бы одно соответствие; 1 — если соответ-зий найдено не было, и 2 (посредством eprintf) — если произошла шбка. Эти значения можно протестировать, использовав в качестве олочки какую-то другую программу.
Функция grep сканирует один файл, вызывая match для каждой его строки:
при различных сбоях. Наша программа

По соглашению программы на С возвращают 0 при успешном заверше-и и ненулевое значение — при различных сбоях. Наша программа Ф, так же как и Unix-версия, считает выполнение успешным, если ла найдена строка, соответствующая шаблону. Поэтому она возвращает 0, если было найдено хотя бы одно соответствие; 1 — если соответ-зий найдено не было, и 2 (посредством eprintf) — если произошла шбка. Эти значения можно протестировать, использовав в качестве олочки какую-то другую программу.
Функция grep сканирует один файл, вызывая match для каждой его строки:
Если открыть файл не удается,

Если открыть файл не удается, то программа не прекращает работу. Такое поведение было выбрано для того, чтобы при вызове
% grep herpolhode *.*
даже если какой-то файл в каталоге не может быть открыт, g rep уведомляла об этом и продолжала работу; если бы на этом она останавливалась, пользователю пришлось бы вручную набивать список всех файлов в каталоге, кроме недоступного. Обратите внимание и на то, что grep выводит имя файла и строку, соответствующую шаблону, однако имя файла не выводится, если чтение происходит из стандартного потока ввода или из одного файла. Может быть, такое поведение покажется кому-то старомодным, однако оно отражает идиоматический стиль, в основе которого лежит опыт. Когда задан только один источник ввода, миссия дгер состоит, как правило, в выборе соответствующих строк, и имя файла никого не интересует. А. вот если поиск происходит в нескольких файлах, то, как правило, задачей является найти все совпадения с какой-то строкой, и здесь имя файла будет весьма информативно. Сравните
% strings markov.exe | grep 'DOS mode'
и
% grep grammer chapter*.txt
Продуманность нюансов — одна из тех черт, что сделали grep столь популярным инструментом; здесь мы видим, что для создания хороших программ нотация обязательно должна учитывать человеческую психологию.
В нашей реализации match завершает работу сразу же, как только найдено соответствие. Для g rep это вполне подходит в качестве поведения по умолчанию, но для реализации оператора подстановки (найти и заменить) в текстовом редакторе больше подошел поиск крайне левого самого длинного совпадения (leftmost longest). Например, если нам задан текст "ааааа", то шаблону а* соответствует, в частности, и пустая строка в начале текста, однако более естественным было бы включить в совпадение все пять а. Для того чтобы match искала крайне левую и самую длинную строку, matchstar надо переписать так, чтобы добиться жадного (greedy) поведения1 алгоритма: вместо того чтобы просматривать каждый символ текста слева направо, он должен каждый раз пытаться пройти до конца самой длинной строки, соответствующей оператору-звездочке, и откатываться назад, если оставшаяся часть обрабатываемого текста не соответствует оставшейся части шаблона. Другими словами, она должна выполняться справа налево. Вот как выглядит версия matchstar, осуществляющая поиск крайне левого максимального совпадения:
Если открыть файл не удается,

Если открыть файл не удается, то программа не прекращает работу. Такое поведение было выбрано для того, чтобы при вызове
% grep herpolhode *.*
даже если какой-то файл в каталоге не может быть открыт, g rep уведомляла об этом и продолжала работу; если бы на этом она останавливалась, пользователю пришлось бы вручную набивать список всех файлов в каталоге, кроме недоступного. Обратите внимание и на то, что grep выводит имя файла и строку, соответствующую шаблону, однако имя файла не выводится, если чтение происходит из стандартного потока ввода или из одного файла. Может быть, такое поведение покажется кому-то старомодным, однако оно отражает идиоматический стиль, в основе которого лежит опыт. Когда задан только один источник ввода, миссия дгер состоит, как правило, в выборе соответствующих строк, и имя файла никого не интересует. А. вот если поиск происходит в нескольких файлах, то, как правило, задачей является найти все совпадения с какой-то строкой, и здесь имя файла будет весьма информативно. Сравните
% strings markov.exe | grep 'DOS mode'
и
% grep grammer chapter*.txt
Продуманность нюансов — одна из тех черт, что сделали grep столь популярным инструментом; здесь мы видим, что для создания хороших программ нотация обязательно должна учитывать человеческую психологию.
В нашей реализации match завершает работу сразу же, как только найдено соответствие. Для g rep это вполне подходит в качестве поведения по умолчанию, но для реализации оператора подстановки (найти и заменить) в текстовом редакторе больше подошел поиск крайне левого самого длинного совпадения (leftmost longest). Например, если нам задан текст "ааааа", то шаблону а* соответствует, в частности, и пустая строка в начале текста, однако более естественным было бы включить в совпадение все пять а. Для того чтобы match искала крайне левую и самую длинную строку, matchstar надо переписать так, чтобы добиться жадного (greedy) поведения1 алгоритма: вместо того чтобы просматривать каждый символ текста слева направо, он должен каждый раз пытаться пройти до конца самой длинной строки, соответствующей оператору-звездочке, и откатываться назад, если оставшаяся часть обрабатываемого текста не соответствует оставшейся части шаблона. Другими словами, она должна выполняться справа налево. Вот как выглядит версия matchstar, осуществляющая поиск крайне левого максимального совпадения:
с символов, соответствующих остальной части
Если регулярное выражение начинается с ", то текст должен начинаться с символов, соответствующих остальной части выражения. При другом начале мы проходим по тексту, используя matchhere для выяснения, соответствует ли текст каждой позиции выражения. Как только мы находим соответствие, миссия наша завершена. Обратите внимание на использование do-while: выражениям может соответствовать пустая строка (например, шаблону $ соответствует пустая строка, а шаблону . * — любое количество символов, включая и ноль), поэтому вызвать matchhere мы должны даже в том случае, если строка текста пуста.
Большая часть работы выполняется в рекурсивной функции matchhere:
g rep абсолютно неважно, какое

Для g rep абсолютно неважно, какое именно совпадение будет обнаруже-ю, поскольку эта программа проверяет лишь наличие соответствий во-эбще и выводит всю строку, содержащую соответствие. Таким образом, поиск крайне левого максимального совпадения, жизненно необходимый для оператора замещения, хоть и делает некоторую ненужную для g rep работу, все же может быть применен без изменений и для этой программы.
Наша версия g rep вполне сравнима с версиями, поставляемыми с различными системами (неважно, что синтаксис наших регулярных выражений несколько скуднее). Существуют некоторые патологические выражения, которые приводят к экспоненциальному возрастанию трудоемкости поиска, — например, выражение а*а*а*а*а*Ь при введенном тексте ааааааааас, однако экспоненциальный поиск присутствует и в некоторых коммерческих реализациях. Вариант g rep, применяемый в Unix (он называется eg rep), использует более сложный алгоритм поиска соответствий; этот алгоритм гарантирует линейный поиск, избегая отката, когда не подходит частичное соответствие.
А как насчет того, чтобы match умела обрабатывать полноценные регулярные выражения? Для этого надо добавить возможность сопоставления классов символов типа [a-zA-Z] конкретному символу алфавита, возможность исключать значение метасимволов (например, для поиска точки нужно обозначить этот символ как литеральный), предусмотреть скобки для группировки, а также ввести альтернативы (аос или clef). Первый шаг здесь — облегчить match работу, скомпилировав шаблон в некое новое представление, которое было бы проще сканировать: слишком накладно производить разбор класса символов для каждого нового сравнения его с одним символом; предварительно вычисленное представление, основанное на битовых массивах, сделает классы символов гораздо более эффективными. Полная реализация регулярных выражений, со скобками и альтернативами, получится гораздо более сложной; облегчить написание помогут некоторые средства, о которых мы поговорим далее в этой главе.
f mt для того, чтобы

Мы часто используем f mt для того, чтобы заново разбить на абзацы почтовые сообщения и другие короткие документы; ее же мы использовали главе 3 для форматирования вывода программы markov.
Программируемые инструменты часто берут свое начало от малых ыков программирования, созданных для более простого выражения шений проблем в какой-то узкой предметной области. Прелестным шмером является инструмент Unix под названием eqn, который обра-тывает математические формулы. Язык, применяемый в нем для вво-., очень похож на обычный: например, выражение f мы прочитали бы лух как "пи на два" (pi over two), и в этом языке оно так и записывается - pi over 2. Тот же подход применяется и в ТЕХ, в нем это выраже-ie было бы записано как \pi \over 2. Если для проблемы, которую вы пытаетесь разрешить, уже существует естественный или привычный :особ записи, используйте его или, на худой конец, попробуйте его дотировать: не пытайтесь писать с нуля.
Awk развился из программы, которая использовала регулярные выражения для выявления аномалий в записях телефонного трафика; теперь же] ffk содержит в себе переменные, выражения, циклы и т. п., — и это делает о полноценным языком программирования. Perl и Tel первоначально проектировались с целью совместить удобство и выразительность малых ыков с мощью полноценных языков программирования. В результате получились действительно удобные языки программирования общего назначения, хотя, конечно, чаще всего их используют для обработки текста.
Для подобных инструментов применяют общий термин — языки скрип-~>в (scripting languages). Такое название объясняется их происхождением
ранних командных интерпретаторов, вся "программируемость" ко-рых ограничивалась исполнением заранее записанных "сценариев" cript) программ. Языки скриптов позволяют использовать регулярные выражения более творчески: не только для поиска соответствий шаблону — простого обнаружения соответствия, но и для определения участ-iB текста, которые должны быть изменены. Именно это осуществляется в двух командах regsub (от regular expression substitution — замена с помощью регулярных выражений), реализованных в приводимой ниже программе на языке Tel. Программа эта является несколько более общей фор-)й программы из главы 4, получающей биржевые котировки; новая версия выполняет это, получая данные из URL, передаваемого ей в качестве первого аргумента. Первая замена удаляет строку http://, если она присутствует; вторая — удаляет символ / и заменяет его пробелом, разбивая, таким образом, аргумент на два поля. Команда lindex получает поля из строки (начиная с индекса 0). Текст, заключенный в квадратные скобки, выполняется как команда Tel и заменяется результирующим текстом; последовательность $х заменяется значением переменной х.
Этот скрипт, как правило, производит

Этот скрипт, как правило, производит весьма объемистый вывод, большую часть которого составляют тэги HTML, заключенные между < и >. Perl удобен для текстовых подстановок, так что нашим следующим инструментом станет скрипт на Perl, который использует регулярные выражения и подстановки для удаления тэгов:
Для тех, кто не знаком

Для тех, кто не знаком с Perl, этот код будет загадкой. Конструкция
$str =" s/regexp/repl/g
строке str подставляет строку repl вместо текста, соответствующего регулярному выражению readexp (берется крайне левое максимальное соответствие); завершающий символу (от "global") означает, что действия надо произвести глобально, для всех найденных соответствий, и не только для первого. Последовательность метасимволов \s является сокращенным обозначением символа пустого места (пробел, знак табуляции, символ перевода строки и т. п.); Это означает перевод строки. Строка " " — это символ HTML, как и те, о которых мы упоминали I главе 2, он означает non-breakable space — неразрывный пробел.
Собрав все написанное воедино, мы получим совершенно идиотский, но функционирующий web-браузер, реализованный как скрипт командного интерпретатора, состоящий из одной строки:
и отформатирует текст по своим

Такой вызов получит web-страницу, отбросит все управление и форма-гирование и отформатирует текст по своим собственным правилам. Получился быстрый способ достать страницу текста из web.
Отметьте, что мы неспроста использовали сразу несколько Жыков (Tel, Perl, Awk) и в каждом из них — регулярные выражения. Собственно говоря, мощь различных нотаций и состоит как раз в подборе наилучшего варианта для каждой проблемы. Teм особенно хорош для получения текста из сети, Perl и Awk прекрасно редактируют и форматируют текст, а регулярные выражения — отличный способ определения фрагментов текста, которые должны быть найдены и изменены. Объединение всех этих языков получается гораздо более мощным, чем любой из них в отдельности. Целесообразно разбить задачу на части, если можнс выиграть за счет составления правильной нотации в каждой из них.
<
в главе 2, вполне] огут
![в главе 2, вполне] огут](image/9-27.jpg)
Многие из алгоритмов работы с деревьями, описанных в главе 2, вполне] огут быть применимы для создания и обработки синтаксических деревьев.
После того как дерево построено, обрабатывать его можно множе- j гвом способов. Наиболее прямолинейный метод, применяемый, кста-1 я, в Awk, — это прямой обход дерева с попутным вычислением узлов. | 'прощенная версия алгоритма такого вычисления для языка, основанного на целочисленных выражениях, может включать в себя восходя-] щи обход типа такого:
Первые несколько выражений case вычисляют

Первые несколько выражений case вычисляют простые выражения вроде констант или значений; следующие вычисляют арифметические выражения, а дальше может идти обработка специальных случаев, условных выражений и циклов. Для реализации управляющих структур нашему дереву потребуется не показанная здесь дополнительная информация, которая представляет собой поток управления.
Подобно тому, как мы делали в pack и unpack, здесь можно заменить явный переключатель таблицей указателей на функции. Отдельные операции при этом будут выглядеть практически так же, как и в варианте с переключателем:
Таблица указателей сопоставляет операции

Таблица указателей сопоставляет операции и функции, выполняющие операции:
Вычисление использует операции для индексирования

Вычисление использует операции для индексирования в таблице указа-1бй на функции для вызова этих функций; в этой версии другие функ-и вызываются рекурсивно.
Обе наши версии eval применяют

Обе наши версии eval применяют рекурсию. Существуют способы уст-нить ее — в частности, весьма хитрый способ, называемый шитым кодом ireaded code), который практически не использует стек вызовов. Самым ящным способом избавления от рекурсии будет сохранение функций в 1ссиве, с последующим выполнением этих функций в записанном поряд-:. Таким образом, этот массив становится просто последовательностью гструкций, исполняемых некоторой специальной машиной.
Для представления частично вычисленных значений из нашего выра-ения мы, так же как и раньше, будем использовать стек, так что, не-ютря на изменение формы функций, преобразования отследить будет трудно. Фактически мы изобретаем некую стековую машину, в которой инструкции представлены небольшими функциями, а операнды хранятся в отдельном стеке операндов. Естественно, это не настоящая машина, но мы можем писать программу так, как будто подобная машина все же существует: в конце концов, мы можем без труда реализовать ее в виде интерпретатора.
При обходе дерева вместо подсчета его значения мы будем генерировать массив функций, исполняющих программу. Массив будет также содержать значения данных, используемых командами, такие как константы и переменные (символы), так что тип элементов массива должен быть объединением:
Ниже приведен блок кода, генерирующий

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

Для выражения а - max( b , с/2 ) сгенерированный код будет выглядеть так:
операции управляют стеком, извлекая из

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

Этот цикл моделирует в программном виде на изобретенной нами стековой машине то, что происходит на самом деле в настоящем компьютере:
Обратите внимание на то, что

Обратите внимание на то, что проверка делимого на ноль осуществляется в divop, а не в generate.
Условное исполнение, ветвление и циклы модифицируют счетчик программы внутри функции-операции, осуществляя тем самым доступ к массиву функций с какой-то новой точки. Например, операция goto всегда переустанавливает значение переменной рс, а операция условного ветвления — только в том случае, если условие есть истина.
Массив code является, естественно, внутренним для интерпретатора, однако представим себе, что нам захотелось сохранить сгенерированную программу в файл. Если бы мы записывали адреса функций, то результат получился бы абсолютно непереносимым, да и вообще ненадежным. Однако мы могли бы записывать константы, представляющие функции, например 1000 для addop, 1001 для pushop и т. д., и переводить их обратно в указатели на функции при чтении программы для интерпретации.
Если внимательно посмотреть на файл, который создает эта процеду-, можно понять, что он выглядит как поток команд виртуальной машины. Эти команды реализуют базовые операции нашего рабочего языка, рункция generate — это компилятор, транслирующий язык на вирту-. ьную машину. Виртуальные машины — стародавняя идея, обретшая госледнее время новую популярность благодаря Java и виртуальной шгане Java (Java Virtual Machine, JVM); виртуальные машины предо-авляют простой способ создавать переносимые и эффективные реали-, ции программ, написанных на языках высокого уровня.
<
Для настоящего протокола потребовалось бы

Для настоящего протокола потребовалось бы написать не один десяток сих функций — на все возможные варианты. Можно было бы несколь-упростить процесс, используя макросы или функции для обработки ювых типов данных (short, long и т. п.), но и тогда подобный повто-ющийся код было бы трудно воспринимать, трудно поддерживать, 5 итоге он стал бы потенциальным источником ошибок.
Именно повторяемость кода и является его основной чертой, и здесь-то м и может помочь грамотно подобранный способ записи. Позаимство-в идею у printf, мы можем определить свой маленький язык специфи-ции, в котором каждый пакет будет описываться краткой строкой, дающей информацию о размещении данных внутри него. Элементы пакета даруются последовательно: с обозначает 8-битовый символ, s — 16-би-iBoe короткое целое, а 1 — 32-битовое длинное целое.Таким образом, на-зимер, пакет первого типа (включая первый байт определения типа) моет быть представлен форматной строкой cscl. Теперь мы в состоянии :пользовать одну-единственную функцию pack для создания пакетов обых типов; описанный только что пакет будет создан вызовом
pack(buf, "cscl", 0x01, count, val, data);
В нашей строке формата содержатся только описания данных, поэтому ам нет нужды использовать какие-либо специальные символы — вроде в printf.
На практике о способе декодирования данных могла бы сообщать риемнику информация, хранящаяся в начале пакета, но мы предположим, что для определения формата данных используется первый байт пакета. Передатчик кодирует данные в этом формате и передает их; приемник считывает пакет, анализирует первый байт и использует его для (екодирования всего остального.
Ниже приведена реализация pack, которая заполняет буфер buf кодированными в соответствии с форматом значениями аргументов. Мы сделали значения беззнаковыми, в том числе байты буфера пакета, чтобы избежать проблемы переноса знакового бита. Чтобы укоротить описания, мы использовали некоторые привычные определения типов:
Точно так же, как sprinth,

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

Имея такой фрагмент на входе, несложная программа сможет произнес* и следующий набор деклараций для сообщений об ошибках:
У такого подхода есть несколько

У такого подхода есть несколько достоинств. Во-первых, соотношение лежду значениями enum и строками, которые они представляют, получается самодокументированным, и его нетрудно сделать независимым от роднoro языка пользователя. Во-вторых, информация хранится только в однтом месте, в одном "истинном месте", из которого генерируется весь эстальной код, так что и всё обновление информации выполняется лишь в одном месте. В случае, когда таких мест несколько, после ряда обновлений они неизбежно начнут друг другу противоречить. И наконец, нетрудно сделать так, чтобы файлы программ на С создавались заново и перекомпилировались при каждом изменении заголовочного файла. Когда потребуется изменить сообщение об ошибке, все, что надо будет сделать, — это изменить заголовочный файл и компилировать таким способом операционную систему, и тогда сообщения автоматически обновятся.
Программа-генератор может быть написана на любом языке. Особенно просто это сделать на языке, специально предназначенном для обработки строк, таком как Perl:
в дело пошли регулярные выражения.

Опять в дело пошли регулярные выражения. Выбираются строки, у которых первое поле выглядит как идентификатор, после которого стоит запятая. Первая замена удаляет все символы до первого непустого символа строки комментария, а вторая — символы конца комментария и все предшествующие им пробелы.
Среди прочих способов для тестирования компилятора Энди Кёниг (Andy Koenig) разработал метод написания кода C++, позволяющий проверить, нашел ли компилятор ошибки в программе. Фрагменты кода, которые должны вызвать реакцию компилятора, снабжаются специальными комментариями, в которых описываются ожидаемые сообщения. Каждая строка такого комментария начинается с /// (чтобы их можно было отличить от обычных комментариев) и регулярного выражения, которое должно соответствовать диагностике компилятора, выдаваемой для этой строки. Таким образом, например, следующие два фрагмента кода должны вызвать реакцию компилятора:
Если мы пропустим второй тест

Если мы пропустим второй тест через компилятор C++, то он напечатает ожидаемое сообщение, вполне соответствующее регулярному выражению:
% СС х.с
"х.с", line 1: error(321): void function may
not return a value
Каждый такой фрагмент кода пропускается через компилятор, и вывод сравнивается с прогнозируемой диагностикой, этим процессом управляет схемная оболочка и программа на Awk. Сбоем считается ситуация, гда вывод компилятора не совпадает с ожидаемым. Поскольку комментарии представлены регулярными выражениями, у них получается которая свобода в оценке вывода: ее можно делать более или менее эогой к отклонениям в зависимости от надобности.
Идея использования семантических комментариев не нова. Такие комментарии используются в языке PostScript, где они начинаются с символа %. Комментарии, начинающиеся с %%, могут содержать дополнительную информацию о номерах страниц, окаймляющем прямоугольнике ounding Box), именах шрифтов и т. п.:
В языке Java комментарии, которые

В языке Java комментарии, которые начинаются с /** и заканчиваются */, пользуются для создания документации для следующего за ними опи-НЕИЯ класса. Глобальным вариантом самодокументации кода является к называемое грамотное программирование (literate programming), и котором программа и ее документация интегрируются в один доку-нт, и при одной обработке документ готовится для чтения, а при дру-й программа готовится к компиляции.
Во всех рассмотренных выше примерах важно отметить роль нотации, смерения языков и использования инструментов. Их комбиниро-ние усиливает и подчеркивает мощь отдельных компонентов.
Упражнение 9-15
В программировании давно известна забавная задачка: написать про-шму, которая при выполнении точно воспроизводила бы саму себя — в виде исходного кода. Это такой гипертрофированный случай программы, пишущей программу. Попробуйте выполнить это — на своем любимом языке.
<
в нескольких строках. Этот макрос

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

Для инициализации иногда применяются и другие операторы, но основная часть, производящая замеры, представлена в этих одноаргументных фрагментах, которые преобразуются в значительный объем кода.
Иногда макросы могут использоваться и для генерации нормального коммерческого кода. Барт Локанти (Bart Locanthi) однажды написал эффективную версию оператора двумерной графики. Этот оператор, называемый bitblt, или rasterop, трудно было сделать быстрым, поскольку он использовал большое количество аргументов, которые могли комбинироваться самыми хитрыми способами. Проведя тщательный разбор вариантов, Локанти уменьшил комбинации до независимых циклов, которые можно было оптимизировать по отдельности. Затем каждый случай был воссоздан с помощью макроподстановки, аналогичной той, что показана в примере на тестирование производительности, и все варианты были перебраны в одном большом выражении switch. Оригинальный исходный код представлял две-три сотни строк, после выполнения макроподстановок он разрастался до многих тысяч строк. Этот конечный код был не самым оптимальным, но, учитывая сложность задачи, весьма
эактйчным и простым в написании. И кстати, как и весь код самого вы-iKoro уровня, неплохо переносимым.
когда мы генерируем код для

Однако, когда мы генерируем код для конкретного шаблона, значение этого literal фиксировано, например ' х ' , так что мы можем вместо показанного выше сравнения использовать оператор вроде следующего:
И затем, вместо того чтобы

И затем, вместо того чтобы предварительно определять специальный оператор для значения каждого символа-литеры, мы можем поступить проще: генерировать код для операторов, которые нам будут действительно нужны для данного выражения. Применив эту идею для полного набора операторов, мы можем написать JlT-компилятор, который будет анслировать заданное регулярное выражение в специальный код, оп-мизированный именно под это выражение.
Кен Томпсон (Ken Thompson) именно это и сделал в 1967 году для реализации регулярных выражений на машине IBM 7094. Его версия генерировала в двоичном коде небольшие блоки команд этой машины для разных операторов выражения, сшивала их вместе и затем запускала шучившуюся программу, просто вызвав ее, совсем как обычную функцию. Схожие технологии можно применить для создания специфических юледовательностей команд для обновлений экрана в графических системах, где может быть так много различных случаев, что гораздо более эффективно создавать динамический код для каждого из них, чем расписать с все заранее или включить сложные проверки в более общем коде.
Демонстрация того, что же включает в себя создание полноценного Т-компилятора, неизбежно вынудит нас обратиться к деталям конкретных наборов команд, однако все же стоит потратить некоторое время, гобы на деле понять, как работает такая система. В оставшейся части :ого параграфа для нас важно будет понимание сути, идеи происходя-,его, а не деталей конкретных реализаций.
Итак, вспомним, в каком виде мы оставили нашу виртуальную маши-у, — структура ее выглядела примерно так:
Для того чтобы адаптировать этот

Для того чтобы адаптировать этот код для JIT-компиляции, в него [адо внести некоторые изменения. Во-первых, массив code будет теперь» re массивом указателей на функции, а массивом исполняемых команд.
Будут ли эти команды иметь тип char, int или long — зависит только от того процессора, под который мы компилируем; предположим, что это будет int. После того как код будет сгенерирован, мы вызываем его как функцию. Никакого виртуального счетчика команд программы в новом коде не будет, поскольку обход кода за нас теперь будет выполнять собственно исполнительный цикл процессора; по окончании вычисления результат будет возвращаться — совсем как в обычной функции. Далее, мы можем выбрать — поддерживать ли нам отдельный стек операндов для нашей машины или воспользоваться стеком самого процессора. У каждого из этих вариантов есть свои преимущества; мы решили остаться верными отдельному стеку и сконцентрироваться на деталях самого кода. Теперь реализация выглядит таким образом:
После того как generate завершит

После того как generate завершит работу, gen return вставит команды, которые обусловят передачу управления от сгенерированного кода к eval.
Функция f lushcaches отвечает за шаги, необходимые для подготовки процессора к запуску свежесозданного кода. Современные машины работают быстро, в частности благодаря наличию кэшей для команд и данных, а также конвейеров (pipeline), которые отвечают за выполнение сразу нескольких подряд идущих команд. Эти кэши и конвейеры исходят из предположения, что код не изменяется; если же мы генерируем этот код непосредственно перед запкуском, топроцессор может оказаться в затруднении: ему нужно обязательно очистить свой конвейер и кэши для исполнения новых команд. Эти операции очень сильно зависят от энкретного компьютера, и, соответственно, реализация f lushcaches будет в каждом случае совершенно уникальной.
Замечательное выражение (void (*)(void)) code преобразует адрес :ассива, содержащего сгенерированные команды, в указатель на функцию, который можно было бы использовать для вызова нашего кода.
Технически не так трудно сгенерировать сам код, однако, конечно, для ого чтобы сделать это эффективно, придется позаниматься инженерной .еятельностью. Начнем с некоторых строительных блоков. Как и раньше, [ассив code и индекс внутри него заполняются во время компиляции. Для [ростоты мы повторим свой старый прием — сделаем их оба глобальными. Затем мы можем написать функцию для записи команд:
Сами команды могут определяться макросами,

Сами команды могут определяться макросами, зависящими от процессора, или небольшими функциями, которые собирали бы код, заполняя поля в командном слове инструкции. Гипотетически мы могли бы завести функцию pop reg, которая бы генерировала код для выталкивания значения из стека и сохраняла его в регистре процессора, и функцию push reg, которая бы генерировала код для получения значения, хранящегося в регистре процессора, и заталкивания его в стек. Наша обновленная функция addop будет использовать некие их аналоги, применяя некоторые предопределенные константы, описывающие команды (вроде ADDINST) и их расположение (различные позиции сдвигов SHIFT, которые определяют формат командного слова):
только самое начало. Если бы

Это, однако, только самое начало. Если бы мы писали настоящий JIT-ком-пилятор, нам бы пришлось заняться оптимизацией. При прибавлении константы нам нет нужды грузить ее в стек, вынимать оттуда и после этого прибавлять: мы можем прибавить ее сразу. Должное внимание к подобным случаям помогает избавиться от множества излишеств. Однако даже в теперешнем своем виде функция addop будет выполняться гораздо быстрее, чем в наших более ранних версиях, поскольку различные операторы уже не сшиты воедино вызовами функций. Вместо этого код, исполняющий их, располагается теперь в памяти в виде единого блока команд, и для нас все сшивается непосредственно счетчиком команд процессора.
Теперешняя реализация функции generate выглядит весьма похоже на реализацию виртуальной машины, но на этот раз она задает реальные машинные команды вместо указателей на предопределенные функции. Чтобы генерировать более эффективный код, следовало бы потратить усилия на избавление от лишних констант и другие виды оптимизации.
В нашем ураганном экскурсе в генерацию кода мы бросили лишь самый поверхностный взгляд на некоторые из технологий, применяемых в настоящих компиляторах, некоторые же вообще обошли молчанием. При этом мы лишь попутно затронули ряд аспектов, связанных со сложностью организации современных процессоров. Но мы показали, как программа может анализировать описание проблемы, чтобы создать специальный код, который наиболее эффективен для конкретной задачи. Изложенные выше идеи вы можете использовать для создания быстрой версии grep, для реализации небольшого языка программирования имени себя, для проектирования и создания виртуальной машины,' оптимизированной для решения специфических задач, или даже для создания компилятора для какого-нибудь интересного языка.
Между регулярным выражением и программой на C++ есть, конечно, немалая разница, но суть у них одна — это всего лишь нотации для решения проблем. При правильной нотации многие проблемы становятся гораздо более простыми. А проектирование и реализация выбранной нотации может дать массу удовольствия.
Функция pack использует заголовочный файл

Функция pack использует заголовочный файл stda rg . h более активно, чем функция eprintf в главе 4. Аргументы последовательно извлекаются с помощью макроса va_arg, первым операндом которого является переменная типа va_list, инициализированная вызовом va_start; а в качестве второго операнда выступает тип аргумента (вот почему va_arg — то именно макрос, а не функция). По окончании обработки должен быть осуществлен вызов va_end. Несмотря на то что аргументы для ' с ' ; ' s ' представлены значениями char и short соответственно, они должны извлекаться как int, поскольку, когда аргументы представлены многоточием, С переводит char и short в int.
Теперь функции pack_type будут состоять у нас всего из одной строки, которой их аргументы будут просто заноситься в вызов pack:
Для распаковки мы делаем то

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

Так же как, например, scanf, функция unpack должна возвращать вызвавшему ее коду множество значений, поэтому ее аргументы являются указателями на переменные, где и будут храниться требуемые результаты. Значением функции является количество байтов в пакете, его можно использовать для контроля.
Все значения у нас беззнаковые. Мы придерживались размеров, которые ANSI С определяет для типов данных, и поэтому наш код можно переносить даже между машинами, имеющими разные размеры для типов short и long. Если только программа, использующая pack, не будет пытаться переслать как long (к примеру) значение, которое не может быть представлено 32 битами, то значение будет передано корректно; на самом деле мы передаем младшие 32 бита числа. Если же потребуется передавать более длинные значения, то нужно придумать другой формат.
Благодаря использованию unpack, функции для распаковки пакетов в зависимости от их типа стали выглядеть гораздо проще:
Программируемые инструменты
Множество инструментов группируется вокруг языков специального.'! назначения. Программа g rep — всего лишь одна из целого семейства инструментов, которые используют регулярные выражения или другие языки для разрешения программистских проблем.
Одним из первых подобных инструментов стал командный интерпретатор, или язык управления заданиями. Достаточно быстро стало понятно, что последовательность команд можно поместить в отдельный файл, а потом запустить экземпляр интерпретатора команд, или оболочки /(shell), указав этот файл в качестве источника ввода. Следующим шагомстало уже добавление параметров, условных выражений, циклов, переменных и т. п., то есть всего того, из чего в нашем представлении состоит нормальный язык программирования. Единственное отличие заключалось в том, что существовал только один тип данных — строки, а операторами в программах-оболочках являлись различные программы, осуществлявшие порой достаточно интересные вычисления. Несмотря на то что программирование под конкретные оболочки сейчас уже уходит в прошлое, уступая место работе с инструментами типа Perl в командных средах или щелканью по кнопкам в графических средах, этот "старинный" подход по-прежнему остается простым и достаточно эффективным способом создания каких-то сложных операций из простых частей.
Еще один программируемый инструмент, Awk, представляет собой небольшой специализированный язык, который предназначен для отбора и преобразования элементов входного потока; он ищет в этом потоке соответствия заданным шаблонам, а когда находит, то выполняет связанные с шаблонами действия. Как мы уже видели в главе 3, Awk автоматически читает входные файлы и разделяет каждую прочитанную строку на поля, обозначаемые от $1 до $NF, где NF есть количество полей в строке. Его поведение "по умолчанию" удобно для выполнения большого числа типовых действий, поэтому многие полезные программы пишутся на Awk в одну строчку. Так, например, программа
# split.awk: расщепляет текст на отдельные слова { for (i = 1; i <= NF; i+3-) print $i }
выводит "слова" по одному в строке. И напротив, вот программа f mt, которая заполняет каждую выводимую строку словами (до 60 символов); пустая строка означает конец параграфа:
С достаточно часто используется подобным
Язык С достаточно часто используется подобным образом -в качестве "языка ассемблера высокого уровня". Modula-З и C++ относятся к тем языкам общего назначения, для которых первые компиляторы создавали код на С, обрабатывавшийся затем уже стандартным компилятором. У такого подхода есть ряд преимуществ — одним из главных является эффективность, поскольку получается, что в принципе программа может выполняться так же быстро, как и программы на С. Еще один плюс — переносимость: такие компиляторы могут быть перенесены на любую систему, имеющую компилятор С. Подобный подход сильно помог этим языкам на ранних стадиях их внедрения.
В качестве еще одного примера возьмем графический интерфейс Visual Basic. Он генерирует набор операторов присваивания Visual Basic для инициализации объектов. Этот набор пользователь выбирает из меню и располагает на экране с по'мощью мыши. Во множестве других языков есть "визуальная" среда разработки и "мастера" (wizard), которые синтезируют код пользовательского интерфейса по щелчку мыши.
Несмотря на мощь программ-генераторов и на большое количество примеров их удачного применения, этот подход еще не признан так широко, как он того заслуживает, и нечасто используется программистами. Однако существует уйма простых возможностей программного создания кода, и вы вполне можете использовать их преимущества. Ниже приведены несколько примеров генерации кода на С или C++.
Операционная система Plan 9 генерирует сообщения об ошибках по заголовочному файлу, содержащему программные имена ошибок и комментарии к ним; эти комментарии механически конвертируются в строки, заключенные в кавычки, они помещаются в массив, который может быть индексирован перечислимым значением. В приведенном ниже фрагменте показана структура такого заголовочного файла:
Программы, которые пишут программы
Возможно, самым примечательным в функции generate является то, о она представляет собой программу, которая пишет другую програм-г. ее вывод есть поток исполнимых команд для другой (виртуальной) шшны. Собственно, подобная идея нам привычна, именно это делают е компиляторы, только команды они генерируют для реальных ма-ин. На самом деле формы, в которых появляются программы, пишу- \ ие программы, очень разнообразны.
Один обычный пример дает динамическая генерация HTML для web-раниц. HTML — это язык, хоть и достаточно ограниченный; кроме то, в себе он может содержать и код JavaScript. Web-страницы часто нерируются "на лету" программами на Perl или С, содержание таких раниц (например, результаты поиска или реклама, нацеленная на определенную аудиторию) зависит от приходящих запросов. Мы использовали специализированные языки для графиков, картинок, таблиц, математических выражений и индекса этой книги. Еще одним примером эжет служить PostScript — язык программирования, тексты на котором создаются текстовыми процессорами, программами рисования и множеством других источников; на финальном этапе обработки книга, которую вы держите в руках, представлена как программа на PostScript, держащая 57 000 строк.
Документ — это статическая программа, однако идея использования языка программирования как способа записи любой проблемы является весьма многообещающей. Много лет назад программисты мечтали о том, что компьютеры когда-нибудь смогут сами писать для себя программы, энечно же, эта мечта никогда не осуществится в полной мере, однако годня можно сказать, что машины нередко пишут программы за нас и иногда в таких областях, где совсем недавно это трудно было себе представить.
Наиболее распространенные программы, создающие программы, — это компиляторы, которые переводят программу с языка высокого уровня в машинный код. Однако нередко оказывается удобным переводить код программы сначала на один из широко известных языков высокого уровня. В предыдущем параграфе мы упоминали о том, что генератор синтаксического анализатора преобразует определение грамматики языка в программу на С, которая и занимается синтаксическим разбором языка.
напросто соответствуют сами себе, так
Большинство символов просто- напросто соответствуют сами себе, так что регулярное выражение abc будет соответствовать именно этой строке символов, где бы она ни появлялась. Но, кроме того, существует несколько метасимволов, которые обозначают повторение, группировку или местоположение. В принятых в Unix регулярных выражениях символ ~ обозначает начало строки, а $ — конец строки, так что шаблон ~х соответствует только символу х в начале строки, х$ — только х в конце строки. Шаблону ~х$ соответствует только строка, содержащая единственный символ х, и, наконец, ~$ соответствует пустая строка.
Символ . (точка) обозначает любой символ, так что выражению х. у будут соответствовать хау, х2у и т. п., но не ху или xaby; выражению ~. $ соответствует строка из одного произвольного символа.
Набору символов внутри квадратных скобок соответствует любой ия этих символов. Таким образом, [0123456789] соответствует одной цифре, это выражение можно записать и в сокращенном виде: [0-9]1.
Эти строительные блоки комбинируются с помощью круглых скобок' для группировки, символа | — для обозначения альтернатив, * — для обозначения нуля или более совпадений, + — для обозначения одного или более совпадений и ? — для обозначения нуля или одного совпадения. Наконец, символ \ применяется в качестве префикса перед метасимволом для исключения его специального использования, то есть \* обозначает просто символ *, а \\ — собственно символ обратной косой черты \.
Наиболее известным инструментом работы с регулярными выраже-йийми является программа g rep, о которой мы уже несколько раз упоминали. Эта программа — чудесный пример, показывающий важность нотации. В ней регулярное выражение применяется к каждой строке вводимого файла и выводятся строки, которые содержат образцы поиска, описываемые этим выражением. Эта простая спецификация с помощью регулярных выражений позволяет справиться с большим количеством ежедневной рутинной работы. В приведенных ниже примерах обратите внимание на то, что синтаксис регулярных выражений, используемых в качестве аргументов g rep, отличается от шаблонов поиска, применяемых для задания набора имен файлов; различие обусловлено разным назначением выражений.
Какой исходный файл использует класс
Какой исходный файл использует класс Regexp?
% grep Regexp *.java
В каком файле этот класс реализован?
% grep 'class.*Regexp' *.java
Куда я подевал это письмо от Боба?
% grep '"From:.* bob@' mail/*
Сколько непустых строк кода в этой программе?
grep *. с++ 1 we
Благодаря наличию флагов, позволяющих выводить номера соответствующих выражению строк, подсчитывать количество соответствий, осуществлять сравнение без учета регистра, инвертировать смысл выражения (то есть отбирать строки, не соответствующие шаблону) и т. п., программа grep используется сейчас настолько широко, что стала уже классическим примером программирования с помощью специальных инструментов.
К сожалению, не во всех системах имеется grep или ее аналог. Некоторые системы включают в себя библиотеку регулярных выражений, которая называется, как правило, regex или regexp, и ее можно использовать для создания собственной версии g rep. Если же нет ни того, ни другого, то на самом деле не так трудно реализовать какое-то скромное подмножество языка регулярных выражений. Здесь мы представляем реализацию регулярных выражений и grep, чтобы работать с ними; для простоты мы ограничимся метасимволами $ . и * , причем * обозначает повторение предыдущей точки или обычного символа. Выбор этого подмножества обеспечивает почти все возможности регулярных выражений, тогда как его программировать значительно проще, чем в исходном общем случае.
Начнем с самой функции, осуществляющей проверку на соответствие. Ее задача — определить, соответствует ли строка текста регулярному выражению:
Регулярные выражения
Спецификаторы формата для pack и unpack — достаточно простой юсоб записи, описывающий компоновку пакетов. Следующая тема iniero обсуждения — несколько более сложный, но гораздо более фазительный способ записи -- регулярные выражения (regular pressions), определяющие шаблоны текста (patterns of text). В этой [иге мы время от времени использовали регулярные выражения, не вая им точного описания; они достаточно знакомы, чтобы понять их без особых пояснений. Хотя регулярные выражения широко распространены в средах программирования под Unix, в других системах они применяются гораздо реже, поэтому в данном разделе мы решили показать некоторые их преимущества. На случай, если у вас нет под рукой библиотеки с регулярными выражениями, мы приведем ее простейшую реализацию.
Существует несколько разновидностей регулярных выражений, но суть у них у всех одинакова — это способ описания шаблона буквенных символов, включающего в себя повторения, альтернативы и сокращения для отдельных классов символов вроде цифр или букв. Примером такого шаблона могут служить всем знакомые символы замещения (wildcards), используемые в процессорах командной строки или оболочках для задания образцов поиска имен файлов. Как правило, символ * используется для обозначения "любой строки символов", так что команда
С:\> del *.exe
использует шаблон, которому соответствуют все имена файлов, оканчивающиеся на ".-. ехе". Как это часто бывает, детали меняются от системы к системе и даже от программы к программе.
Причуды отдельных программ могут навести на мысль, что регулярные выражения являются механизмом? создаваемым ad hoc, для каждого случая отдельно, но на самом деле регулярные выражения — это язык с формальной грамматикой и точным значением каждого выражения. Более того, при должной реализации конструкция работает очень быстро: благодаря соединению теории и практического опыта; вот пример в пользу специализированных алгоритмов, упоминавшихся нами в главе 2.
Регулярное выражение есть последовательность символов, которая определяет множество образцов поиска.
и unpack так, чтобы можно
Измените pack и unpack так, чтобы можно было передавать и значения со знаком, причем даже между машинами, имеющими разные размеры short и long. Как вы измените форматную строку для обозначения элемента данных со знаком? Как можно протестировать код, чтобы убедиться, что он корректно передает, например, число -1 с компьютера с 32-битовым long на компьютер с 64-битовым long?
В нашей реализации match специальное
В нашей реализации match специальное значение символов " и $ отключается, если они не стоят, соответственно, в начале или конце выражения, а звездочка * рассматривается как обычный символ, если она не стоит непосредственно после литерального символа или точки. Однако более стандартным поведением является~отключение значения метасимвола постановкой перед ним символа обратной косой черты (\). Измените match так, чтобы она обрабатывала этот символ именно таким образом.
в match классы символов. Класс
Добавьте в match классы символов. Класс символов определяет соответствие любому из символов, заключенных в квадратные скобки. Для Удобства лучше ввести диапазоны, например [a-z] соответствует любой строчной букве (английского алфавита!), а также определить способ инвертирования смысла, — например, [~0-9] соответствует любому символу, кроме цифры.
Упражнение 9-12
Измените match так, чтобы в ней использовалась версия matchstar, ( которой ищется крайне левое максимальное соответствие. Кроме того, :ледует добиться возвращения позиции символов начала и конца текста, ;оответствующего шаблону. Теперь создайте новую версию дгер, кото- j рая вела бы себя как старая, но выполняла замену текста, соответству-. ющего шаблону, заданным новым текстом и выводила полученные стро-1 ки. Пример вызова:
grep 'homoiousian' 'homoousian' mission.stmt
Упражнение 9-13
Измените match и дгер так, чтобы они-работали со строками символов Unicode формата UTF-8. Поскольку UTF-8 и Unicode являются расширением набора 7-битового ASCII, такое изменение будет совместимо с предыдущей версией. Регулярные выражения, так же как и текст, в котором происходит поиск, должны корректно работать с UTF-8. Как в этом случае должны быть реализованы классы символов?
Напишите программу для автоматического тестирования
Напишите программу для автоматического тестирования регулярных выражений: она должна генерировать тестовые выражения и строки, в которых будет происходить поиск. Если можете, используйте уже существующую библиотеку как прототип для ответов, — возможно, вы найдете какие-то ошибки и в ней.
<
вам предлагалось написать программу,
В упражнении 7- 7 вам предлагалось написать программу, оценивающую траты на различные операции в C++. Используя идеи, изложенные в последнем параграфе, попробуйте написать новую версию этой программы.
надо было построить модель
В упражнении 7- 8 надо было построить модель оценки затрат для tva, а в этом языке нет макросов. Попробуйте решить эту проблему, написав другую программу — на любом другом языке (или языках), которая создавала бы Java-версию и автоматизировала бы запуск тестов на эоизводительность.
<
компилятор сгенерирует более быстрый код,
JIT- компилятор сгенерирует более быстрый код, если сможет заменить выражения, содержащие только константы, такие как тах(3*3, 4/2), их значением. Опознав такое выражение, как он должен вычислять его значение?
Упражнение 9-19
Как бы вы тестировали JIT-компилятор?
<
и unpack возможности обработки строк.
Добавьте в pack и unpack возможности обработки строк. (Есть вариант включать длину строки в форматную строку.) Добавьте возможность обработки повторяющихся значений с помощью счетчика. Как это соотносится с кодировкой строк?
Вспомните таблицу указателей на функции,
Вспомните таблицу указателей на функции, которую мы применили олько что в программе на С. Такой же принцип лежит в основе механиз-ia виртуальных функций C++. Перепишите pack, unpack и receive на ;++, чтобы прочувствовать все удобство этого способа.
Напишите версию printf для командной
Напишите версию printf для командной строки: пусть эта функция ечатает свой второй и последующие аргументы в формате, заданном первом аргументе. Надо отметить, что во многих оболочках имеется строенный аналог такой функции.
классе Decimal Format, где числа
Напишите функцию, реализующую спецификации.формата, .исполь-/емого в какой-нибудь программе работы с электронными таблицами ли в Java- классе Decimal Format, где числа отображаются в соответствии некоторым заданным шаблоном, указывающим количество обязатель-ых и возможных символов, положение десятичной точки и тысячных шятых и т. п. Для иллюстрации рассмотрим строку
##,##0.00
Эта строка задает число с двумя знаками после десятичной точки, э крайней мере одним знаком перед десятичной точкой, запятой в качестве разделителя тысяч и заполняющими пробелами до позиции 10 000. аким образом, число 12345.67 будет представлено как 12, 345. 67, а .4 — ж **,**0.40 (для наглядности вместо пробелов мы вставили звездоч-i). Для получения полной спецификации можете обратиться к описа-«о DecimalFormat или программам работы с электронными таблицами.
<
и strstr при поиске простого
Как соотносится быстродействие match и strstr при поиске простого текста (без метасимволов)?
Напишите версию matchhere без рекурсии
Напишите версию matchhere без рекурсии и сравните ее быстродействие с рекурсивной версией.
g rep несколько ключей командной
Добавьте в g rep несколько ключей командной строки. К наиболее популярным ключам относятся -у для инвертирования смысла шаблона, -i для поиска без учета регистра и ~п для вывода номеров строк. Как должны выводиться номера строк? Должны ли они печататься на той же строке, что и совпавший текст?
соответствует строка из одного или
Добавьте в match операторы + (один или более) и ? (ноль или один). Шаблону a+bb? соответствует строка из одного или более а, за которыми следует одно или два b.
Программирование: Языки - Технологии - Разработка