- •1. Стиль 10
- •3. Проектирование и реализация 63
- •4. Интерфейсы 85
- •5. Отладка 115
- •6. Тестирование 134
- •7. Производительность 157
- •8. Переносимость 180
- •9. Нотация 203
- •Введение
- •Брайан в. Керниган
- •1.1. Имена
- •1.2. Выражения
- •Упражнение 1 -6
- •1.3. Стилевое единство и идиомы
- •1.4. Макрофункции
- •1.5. Загадочные числа
- •1.6. Комментарии
- •1.7. Стоит ли так беспокоиться?
- •Дополнительная литература
- •2.1. Поиск
- •2.2. Сортировка
- •2.3. Библиотеки
- •2.4. Быстрая сортировка на языке Java
- •2.5. "О большое"
- •2.6. Динамически расширяемые массивы
- •2.7. Списки
- •Упражнение 2-8
- •2.8. Деревья
- •Упражнение 2-15
- •2.10. Заключение
- •Дополнительная литература
- •Проектирование и реализация
- •3.1. Алгоритм цепей Маркова
- •3.2. Варианты структуры данных
- •3.3. Создание структуры данных в языке с
- •3.4. Генерация вывода
- •3.5.Java
- •Into the air. When water goes into the air it
- •3.7. Awk и Perl
- •3.8. Производительность
- •3.9. Уроки
- •Дополнительная литература
- •4. Интерфейсы
- •4.1. Значения, разделенные запятой
- •4.2. Прототип библиотеки
- •4.3. Библиотека для распространения
- •Упражнение 4-4
- •4.5 Принципы интерфейса
- •4.6. Управление ресурсами
- •4.7. Abort, Retry, Fail?
- •4.8. Пользовательские интерфейсы
- •Дополнительная литература
- •5. Отладка
- •5.1. Отладчики
- •5.2. Хорошие подсказки, простые ошибки
- •5.3, Трудные ошибки, нет зацепок
- •5.4. Последняя надежда
- •5.5. Невоспроизводимые ошибки
- •5.6. Средства отладки
- •5.7. Чужие ошибки
- •5.8. Заключение
- •Дополнительная литература
- •6. Тестирование
- •6.1. Тестируйте при написании кода
- •6.2. Систематическое тестирование
- •6.3. Автоматизация тестирования
- •6.4. Тестовые оснастки
- •6.5. Стрессовое тестирование
- •6.6. Полезные советы
- •6.7. Кто осуществляет тестирование?
- •6.8. Тестирование программы markov
- •6.9. Заключение
- •Дополнительная литература
- •7.Производительность
- •7.1. Узкое место
- •7.2. Замеры времени и профилирование
- •7.3. Стратегии ускорения
- •7.4. Настройка кода
- •7.5. Эффективное использование памяти
- •7.6. Предварительная оценка
- •7.7. Заключение
- •Дополнительная литература
- •8. Переносимость
- •8.1. Язык
- •8.2. Заголовочные файлы и библиотеки
- •8.3. Организация программы
- •8.4. Изоляция
- •8.5. Обмен данными
- •8.6. Порядок байтов
- •8.7. Переносимость и внесение усовершенствований
- •8.8. Интернационализация
- •8.9. Заключение
- •Дополнительная литература
- •9.1. Форматирование данных
- •9.2. Регулярные выражения
- •Упражнение 9-12
- •9.3. Программируемые инструменты
- •9.4. Интерпретаторы, компиляторы и виртуальные машины
- •9.5. Программы, которые пишут программы
- •9.6. Использование макросов для генерации кода
- •9.7. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
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) {
…..
}
При каких обстоятельствах компилятор проверяет синтаксис? Когда он генерирует код?
Если у вас есть доступ к разным компиляторам, поэкспериментируйте с ними и сравните результаты.
