Стратегии отладки, советы и приемы

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

Используйте правильные инструменты

Само собой разумеется, что вы всегда должны использовать лучшие из доступных инструментов. Если вы ищите ошибки сегментации, используйте отладчик, он облегчит ваши страдания. Если вы имеете дело со странными проблемами памяти (или трудно диагностируемыми ошибками сегментации), используйте Valgrind на Linux или Purify для Windows.

Проблемы отладки

Первое, что приходит на ум при отладке, это вопрос: «Мой код слишком сложный?» Иногда мы находим решение проблемы и понимаем, что оно слишком сложное для реализации. Настолько сложное, на самом деле, что может быть проще решить эту проблему по-другому. Когда я вижу, как кто-то пытается отладить сложный код, первое, о чем я хочу спросить: «Есть ли более простое решение?». Часто, когда вы напишите плохой код, у вас появляется представление о том, как должен выглядеть хороший код. Помните, что вы не должны хранить код только потому, что вы его написали!

Хитрость заключается в умении определить, пытаетесь ли вы решить исходную задачу или найти конкретное решение. Если это решение, то вполне возможно, что ваши проблемы не связаны с исходной задачей вообще — может быть, вы слишком много думаете о задаче или неправильно к ней подходите. Например, недавно мне пришлось обработать файл и импортировать некоторые данные для доступа к базе данных для создания прототипа инструмента анализа. Моим первым побуждением было написать скрипт на Ruby, который обращается непосредственно к Access и вставляет все данные в базу с помощью SQL запросов. Когда я посмотрел на поддержку этого в Ruby, я быстро понял, что мое «решение» проблемы займет намного больше времени, чем я предполагал. И я поступил по-другому, написал скрипт, который просто выводит значения в csv файл, и мои данные полностью импортировались за час.

Плохой код

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

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

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

Минимизируйте потенциальные проблемы, избегая синдрома “копировать/вставить”

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

Лучший способ избежать синдрома “копировать/вставить” заключается в использовании в вашем коде функции для инкапсуляции столько раз, сколько это возможно. Некоторых вещей не так легко избежать в C++. Вам придётся писать много циклов независимо от того, что вы делаете, так что вы не можете абстрагироваться от всех циклов. Но если у вас одно и то же тело цикла в нескольких местах, это может быть признаком того, что такой код следует вывести в отдельную функцию. В качестве бонуса, это делает другие будущие изменения в коде проще и позволяет повторно использовать функцию без надобности поиска куска кода для копирования.

Когда копировать код

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

Вторая причина для копирования кода — это длинные имена переменных и плохой текстовый редактор. Самое лучшее решение, как правило, это хороший текстовый редактор с завершением по ключевым словам (автокомплит зарезервированных слов).

Найдите маленькие проблемы раньше, чтобы потом найти большие проблемы

Тестирование на ранней стадии

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

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

Предупреждения компилятора

Многие потенциальные ошибки могут быть обнаружены компилятором. Некоторые из таких ошибок включают в себя использование неинициализированных переменных, случайно заменив проверку на равенство с заданием в условном выражении, или, в C++ — ошибки, связанные с смешением типов, таких как указатели и целые. Так как об этом говорилось ранее, я предлагаю вам самостоятельно найти информацию  о предупреждениях компилятора.

printf лжет

Поскольку ввод/вывод, как правило, буферизуется операционной системой, использование printf в процессе отладки рискованно. По возможности используйте отладчик, чтобы выяснить, в каких строках кода — ошибка, вместо того чтобы находить место путем вставки printf в код. (И остерегайтесь одиноких printf, которые остались после отладки и проскальзывают в финальной версии).

Сброс вывода

Тем не менее, бывают случаи, когда вам действительно нужно отслеживать состояние в лог-файле — возможно, вам просто нужно собрать слишком много данных, и вам нужны данные от запуска программы до момента появления ошибки. Чтобы обеспечить сбор всех данных, обязательно сбросьте его: вы можете использовать fflush в C, или оператор  endl в C++. fflush принимает указатель файла, в который вы пишете, например, чтобы сбросить stderr, можно было бы написать fflush( stderr);

Проверяйте вспомогательные функции

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

Когда ошибка не приводит к немедленному эффекту

Даже если вспомогательная функция не является непосредственным источником проблемы, ее побочные эффекты могут привести к скрытым проблемам. Например, если у вас есть вспомогательная функция, которая может возвращать NULL и вы передаете ее выход в библиотечную функцию, имеющую дело с Cи-строками, вы можете увидеть  ошибку разыменования указателя NULL в strcat, но настоящая ошибка была в глючной функции, которую вы написали ранее (или то, что вы не проверили NULL после его вызова).

Помните, что код может быть использован в более чем одном месте

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

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

Автор: Marienko L.
Дата: 20.10.2012
Поделиться:

Оставить комментарий

Вы должны войти, чтобы оставить комментарий.