- •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. Компиляция "налету"
- •Дополнительная литература
- •Интерфейсы
- •Отладка
- •Тестирование
- •Производительность
- •Переносимость
5.4. Последняя надежда
Что делать, если вы все перепробовали, но ничего не помогает? Может быть, как раз наступило время взять хороший отладчик и пройтись пo программе. Если ваша мысленная модель работы программы по какой-то причине попросту не соответствует действительности и вы смотрите в совершенно другом направлении, чем нужно, или же смотрите в правильном направлении, но в упор не видите проблему, то отладчик заставит вас изменить ход мыслей. Такие ошибки в "мысленной модели" наиболее сложны, и помощь со стороны машины здесь бесценна,
Иногда источник непонимания очень прост: неверный приоритет операторов, неверный оператор, выравнивание, не соответствующее действительной структуре программы, или же ошибка области видимости когда локальная переменная прячет под собой глобальную или же глобальная переменная вторгается в локальную область видимости. Например, программисты часто забывают, что & и | имеют меньший приоритет чем == и ! =. Они пишут так:
? if (х & 1 == 0)
? …..
и не могут понять, почему результат этого выражения — всегда "ложь" Иногда неверное движение пальца при наборе превращает одиночный символ = в двойной и наоборот:
? while ((с == getcharQ) != EOF)
? if (с = ‘\n')
? break;
Или после редактирования случайно остается лишний код:
? for (i = 0; i < n; i++);
? a[i++] = 0;
Или проблему создает спешка при наборе текста кода:
? switch (с) {
? case '<’ :
? mode = LESS;
? break;
? case '> ':
? mode = GREATER;
? break;
? defualt:
? mode = EQUAL;
? break;
? }
Иногда ошибка случается из-за неправильного порядка аргументов в вызове процедуры. Если проверка типов не может помочь, например:
? memset(p, n, 0); /* записать n нулей в р */
вместо
memset(p, 0, n); /* записать n нулей в р */
то транслятор такой ошибки не обнаружит.
Иногда незаметно для вас что-то изменяется, например глобальные или общие переменные, а вы об этом ничего не знаете, пока какая-нибудь функция не обратится к ним.
Иногда в алгоритме или структуре данных есть фатальная ошибка, которую вы просто не замечаете. Во время подготовки материала по цеп-кным спискам мы написали набор функций, создающих новые элементы, вставляющих эти элементы в начало и конец списка, и т. п.; эти функции приведены во второй главе. Конечно же, мы написали тестовые программы, чтобы убедиться, что все правильно. Первые несколько тестов работали, тогда как один эффектно "валился". В сущности, тестовая программа была такой:
? while (scanf("%s %d", name, &value) != EOF) {
? p = newitem(name, value);
? list1 = addfront(list1, p);
? list2 = addend(list2, p);
? }
? for (p = listl; p != NULL; p = p->next)
? printf("%s %d\n", p->name, p->value);
Как выяснилось, поразительно трудно заметить, что в первом цикле один и тот же узел р добавлялся в два списка сразу, поэтому к тому времени, когда надо было печатать, указатели оказывались безнадежно испорченными.
Такие ошибки искать довольно трудно, потому что мозг просто обходит их. Отладчик помогает здесь, вынуждая вас двигаться в другом направлении, именно в том, в котором движется программа, а не в том, в котором : вы думаете. Часто проблема заключается в структуре всей программы, и для того, чтобы увидеть ошибку, требуется вернуться к исходным предпосылкам.
Заметьте, кстати, что в примере со списками ошибка была в тестовом коде, и поэтому ее гораздо сложнее обнаружить. К сожалению, бывает поразительно легко потерять кучу времени, отыскивая несуществующие ошибки, потому что тестовая программа была ошибочной, или же тестировалась не та версия программы, или перед тестированием не были проведены обновление программы и ее перекомпиляция.
Если вы, приложив массу усилий, не смогли найти ошибку, передохните. Проветрите голову, займитесь чем-нибудь посторонним. Поговорите с приятелем, попросите помощи. Ответ может появиться сам собой, из ниоткуда, но даже если это и не так, вы хотя бы не застрянете в той же самой колее во время следующей отладочной сессии.
Крайне редко проблема действительно заключается в компиляторе, библиотеке, операционной системе или даже в "железе", особенно если что-нибудь изменилось в конфигурации непосредственно перед тем, как появилась ошибка. Никогда нельзя сразу начинать винить все перечисленное, но если все остальные причины устранены, то можно начать думать в этом направлении. Однажды мы переносили большую программу форматирования текста из Unix-среды на PC. Программа отлично ском-пилировалась, но вела себя очень странно: теряла почти каждый второй символ входного текста. Нашей первой мыслью было, что это как-то свя зано с использованием 16-битовых целых вместо 32-битовых или, може-быть, с другим порядком байтов в слове. Печатая" символы, полученньк во входном потоке, мы наконец нашли ошибку в стандартном заголовочном файле ctype.h, поставлявшемся вместе с компилятором. В этом файле функция isprint была реализована в виде макроса:
? #define isprint(c) ((с) >= 040 && (c) < 0177)
а в главном цикле было написано так:
? while (isprint(c = getchar()))
? ……..
Каждый раз, когда входной символ был пробелом (восьмеричное 040, плохой способ записи ' ') или стоял в кодировке еще дальше, а это почти всегда так, функция getchar вызывалась еще раз, потому что макрос вычислял свой аргумент дважды, и первый входной символ пропадал. Исходный код был не столь чистым, как следовало бы, — слишком сложное условие цикла, — но заголовочный файл был непростительно неверен.
Сейчас все еще можно встретиться с такой проблемой: вот этот макрос можно найти в заголовочных файлах одного современного производителя:
? #define __iscsym(c) (isalnum(c) | | ((с) == ‘_ '))
Обильным источником ошибок являются "утечки" памяти, когда забывают освободить неиспользуемую память. Другая проблема — забывают закрывать файлы до тех пор, пока не переполнится таблица открытых файлов и программа больше не сможет открыть ни одного. Программы с утечками таинственным образом отказываются работать, потому что у них заканчивается тот или иной ресурс, но конкретную ошибку бывает невозможно воспроизвести.
Иногда отказывает само "железо". Ошибка в вычислениях с плавающей точкой в процессоре Pentium в 1994 году, которая приводила к неверным ответам при некоторых вычислениях, была обширно освещена в печати и довольно дорого обошлась. После того как она была обнаружена, ее, конечно же, можно было легко воспроизвести. Одна из самых странных ошибок, которую мы когда-либо видели, содержалась в программе-калькуляторе, некогда работавшем на двухпроцессорной машине. Иногда выражение 1 /2 выдавало результат 0. 5, а иногда — постоянно появляющееся, но совершенно неправильное значение 0.7432; никаких закономерностей в появлении правильного или неправильного значений не было. В конце концов проблему обнаружили в модуле вычислений с плавающей точкой в одном из процессоров. Программа-калькулятор случайным образом выполнялась то на одном из них, то на другом, и в зависимости от этого ответы были либо верными, либо совершенно
бессмысленными.
Много лет назад мы использовали машину, температуру которой можно было оценить, исходя из количества младших битов, бывших неправильными при вычислениях с плавающей точкой. Одна из ее плат была плохо закреплена, и, когда машина нагревалась, эта плата сильнее выдвигалась из своего разъема и больше битов данных отключалось от основной платы.
