Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Kernigan_B__Payk_R_Praktika_programmirovania.pdf
Скачиваний:
80
Добавлен:
18.03.2016
Размер:
2.53 Mб
Скачать

#else

This will give a syntax error on other systems #endif

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

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

Упражнение 8-1

Выясните, как ваш компилятор обрабатывает код, содержащийся внутри условного блока типа

const int DEBUG = 0;

/* или enum { DEBUG = 0 }; */

/* или final boolean DEBUG = false; */ if (DEBUG) {

}

При каких обстоятельствах компилятор проверяет синтаксис? Когда он генерирует код?

Если у вас есть доступ к разным компиляторам, поэкспериментируйте с ними и сравните результаты.

Изоляция

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

Выносите системные различия в отдельные файлы. Когда для разных систем требуется разный код, его лучше выносить в отдельные файлы — один файл на каждую систему. Например, текстовый редактор Sam работает под Unix, Windows и в ряде других операционных систем. Интерфейсы с системой меняются от среды к среде очень широко, но большая часть кода Sam везде идентична. Все различия, связанные с конкретной системой, вынесены в отдельные файлы: unix. с содержит код интерфейса с системами Unix, a windows, с — со средой Windows. В этих файлах реализуются переносимые интерфейсы с операционной системой, различия же

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

Самое интересное, что графические оболочки различных операционных систем не играют почти никакого значения: Sam имеет собственную переносимую библиотеку для своей графики. Несмотря на то что написать такую библиотеку, конечно же, гораздо труднее, чем просто адаптировать код под данную систему (код интерфейса с системой X Window, например, по своим размерам приближается к половине всего остального ко,ш Sam), суммарные затраты для нескольких систем получаются все равно меньше. При этом не надо забывать, что графическая библиотека ценна сама по себе и использовалась для создания и других переносимых программ.

Sam — это довольно старая программа; в наши дни переносимые графические оболочки, такие как OpenGL, Tcl/Tk и Java, доступны для большого числа платформ. Создание кода на их основе вместо использования собственных графических библиотек обеспечит вашим программам большую область применения.

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

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

Подход к переносимости, реализованный в системе Java, — пример того, насколько далеко можно здесь продвинуться. Программа на Java транслируется вЪперации "виртуальной машины", модели компьютера, которую можно реализовать на любой настоящей машине. Библиотеки Java предоставляют унифицированный доступ к возможностям системы: графике, пользовательскому интерфейсу, сети и т. п.; библиотеки же отображают этот доступ в возможности локальной системы. Теоретически должно быть возможно исполнить Java-программу (даже после трансляции) где угодно без каких-либо изменений.

Обмен данными

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

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

способами. Например, если вывод одной программы нельзя использовать напрямую для ввода в другую, то скрипт на Awk или Perl позволяет легко его преобразовать в нужный вид. С помощью g rep можно производить отбор строк, а ваш любимый редактор поможет произвести более сложные преобразования. Кроме всего прочего, I текстовые файлы легко документировать, да и пояснений к ним требуется гораздо меньше, потому что их всегда можно прочитать. Комментарий в текстовом файле может указывать, какая версия программы необ- \ ходима для обработки данных: например, первая строка файла PostScript определяет версию языка (и, возможно, тип документа):

%!PS-Adobe-2.0

В противоположность текстовым двоичные файлы требуют для своей j обработки специализированные средства, и их редко удается использовать совместно даже на одной машине. Существует множество известных программ, преобразующих произвольные двоичные данные в текст. Среди них стоит назвать binhex для Macintosh, uuencode и uudecode для Unix и различные инструменты, использующие кодировку MIME для преобразования двоичных данных в почтовые сообщения. В главе 9 мы расскажем о ряде средств паковки и распаковки двоичных данных для их передачи с сохранением переносимости. Кстати, уже само обилие таких инструментов подчеркивает наличие серьезных проблем, связанных с двоичными форматами.

С передачей текста связана одна давняя проблема: PC-системы (операционные системы D'OS и Windows) используют для обозначения конца строки символ возврата каретки '\r' к символ перевода строки ' \п', а системы Unix — только символ перевода строки. Возврат каретки — это артефакт, дошедший до нас от древнего устройства, называемого телетайпом, который имел операцию возврата каретки (CR) для возврата печатающего механизма в начало строки и отдельный оператор протяж-'ки на строку (LF — от Line Feed) для перевода этого механизма на следующую строку.

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

Мы можем посоветовать использовать стандартные интерфейсы, которые воспринимают CRLF в зависимости от конкретной системы -либо (для PC) удаляя символ \r при вводе и добавляя его обратно на выходе, либо (для Unix), никогда даже не создавая его. Для файлов, которые должны будут передаваться туда и обратно, необходимо написать программу, преобразующую их форматы.

Упражнение 8-2

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

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

Порядок байтов

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

По крайней мере один вопрос решен: все современные машины имеют 8-битовые байты. Однако все объекты, большие байта, представляются на разных машинах поразному, поэтому полагаться на какие-то определенные свойства было бы ошибкой. Короткие целые числа (обычно 16 битов, или 2 байта) могут иметь младший байт, расположенный как по меньшему адресу (little-endian, младшеконечное расположение), чем старший, так и по большему (big-endian, старшеконечное)i. Выбор варианта произволен, а некоторые машины вообще поддерживают обе! модели.

Итак, несмотря на то, что и старшеконечные и младшеконечные машины рассматривают память как последовательность слов, расположенных в одном и том же порядке, байты внутри слова они интерпретируют! различно. На приведенной диаграмме четыре байта, начинающиеся с поя зиции 0, представляют шестнадцатеричное целое 0x11223344 для старшеконечников и 0x44332211 —для младшеконечников.

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

/* byteorder: отображает байты длинного целого */ int main(void)

{

unsigned long x; unsigned char *p; int i;

/* 11 22 33 44 => big-endian */ /* 44 33 22 11 => little-endian */

/* x = Ox1122334455667788UL; для 64-битового long */ x = Ox11223344UL;

p = (unsigned char *) &x;

for (i = 0; i < sizeof(long); i++) pnntf("%x ", *p++); printf("\n"); return 0; }

На 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 x;

?fread(&x, sizeof(x), 1, stdin);

?flifdef BIG_ENDIAN

?/* осуществляем перестановку байтов */

?x = ((x&OxFF) « 8) | ((x»8) & OxFF);

?#endif

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

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

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

Решение проблемы все же существует. Записывайте байты в каноническом порядке, используя переносимый код:

unsigned short x;

putchar(x » 8); /* пишем старший байт */ putchar(x & OxFF); /* пишем младший байт */

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

short x;

x = getchar() « 8; /* читаем старший байт */

x |= getchar() & OxFF; /* читаем младший байт */

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

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

Система X Window воспринимает данные от клиента с любым порядком байтов: считается, что это забота сервера. А система Plan 9, наоборот, сама задает порядок байтов для отправки сообщений файл-серверу (или графическому серверу), а данные запаковываются и распаковываются неким универсальным кодом вроде приведенного выше. На практике определить временные затраты во время исполнения оказывается практически невозможно; можно только сказать, что на фоне ввода-вывода упаковка данных оказывается малозаметной.

Java — язык более высокого уровня, чем С и C++, в нем порядок байтов скрыт совсем. Библиотеки представляют интерфейс Serializable, который определяет, как элементы данных пакуются для передачи.