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

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; никаких закономерностей в появлении правильного или неправильного значений не было. В конце концов проблему обнаружили в модуле вычислений с плавающей точкой в одном из процессоров. Программа-калькулятор случайным образом выполнялась то на одном из них, то на другом, и в зависимости от этого ответы были либо верными, либо совершенно

бессмысленными.

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

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