Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
kernigan_paik.doc
Скачиваний:
0
Добавлен:
01.07.2025
Размер:
2.91 Mб
Скачать

8.3. Организация программы

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

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

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

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

? #if defined (STDC_HEADERS) | | defined (_LIBC)

? #include <stdlib.h>

? #else

? extern void *malloc(unsigned int);

? extern void *realloc(void *, unsigned int);

? #endif

Защитный стиль приемлем, если он применяется время от времени, а не всегда. Возникает резонный вопрос: а для скольких еще функций из stdlib.h придется писать аналогичный код? В частности, если вы со­бираетесь использовать mаllос и realloc. to явно потребуется еще и free. А что, если тип unsigned int не тождественен size_t — правильному ти­пу аргумента для malloc и realloc? Более того, откуда мы знаем, что STDC_HEADERS и _LIВС определены, и определены корректно? Можем ли мы быть уверенными в том, что не существует другого имени, которое потребует замены для другой среды? Любой условный код вроде этого неполон, а значит — непереносим, поскольку рано или поздно встретит­ся система, не удовлетворяющая его условию, и тогда придется редакти­ровать #ifdef. Если нам удастся решить задачу без помощи условной компиляции, мы избавимся и от проблем, связанных с дальнейшим под­держанием этого кода.

Итак, проблема, которая решается в рассмотренном примере, суще­ствует в реальности. Так как же нам решить ее раз и навсегда? На самом деле нам просто надо предположить, что стандартные заголовочные фай­лы присутствуют во всех системах всегда; если одного из них нет, то это уже не наши проблемы. Но мы можем решить и их; для данного случая достаточно вместе с программой поставить и заголовочный файл, кото­рый определяет malloc, realloc и free в точности так, как этого требует стандарт ANSI С. Такой файл всегда может быть включен полностью вме­сто "заплаток", и мы будем уверены, что нужный интерфейс обеспечен.

Избегайте условной компиляции. Условной компиляцией с помощью «ifdef и подобных ей директив препроцессора трудно управлять, по­скольку информация оказывается рассеянной по всему коду:

? #ifdef NATIVE

? char *astring = "convert ASCII to native character set";

? #else

? #ifdef MAC

? char *astring = "convert to Mac textfile format";

? #else

? #ifdef DOS

? char *astring = "convert to DOS textfile format";

? #else

? char *astring = "convert to Unix textfile format";

? #endif /* ?DOS */

? #endif /* ?MAC */

? #endif /* 7NATIVE */

В этом фрагменте, вообще говоря, лучше было бы использовать #elif после каждого определения — тогда бы не было такого ненужного скоп­ления #endif в конце. Однако главная проблема вовсе не в этом, а в том, что, несмотря на все старания, код плохо переносим, потому что он ведет себя по-разному в разных системах, а для работы в каждой новой систе­ме должен быть дополнен новым #ifdef. Одна-единственная строка с уни­фицированным текстом (но на самом деле столь же информативным) была бы гораздо удобнее, проще и переносимее:

char *astring = "convert to local text format";

Для этой строки никаких условий не нужно, она будет выглядеть одина­ково во всех системах.

Смешивание управляющей логики времени компиляции (определяе­мой выражениями #ifdef) и времени исполнения приводит к еще более трудно воспринимаемому коду:

? #ifndef DISKSYS

? for (i = 1; i <= msg->dbgmsg.msg_total; i++)

? #endif

? #ifdef DISKSYS

? i = dbgmsgno;

? if (i <= msg->dbgmsg.msg_total)

? #endif

? {

? ……

? if (msg->dbgmsg.msg_total == i)

? #ifndef DISKSYS

? break; /* больше ожидаемых сообщений нет */

? еще около 30 строк с условной компиляцией

? #endif

? }

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

? #ifdef DEBUG

? printf(...);

? #endif

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

enum { DEBUG = 0 };

…….

if (DEBUG) {

printf(...);

}

Если DEBUG есть ноль, то большинство компиляторов не сгенерируют для приведенного фрагмента никакого кода, но при этом они еще и проверят синтаксис. Секция с #ifdef, наоборот, может содержать синтаксические ошибки, которые сорвут компиляцию, как только соответствующее ус­ловие #ifdef окажется выполнено.

Иногда условия компиляции содержат большие блоки кода:

#ifdef notdef /* неопределенный символ */

….

#endif

или

#if 0

….

#endif

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

Начиная адаптировать программу к новой среде, не делайте копии всей программы, а перерабатывайте исходный код. Скорее всего, вам придет­ся вносить изменения в основное тело программы, и при редактирова­нии копии вы через какое-то время получили бы новую, отличающуюся от исходной версию. Изо всех сил стремитесь к тому, чтобы у вас суще­ствовала единственная версия программы; при необходимости подстро­иться под конкретную систему старайтесь вносить изменения таким образом, чтобы они работали во всех системах. Измените внутренние интерфейсы, если надо, но не нарушайте целостности кода; не пытайтесь решить проблему с помощью #ifdef. При таком подходе каждое измене­ние сделает вашу программу все более переносимой, а не более специа­лизированной. Сужайте пересечение, а не расширяйте объединение.

Мы уже привели много доводов против использования условной ком­пиляции, но не упоминали еще о главной проблеме: ее практически невоз­можно оттестировать. Каждое выражение #ifdef разделяет всю програм­му на две по отдельности компилируемые программы, и определить, все ли возможные варианты программ были скомпилированы и провере­ны, очень сложно. Если в один блок #ifdef было внесено изменение, то может статься, что изменить надо и другие блоки, но проверить эти из­менения можно будет только в той среде, которая вызовет эти #ifdef к исполнению. Точно так же, если мы добавляем блок #ifdef, то трудно изолировать это изменение, то есть определить, какие еще условия дол­жны быть учтены и в каких еще местах должен быть изменен код. Нако­нец, если некий блок кода должен быть опущен в соответствии с услови­ем, то компилятор его просто не видит, и проверить этот блок можно только в соответствующей конфигурации. Вот небольшой пример по­добной проблемы — программа компилируется, если _МАС определено, и отказывается это делать в противном случае:

#ifdef _MAC

printf("This is Macintosh\r");

#else

This will give a syntax error on other systems

#endif

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

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

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

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

const int DEBUG = 0;

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

/* или final boolean DEBUG = false; */

if (DEBUG) {

…..

}

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

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

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]