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

a[i++] = 0; с = *р++; *s++ = *t++;

Не сравнивайте char с EOF. Всегда используйте sizeof для вычисления размера типов и объектов. Никогда не сдвигайте вправо знаковые значения. Убедитесь, что тип данных достаточно велик для диапазона значений, которые вы собираетесь в нем хранить.

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

Поскольку язык, воспринимаемый различными компиляторами, может несколько отличаться от стандарта, тот факт, что ваша программа компилируется одним компилятором, не дает гарантии даже того, чтс она корректна синтаксически. А вот если несколько компиляторов принимают ваш код, значит, все не так плохо. Мы компилировали каждув программу, приведенную в книге, на трех компиляторах С для трех различных операционных систем (Unix, Plan 9, Windows) и на паре компиляторов C++. При таком подходе были найдены десятки ошибок переносимости — никакое самое пристальное изучение программ чeлoвeкoм не смогло бы найти их все. Исправлялись же все ошибки тривиально.

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

Заголовочные файлы и библиотеки

Заголовочные файлы и библиотеки предоставляют возможности, расширяющие базовый язык. Например, ввод и вывод осуществляются с помощью библиотек stdio в С, lost ream в C + + и Java, io в Java. Строго говоря, эти элементы не являются частью языка, но они определен! вместе с языком и представляют собой составную часть любой среды, поддерживающей этот язык. Однако, поскольку библиотеки покрывают широкий спектр возможностей и нередко имеют дело со специфическими вопросами устройства операционных систем, в их использовании может крыться причина плохой переносимости программы.

Используйте стандартные библиотеки. Здесь, как и при обсуждении самого языка, применимо то же самое общее правило: придерживайтесь стандарта, отдавая предпочтение старым, устоявшимся компонентам. В С определена стандартная библиотека функций ввода-вывода, операций со строками, проверок символов, выделения памяти под данные и ряда других задач. Если вы ограничите взаимодействие своей программы с операционной системой использованием этих функций, с хорошей долей уверенности можно считать, что программа будет вести себя одинаково при переходе от одной операционной системы к другой. Однако и здесь надо соблюдать осторожность: существуют различные реализации библиотек, и некоторые из них имеют отклонения от стандарта.

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

Заголовочные файлы и описания пакетов описывают интерфейс со стандартными функциями. Один из недостатков многих заголовочных файлов состоит в том, что в них приводятся описания сразу для нескольких языков. Нередко можно встретить один файл вроде stdiо. h с описаниями одновременно для старого (до стандарта ANSI) С, ANSI С и даже C++ компиляторов. Такой файл получается очень громоздким — в нем много директив условной компиляции вроде #if и ffif def. Язык препроцессора не слишком гибок, поэтому такие файлы получаются довольно сложными для восприятия; иногда в них даже содержатся ошибки.

Ниже приведен фрагмент заголовочного файла одной из систем, причем он еще гораздо лучше многих, по крайней мере нормально отформатирован:

?# ifdef _OLD_C

?extern int fread();

?extern int fwrite();

?# else

?# if defined(__STDC__) || defined(__cplusplus)

?extern size_t fread(void*, size_t, size_t, FILE*);

?extern size_t fwrite(const void*, size_t, size__t, FILE*);

?# else /* not __STDC__ || __cplusplus */

?extern size_t fread();

?extern size_t fwriteO;

?# endif /* else not „_STDC__ | _cplusplus */

?#endif

Даже из этого сравнительно простого примера видно, что заголовочные файлы и программы, структурированные подобным образом, получаются довольно запутанными и сопровождать их достаточно сложно. Возможно, проще использовать отдельный заголовочный файл для каждого компилятора или среды. При этом придется сопровождать множество отдельных файлов, но каждый из них будет предназначен только для использования в конкретной конфигурации, что уменьшит вероятность появления ошибок вроде включения функции st rdup в среду, строго поддерживающую стандарт ANSI С.

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

?/* в stdio.h иногда входит wprintf, переопределим его: */

?tfdefine wprintf stdio_w'printf

?Jtinclude <stdio. h>

?#undef wprintf

?/* далее можно использовать нашу wprintf() ... */

Этот фрагмент изменяет все появления wprintf в заголовочном файле на stdio_wp rintf, так что теперь они не повлияют на нашу версию. Теперь мы можем использовать нашу wprintf, не изменив ее имени, правда, при этом неизбежно появится некая путаница, а также риск, что подключенная библиотека будет вызывать нашу wprintf, подразумевая обращение к своей версии. Для одной функции проблемы, может, и невелики, но уже для нескольких лучше придумать более радикальное решение. Всегда комментируйте назначение конструкции; без крайней необходимости не ухудшайте ее добавлением условной компиляции. Если в некоторых средах определена wprintf, то стоит считать, что она определена во всех; тогда единственный разумный выход — переименовать ее, избавившись при этом от выражения tfifdef. Нередко проще не превозмогать трудность, а подстраиваться под нее; да это и безопаснее, вот почему мы решили переименовать свою функцию в weprintf.

Даже если вы следуете всем правилам и неясностей со средой не возникает, все равно вполне возможно появление ошибок. Так, можно ошибиться, предположив, что какая-нибудь ваша излюбленная возможность одинакова во всех системах. К примеру, ANSI С определяет шесть сигналов, которые можно поймать с помощью signal, в стандарте POSIX их определено 19, а большинство"систем Unix поддерживает 32 и более. Если вы хотите использовать сигнал, отличный от описанного в ANSI С, вам придется выбирать между функциональностью и переносимостью, так что сами решайте *ITO для вас важнее.

Существует болыпсте количество других стандартов, не являющихся частью определения языка: среди них можно назвать интерфейсы операционных систем и сетей, графические интерфейсы и тому подобные вещи. Некоторые стандарты распространяются на несколько систем — например POSIX; другие определены исключительно для одной системы, например различные API Microsoft Windows. Здесь можно еще раз повторить наши главные советы: ваша программа станет более переносимой, если вы выберете самые распространенные и устоявшиеся стандарты и будете пользоваться самыми важными и общепринятыми их свойствами.

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

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

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

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

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

Следующий небольшой фрагмент пытается справиться с системой, в которой по некоторым причинам нет стандартного заголовочного файлa 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 придется писать аналогичный код? В частности, если вы собираетесь использовать mall ос и real loc, то явно потребуется еще и free. А что, если тип unsigned int нетождественен size_t — правильному типу аргумента для malloc и realloc? Более того, откуда мы знаем, что STOC_HEADERS и _LIBC определены, и определены корректно? Можем ли мы быть уверенными в том, что не существует другого имени, которое потребует замены для другой среды? Любой условный код вроде этого неполон, а значит — непереносим, поскольку рано или поздно встретится система, не удовлетворяющая его условию, и тогда придется редактировать #ifdef. Если нам удастся решить задачу без помощи условной компиляции, мы избавимся и от проблем, связанных с дальнейшим поддержанием этого кода.

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

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

?#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 /* ?NATIVE */

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

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

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

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

?#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

?}

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

?ftifdef DEBUG

?printf(...);

?tfendif

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

enum { DEBUG = 0 }; if (DEBUG) {

printf(...);

}

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

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

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

или

#if О tfendif

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

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

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

flifdef _MAC

printf("This is Macintosh\r");