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

5.2. Хорошие подсказки, простые ошибки

Ой! Что-то случилось. Моя программа "свалилась", напечатала ка­кой-то мусор или, кажется, "зависла". Что мне делать?

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

К счастью, в большинстве своем ошибки просты, и их можно обна­ружить с помощью простых приемов. Изучите улики — неверные ре­зультаты работы и попытайтесь догадаться, как такие результаты могли возникнуть. Посмотрите на отладочную выдачу перед аварий­ным завершением; если возможно, получите у отладчика стек вызо­вов. Теперь вы уже кое-что знаете о том, что именно произошло и где. Остановитесь, подумайте. Как такое могло случиться? Рассуждайте, исходя из состояния "свалившейся" программы, чтобы определить причину.

Процесс отладки включает в себя обратную трассировку (backward reasoning) — прослеживание событий в обратном порядке, как в детекти­ве. Случилось что-то невозможное, и единственное, что известно точ­но, — невозможное случилось. Для того чтобы раскрыть причины, нуж­но мысленно проходить обратный путь от результата к возможной

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

Ищите знакомые ситуации. Спросите себя, известна ли уже вам эта ситуация. "Я уже видел это" — с этой фразы часто начинается понимание, а иногда даже и возникает ответ. Обычные ошибки имеют четко различимые признаки. Например, начинающие программисты на С часто

пишут

? int n;

? scanf("%d", n);

вместо

int n;

scanf("%d", &n);

При такой попытке ввода значения обычно возникает ошибка обращения за пределы доступной памяти. Преподаватели языка С немедленно узнают этот симптом.

Несовпадающие типы и преобразования при вызове printf и scanf рождают бесконечный поток тривиальных ошибок:

? int n;

? double d = PI;

? printf("%d %f\n", d, n);

Признаком этой ошибки иногда бывают абсурдные значения переменных: огромные целые, невероятно большие или невероятно маленькие значения с плавающей точкой. На Sun SPARC эта программа выводит огромное целое и астрономическое число с плавающей точкой (выдача отформатирована, чтобы не выходить за поля страницы):

1074340347 268156158598852001534108794260233396350\

1936585971793218047714963795307788611480564140\

0796821289594743537151163524101175474084764156\

422771408323839623430144.000000

Другой обычной ошибкой является использование %f вместо %lf, когда значение типа double читается с помощью scanf. Некоторые компиляторы ловят такие ошибки, проверяя, соответствуют ли типы аргументов scanf и printf параметрам форматной строки; если вывод всех пред­упреждений компилятора разрешен, то относительно приведенного выше обращения к printf компилятор GNU gcc сообщит

х.с:9: warning: int format, double arg (arg 2)

x.c:9: warning: double format, different type arg (arg 3)

Неинициализированные локальные переменные — еще один источ­ник четко отличимых ошибок. Результатом часто являются слишком большие значения, возникшие из-за мусора, оставшегося в этом месте памяти от другой переменной. Некоторые компиляторы предупредят вас, если вы включите это предупреждение, но часть случаев они отсле­дить все же не могут. Память, выделенная функциями типа malloc, realloc и new, скорее всего, также содержит мусор; обязательно инициа­лизируйте ее.

Проверьте самое последнее изменение. В чем оно заключалось? Если в процессе разработки вы изменяете только один участок за раз, то ошибка, как правило, находится в новом коде или же в участке ста­рого кода, который используется из нового кода. Тщательно посмот­рите на последние изменения, это поможет локализовать проблему. Если ошибка появляется в новой версии, а в старой ее нет, следова­тельно, новый код является частью проблемы. Это означает, что вам следует сохранять как минимум предыдущую версию программы, ту, которую'вы считаете правильной, чтобы можно было сравнить пове­дение версий. Это также означает, что вам следует делать записи об изменениях и исправленных ошибках, чтобы не пришлось переот­крывать эту информацию при попытках исправления ошибок. Здесь будут полезны системы контроля исходных текстов и другие меха­низмы хранения истории.

Не повторяйте дважды ту же самую ошибку. После того как вы исправи­те ошибку, спросите себя, не совершали ли вы подобной ошибки когда-то раньше. Такая история случилась с нами буквально за несколько дней до того, как мы писали эту главу. Для нашего коллеги была написана программа-прототип, которая включала в себя стереотипную конструк­цию для разборки опций:

? for (i = 1; i < argc; i++) {

? if (argv[i][0] != '-') /* аргументы кончились */

? break;

? switch (argv[i][1]) {

? case 'o': /* имя выходного файла */

? outname = argv[i];

? break;

? case ‘f’:

? from = atoi(argv[i]);

? braek;

? case ‘t’:

? to = atoi(argv[i]);

? break;

? …….

Довольно скоро после опробования программы наш коллега сообщил, Вто имя выходного файла всегда начиналось с -о. Это было обидно, но, как оказалось, легко исправимо: код следовало читать так:

outname = &argv[i][2];

Программа была исправлена и отослана обратно, а затем пришла опять с сообщением, что программа не обрабатывала должным образом аргументы типа - f 123: преобразованное числовое значение всегда содержало ноль. Это та же самая ошибка: следующая часть оператора выбора должна была звучать так:

from = atoi(&argv[i][2]);

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

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

Не откладывайте отладку на потом. Чрезмерная торопливость может повредить и в других ситуациях. Не игнорируйте проявившуюся ошибку: отследите ее прямо сейчас, потому что потом она может и не возникнуть. Пример — знаменитая история, случившаяся при запуске космической станции "Mars Pathfinder". После безупречного "приземления" в июле 1997 года компьютеры станции имели обыкновение перезагружаться в среднем один раз в день, и это поставило инженеров в тупик. Когда они отследили ошибку, то поняли, что уже встречались с ней. Во время предпусковых проверок такие перезагрузки случались, но были проигнорированы, потому что инженеры работали над другими вопросами. Теперь они оказались вынуждены решать проблему, когда машина находится на расстоянии десятков миллионов километров, и ис­править ошибку стало значительно труднее.

Пользуйтесь стеком вызовов. Хотя отладчики умеют обращаться с про­граммами и в процессе их работы, все Же одним из основных их при­менений является исследование "посмертного" состояния программы. Номер строки исходного текста, в котором произошла ошибка, или, за­частую, кусок стека вызовов — это самая полезная отладочная информа­ция. Хорошей подсказкой также бывают невероятные значения аргу­ментов (нулевые указатели, огромные целые, тогда как они должны быть небольшими, или отрицательные, когда они должны быть положи­тельными, строки, состоящие из неалфавитных символов).

Вот типичный пример, основанный на обсуждении сортировки из главы 2. Для того чтобы отсортировать массив целых, нужно вызвать qsort с функцией сравнения целых чисел icmp:

? int arr[N];

? qsort(arr, N, sizeof(arr[0]), icmp);

Предположим, что мы по недосмотру передаем вместо icmp функцию сравнения строк scmp:

? int arr[N];

? qsort(arr, N, sizeof(arr[0]), scmp);

Компилятор не может обнаружить несовпадения типов, поэтому непри­ятность ожидает своего часа. Когда мы запускаем программу, она "ва­лится", пытаясь обратиться к неразрешенному адресу. Отладчик dbx вы­дает такую трассировку стека вызовов:

0 strcmp(0x1a2, Ox1c2) ["strcmp.s":31]

1 scmp(p1 = 0x10001048, р2 = ОхЮООЮбс) ["badqs. с": 13]

2 qst(0x10001048, 0x10001074, Ох400Ь20, 0x4) ["qsort.с":147]

3 qsort(0x10001048, Ох1с2, 0x4, Ох400Ь20) ["qsort.с":63]

4 main() ["badqs.с":45]

5 __istart() t"crt1tinit.s":13]

Это означает, что программа "погибла" в функции strcmp; при изучении ситуации становится ясно, что два указателя, переданных этой функ­ции, слишком малы — явное указание на проблему. Строка 13 в нашем тестовом файле badqs. с содержит вызов

return strcmp(v1, v2);

который обнаруживает загубивший вызов и указывает на ошибку.

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

Читайте код перед тем, как исправлять. Один из эффективных, но недооцененных приемов отладки — тщательное чтение и обдумывание вода перед внесением в него исправлений. Порою хочется добраться до клавиатуры и начать редактировать программу, чтобы посмотреть, не исчезнет ли ошибка сама собой. Но все же, скорее всего, вы не знаете, что именно сломано, и измените что-нибудь не то, может быть сломав при этом что-нибудь еще. Распечатанный на бумаге критический участок кода выглядит совсем не так, как на экране, и поощряет потратить больше времени на обдумывание. Однако не печатайте листинги постоянно. На распечатку целой программы вы изведете уйму деревьев, а структуру программы, разбросанной по множеству страниц, гораздо сложнее увидеть. Кроме того, распечатка устареет в тот момент, когда вы начнете вносить изменения.

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

Боритесь с желанием начать исправлять немедленно: подумать — хорошая альтернатива.

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

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