Тестирование и отладка - Язык Паскаль и начала программирования

Программирование: введение в профессию. 1: Азы программирования - 2016 год

Тестирование и отладка - Язык Паскаль и начала программирования

2.16.1. Отладка в жизни программиста

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

Для начала мы попытаемся сформулировать ряд базовых принципов, связанных с отладкой, и сделаем это в такой форме, что они, возможно, покажутся вам шуткой; вы вскоре сами убедитесь, что в этой шутке доля шутки совсем незначительна, а всё остальное — самая настоящая лютая правда. Итак:

• ошибка всегда есть;

• ошибка всегда не там;

• если вы точно знаете, где ошибка, то у ошибки может оказаться другое мнение;

• если вы считаете, что программа должна работать, то самое время вспомнить, что “должен” — это когда взял взаймы и не отдал;

• если отладка — это процесс исправления ошибок, то написание программы — это процесс их внесения;

• сразу после обнаружения ошибки дело всегда выглядит безнадёжным;

• найденная ошибка всегда кажется глупой;

• чем безнадёжнее всё выглядело, тем глупее кажется найденная ошибка;

• компьютер делает не то, чего вы хотите, а то, о чём вы попросили;

• корректная программа работает правильно в любых условиях, некорректная — тоже иногда работает;

• и лучше бы она не работала;

• если программа работает, то это ещё ничего не значит;

• если программа “свалилась”, надо радоваться: ошибка себя проявила, значит, её теперь можно найти;

• чем громче грохот и ярче спецэффекты при “падении” программы, тем лучше — заметную ошибку искать гораздо проще;

• если ошибка в программе точно есть, а программа всё-таки работает, вам не повезло — это самый противный случай;

• ни компилятор, ни библиотека, ни операционная система ни в чём не виноваты;

• никто не хочет вашей смерти, но если что — никто не расстроится;

• на самом деле всё совсем не так плохо — всё гораздо хуже;

• первая написанная строчка текста будущей программы делает этап отладки неизбежным;

• если вы не готовы к отладке — не начинайте программировать;

• компьютер не взорвётся; но большего вам никто не обещал.

В компьютерных классах часто (особенно на контрольных работах и зачётах) можно наблюдать студентов, которые, написав некий текст на языке программирования, добившись с горем пополам успешной компиляции и убедившись, что полученный результат никак не отвечает поставленной задаче, на этом какую-либо конструктивную деятельность прекращают, переключаясь, например, на другую задачу — как правило, с аналогичным результатом на выходе. Столь странный выбор стратегии они обычно объясняют сакраментальной фразой “ну, я её написал, а она не работает”, причём произносится с интонацией, подразумевающей, что во всём виновата сама программа, а ещё преподаватель, компьютер, погода в Африке, посол Аргентины в Швеции или буфетчица из студенческой столовой, но уж точно не говорящий — ведь он же написал программу.

В такой ситуации следует сразу же вспомнить один простой принцип: компьютер делает ровно то, что написано в программе. Этот факт кажется банальным, но из него немедленно следует второй, а именно: если программа работает неправильно, то она неправильно написана. С учётом этого утверждение “я написал программу” требует уточнения: правильнее будет сказать “я написал неправильную программу”.

Ясно, что написание неправильной программы, пусть даже успешно проходящей компиляцию, уж точно не является сколько-нибудь достойным внимания делом; в конце концов, простейший текст на Паскале, успешно проходящий компиляцию, состоит всего из двух слов и одной точки: “begin end.” Конечно, эта программа не решает поставленную задачу — но ведь и та, которую “написали, а она не работает”, тоже ничего не решает, и чем же она в таком случае лучше?

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

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

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

Будучи психологически готовым к отладке, программист рационально рассчитывает свои силы и время, так что ошибки, обнаруженные при первом запуске программы, его не обескураживают: так и должно быть! Если мало-мальски сложная программа при первых запусках не проявляет ошибок — это довольно странно. Проблема новичка может оказаться в том, что, забыв о предстоящей отладке, он всё своё время и силы потратил на написание первой версии текста; когда дело доходит до самого интересного, ни сил, ни времени уже нет.

Альпинисты, штурмующие серьёзные горы, твёрдо следуют одному важнейшему принципу: цель восхождения не в том, чтобы достичь вершины, а в том, чтобы вернуться обратно. Те, кто про этот принцип забывают, зачастую, достигнув вожделенной вершины, потом гибнут на спуске. Конечно, с программированием всё не столь жестоко — во всяком случае, гибель вам здесь не грозит; но если ваша цель — написать программу, которая делает, что от неё требуется, то вам нужна готовность потратить две трети времени и сил именно на отладку, а не на что-то другое.

Избежать отладки невозможно, но соблюдение некоторых достаточно простых правил может её изрядно облегчить. Итак, самое главное: старайтесь проверять работу отдельных частей программы по мере их написания. Здесь на вас работают сразу два закона: во- первых, только что написанный код отлаживать гораздо проще, чем тот, который вы уже успели подзабыть; во-вторых, с ростом объёма текста, который нужно отлаживать, сложность отладки растёт нелинейно. Кстати, из этого правила вытекает довольно очевидное следствие: старайтесь разбивать программу на подпрограммы так, чтобы они как можно меньше зависели друг от друга; помимо других выгод, этот подход позволит упростить раздельное тестирование частей вашей программы. Можно предложить ещё более общий принцип: когда вы пишете код, думайте о том, как вы будете его отлаживать. Отладка не прощает безалаберности, проявленной на стадии кодирования; даже такое “безобидное” нарушение стиля оформления, как короткое тело оператора ветвления или цикла, оставленное на одной строчке с заголовком оператора, может вам стоить изрядного количества впустую испорченных нервов.

Второе правило мы уже видели в начале параграфа в виде простой и ёмкой фразы: ошибка всегда не там. Если у вас возникла “интуитивная уверенность”, что к тому эффекту, который вы наблюдаете, может привести, конечно же, только ошибка вот в этой процедуре, вот в этом цикле, вот в этом фрагменте — не верьте своей интуиции. В целом интуиция — прекрасная штука, но во время отладки программ она не работает. Объясняется это очень просто: если бы ваша интуиция чего-то стоила против данной конкретной ошибки, то вы бы эту ошибку не допустили. Итак, прежде чем пытаться исправить тот или иной фрагмент, вам необходимо объективно (а не “интуитивно”) убедиться в том, что ошибка находится именно здесь. Помните: в таком месте программы, где вы знаете, что можно ошибиться, вы скорее всего не ошибётесь; напротив, самые заковыристые ошибки оказываются как раз там, где вы никак не могли их ожидать; между прочим, именно поэтому они там и оказываются.

К объективным методам локализации ошибок относятся отладочная печать и пошаговое выполнение под управлением программы-отладчика; если вы надеетесь без них обойтись, лучше вообще не начинайте писать программы; и здесь мы подходим к третьему правилу: метод пристального взгляда в ходе отладки практически не работает. Сколько бы вы ни пялились в свой текст, результат будет выражаться фразой “вроде всё правильно, почему же она не работает?!” Опять же, причина здесь достаточно очевидна: если бы вы могли увидеть собственную ошибку, вы бы не ошиблись. Можно назвать ещё два соображения в пользу неэффективности “пристального взгляда”: наиболее внимательно вы будете просматривать те фрагменты своего кода, где ожидаете обнаружить ошибку, а она, как мы уже знаем, наверняка не там; плюс к тому здесь включается такое хорошо известное явление, как “замыленность взгляда” — даже глядя прямо на ту строчку программы, в которой допущена ошибка, вы вряд ли ошибку заметите. Причину этого эффекта тоже несложно понять: вы сами только что написали этот фрагмент кода, задействовав некие соображения, которые кажутся вам правильными, и, как следствие, сам фрагмент кода продолжает вам казаться правильным, даже если на самом деле содержит ошибку. Итак, не тяните резину и не тратьте драгоценное время на “внимательное изучение” собственного кода: отлаживать программу всё равно придётся.

Отметим здесь ещё один момент: переписывание программы заново — в целом дело хорошее, но избежать отладки это вам не поможет; скорее всего, вы просто снова наделаете ошибок в тех же местах. Что касается переписывания заново отдельных фрагментов программы, то с этим всё ещё хуже: ошибка наверняка окажется не в том фрагменте, который вы решите переписать.

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

Ещё один момент, который стоит учитывать: если вы сами не можете найти ошибку в своей программе, то никто другой её тем более не найдёт. Студенты часто задают один и тот же вопрос: “а почему моя программа не работает?” Ваш покорный слуга, слыша этот вопрос, обычно, в свою очередь, спрашивает, за кого его изволят держать: за экстрасенса, за телепата или за ясновидящего. Разобраться в вашей программе стороннему человеку в большинстве случаев сложнее, чем написать аналогичную программу с нуля. Кроме того, это же ваша программа; сами наворотили, сами разбирайтесь. Интересно, что в подавляющем большинстве случаев студент, задающий такой вопрос, даже не пытался ничего сделать для отладки своей программы.

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

2.16.2. Тесты

Если вы обнаружили, что ваша программа содержит ошибку — значит, вы по меньшей мере один раз запустили её и, скорее всего, подали ей на вход какие-то данные; впрочем, последнее не обязательно, в некоторых случаях программы “падают” сразу после старта, не успев ничего прочитать. Так или иначе, вы уже приступили к тестированию вашей программы; об организации этого дела тоже стоит сказать пару слов.

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

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

Чтобы понять, в чём такой студент неправ и как действовать правильно, вспомним, что поток стандартного ввода не обязательно связан с клавиатурой; при запуске программы мы можем сами решить, откуда она будет читать информацию. Мы уже использовали это, причём речь шла о тестировании очень простых программ; даже для программы, которой на вход требуется подать всего одно целое число, мы предпочли не вводить это число каждый раз, а воспользовались командой echo. Конечно, число всё-таки приходится набирать, когда мы формируем команду, но сама команда остаётся в истории, которую для нас запоминает командный интерпретатор, так что второй раз нам то же число набирать не нужно: вместо этого мы воспользуемся “стрелкой вверх” или поиском через Ctrl-R (см. § 1.4.6), чтобы повторить команду, которую уже вводили.

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

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

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

Когда программа будет написана и пройдёт компиляцию, дальнейшие действия начинающего программиста, никогда не задумывавшегося о правильной организации тестирования, могут выглядеть так:

В большинстве случаев начинающие на этом успокаиваются, решив, что программа “правильная”, но задача сокращения дроби не столь проста, как кажется на первый взгляд. Если программу написать “в лоб”, она, скорее всего, не будет работать для отрицательных чисел. Об этом нашему начинающему может сказать его более опытный товарищ или, если дело происходит на занятиях в классе — преподаватель; попробовав запустить свою программу и подать ей на вход что-нибудь “с минусом”, её автор может убедиться, что старшие товарищи правы и программа, к примеру, “зациклилась” (именно это произойдёт с простейшей реализацией алгоритма Евклида, которая не учитывает особенностей работы операции mod для отрицательных операндов). Конечно, исправить программу особых проблем не составляет, важнее другое: любые исправления могут “сломать” то, что до этого работало, так что новую версию программы придётся тестировать с самого начала, то есть тестовые запуски, которые уже были проделаны, придётся повторить, каждый раз вводя числа с клавиатуры.

Как уже, скорее всего, догадался читатель, тестирование в исполнении более опытного программиста могло бы выглядеть так:

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

Чтобы понять, как правильно организовать тестирование программы, давайте для начала заметим, что каждый тест у нас состоит из четырёх чисел: два из них программе подаются на вход, а два других нужны, чтобы сравнить с ними напечатанный программой результат. Такой тест можно записать в одну строчку; так, три теста из нашего примера выражаются строками

Осталось придумать какой-нибудь механизм, который, имея набор тестов в таком виде, сам, без нашего участия запустит тестируемую программу нужное число раз, подаст ей на вход тестовые данные и проверит правильность результатов. В нашей ситуации самый простой способ добиться этого — написать скрипт на языке командного интерпретатора; если вы не помните, как это делается, перечитайте § 1.4.13.

Чтобы понять, как наш скрипт будет выглядеть, представим себе, что четыре числа, составляющих тест, располагаются в переменных $а, $b, $с и $d. “Прогнать” тест можно командой “echo $а $b │ ,/frcancel”; но нам нужно не просто запустить программу, а сравнить результат с ожидаемым, для чего результат нужно тоже поместить в переменную. Для этого можно воспользоваться присваиванием и “обратными апострофами”, которые, как мы помним, подставляют вместо себя результат выполнения команды:

Результат, попавший в переменную $res, можно сравнить с ожидаемым, и если обнаружено несовпадение, сообщить об этом пользователю:

Последовательно “загнать” в переменные $а, $b, $с и $d числа из тестов нам поможет встроенная в интерпретатор команда read; в качестве параметров эта команда принимает имена переменных (без знака “$”), читает из своего потока ввода строку, разбивает её на слова и “раскладывает” эти слова по заданным переменным, причём если слов оказалось больше, то в последнюю переменную попадёт весь остаток строки, состоящий из всех “лишних” слов. Команда read обладает полезным свойством: если очередную строку прочитать удалось, она завершается успешно, а если поток кончился — неуспешно. Это позволяет с её помощью организовать цикл while примерно так:

В теле такого цикла переменные $а, $b, $с и $d последовательно принимают своими значениями первое, второе, третье и четвёртое слово из очередной строки. Отметим, что каждый наш тест как раз представляет собой строку из четырёх слов (в роли слов выступают числа, но они ведь тоже состоят из символов; в командно-скриптовых языках нет ничего, кроме строк). В теле цикла мы поместим приведённый выше if, прогоняющий отдельный тест, и останется только придумать, как подать получившейся конструкции на стандартный ввод последовательность наших тестов. Для этого можно воспользоваться перенаправлением вида “документ здесь”, которое делается с помощью знака “<<”. После этого знака ставится некоторое слово (“стоп-слово”), а затем пишется текст, который следует подать на вход команде, и завершается этот текст строкой, состоящей целиком из стоп-слова.

Всё вместе будет выглядеть примерно так:

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

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

Если очередной тест выявил ошибку в программе, не торопитесь бросаться что-то исправлять. Для начала стоит подумать, нельзя ли упростить тест, на котором проявилась ошибка, то есть нельзя ли написать тест, проявляющий ту же ошибку, но при этом более простой. Конечно, это не значит, что более сложный тест при этом нужно выбросить. Тесты вообще не надо выбрасывать. Но чем тест проще, тем меньше факторов, которые могут повлиять на работу программы, и тем легче будет найти ошибку. Последовательно упрощая один тест, вы можете создать целое семейство тестов, причём, возможно, самые простые из них уже не будут проявлять ошибку. Это не повод останавливаться: попробуйте взять самый простой из тестов, который всё ещё “ошибается”, и упростить его в каком-нибудь другом направлении. В любом случае все созданные тесты, вне зависимости от того, проявляют ли они прямо сейчас какую-то ошибку или нет, ценны для дальнейшей отладки: одни могут указать путь поиска ошибки, другие — показать, где ошибку искать точно не надо.

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

При этом подходе тесты в основном пишут не для программы целиком, а для каждой её процедуры и функции, для подсистем, включающих несколько связанных между собой процедур и т. д. Следование принципу “test first” позволяет править программу смелее, не боясь её испортить: если мы что-то испортим, об этом нам скажут переставшие работать тесты, если же что-то испортится, но ни один тест при этом не перестанет работать — значит, тестовое покрытие недостаточно и нужно написать больше тестов.

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

2.16.3. Отладочная печать

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

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

Первое, что нужно запомнить относительно отладочной печати: отладочные сообщения должны быть легко узнаваемы и не должны сливаться с остальной информацией, которую выдаёт ваша программа. Можно начать каждое отладочное сообщение, например, с пяти звёздочек, или со слова “DEBUG” (обязательно заглавными буквами, если только ваша программа штатно не выдаёт сообщений заглавными — в таком случае маркер отладочной печати стоит набрать, напротив, строчными), или с лаконичного “XXX”; главное — чтобы отладочную печать было хорошо видно. Второе простое правило для отладочной печати — не забывайте про переводы строк; для Паскаля это означает, что нужно применять writeln. Дело тут в так называемой буферизации вывода; сообщение, которое не закончилось переводом строки, вы можете увидеть не сразу, а если программа завершится аварийно — то и вообще не увидеть, что совершенно не годится для отладочной печати.

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

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

Впрочем, не торопитесь убирать из текста отладочную печать. Как показывает практика и гласит вселенский закон подлости, как только из программы будет убран последний оператор отладочной печати, в ней тут же обнаружится очередная ошибка, для отлова которой большую часть только что убранных операторов придётся вставить обратно. Будет лучше взять отладочные операторы в фигурные скобки, превратив их в комментарии (но не нарушая при этом структурных отступов!) Однако даже вставка и удаление знаков комментария может оказаться неоправданно трудоёмким делом. Правильнее будет воспользоваться директивами условной компиляции, которые пришли в Паскаль из языка Си и на первый взгляд выглядят довольно странно. Для условной компиляции используются “символы”, которые нужно “определять”; ни для чего другого они не годятся, в отличие от Си, где основная роль аналогичных “символов” совершенно иная. Так или иначе, придумайте себе какой-то идентификатор, который вы будете использовать в качестве “символа” для включения и отключения отладочной печати и больше ни для чего; можно порекомендовать на эту роль слово “DEBUG”. Поместите в начале программы директиву “{$DEFINE DEBUG}”, которая “определит” этот символ. Теперь каждый фрагмент программы, предназначенный для отладочной печати, поместите между директивами условной компиляции, примерно так:

Пока в начале программы есть директива, определяющая символ DEBUG, такой оператор writeln будет учитываться компилятором, как обычно, но если убрать директиву DEFINE, “символ” DEBUG станет неопределён, так что всё, что заключено между {$IFDEF DEBUG} и {$ENDIF}, компилятор просто проигнорирует. Заметим, директиву DEFINE даже не обязательно убирать полностью, достаточно убрать из неё символ “$”, и она превратится в обычный комментарий, а вся отладочная печать отключится; если отладочная печать потребуется снова, достаточно будет вставить символ на место. Но можно сделать ещё лучше: директиву DEFINE в программу вообще не вставлять, а символ при необходимости определять из командной строки компилятора. Для этого достаточно добавить в командной строке флажок “-dDEBUG”, примерно так:

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

В некоторых случаях при отладке требуется узнать, каково текущее наполнение сложной структуры данных — списка, дерева, хеш-таблицы или чего-то ещё более сложного. Бояться таких ситуаций не следует, ничего сложного они собой не представляют; просто стоит, по-видимому, описать какую-нибудь процедуру, специально предназначенную для распечатки текущего состояния нужной нам структуры данных. Такую процедуру можно целиком заключить в условно-компилируемый фрагмент, чтобы в версию исполняемого файла без отладочной печати процедура не входила и не увеличивала объём машинного кода.

2.16.4. Отладчик gdb

Отладочная печать — средство, бесспорно, мощное, но в ряде случаев мы можем разобраться в происходящем гораздо быстрее, если нам позволят выполнить нашу программу по шагам с просмотром текущих значений переменных. Для этого используется специальная программа, которая называется отладчиком; в современных версиях ОС Unix, в том числе Linux, наиболее популярен отладчик gdb49, его мы сейчас и попробуем задействовать.

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

Отладочная информация занимает сравнительно много места в исполняемом файле, никак не отражаясь на исполнении программы — она нужна только при использовании отладчика. Поэтому компилятор не снабжает исполняемый файл отладочной информацией, если его об этом не попросить, указав в командной строке ключ -g:

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

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

Запустить отладчик для работы в этом режиме можно, указав параметром имя исполняемого файла:

Если нужно передать нашей программе те или иные аргументы командной строки, это делается с помощью ключа --args, например:

(в этом примере программа myprog будет запущена с тремя аргументами командной строки — abra, schwabra и kadabra).

После запуска отладчик сообщит свою версию и некоторую другую информацию и выдаст приглашение своей командной строки, обычно выглядящее так:

(gdb)

Начать выполнение программы мы можем командой start, в этом случае отладчик запустит нашу программу, но остановит её на первом же операторе главной части, не позволив ей ничего сделать; дальнейшее выполнение будет происходить под нашим контролем. Можно поступить иначе: дать команду run, тогда программа запустится и будет работать как обычно (то есть так, как она работает без отладчика), и если всё будет в порядке, то программа благополучно завершится, а отладчик так ничего и не сделает; но если выполнение программы прервать нажатием Ctrl-C, то при выполнении под отладчиком программа не будет уничтожена; вместо этого отладчик остановит её и выдаст своё приглашение командной строки, спрашивая нас, что дальше делать. Кроме того, если в ходе выполнения программы под отладчиком произойдёт её аварийное завершение, отладчик покажет нам то место в исходном тексте, где это произошло, и позволит просмотреть текущие значения переменных, что в большинстве случаев позволяет понять, почему произошла авария.

Когда отлаживаемая программа остановлена, мы можем приказать отладчику выполнить в ней один шаг; вопрос в том, что будет считаться “шагом”. Ответов на этот вопрос предусмотрено два. Команда next рассматривает в качестве “шага” одну строку текста исходной программы, причём если в этой строке встречаются вызовы процедур и функций, их выполнение рассматривается как часть “шага”, то есть внутрь вызываемых подпрограмм команда next не заходит. Вторая команда для “одного шага” называется step и отличается тем, что заходит внутрь вызываемых процедур и функций, то есть если в текущей строке присутствовал вызов подпрограммы и мы дали команду step, то после этого текущей станет первая строка текста этой подпрограммы. Команды step и next можно повторять, просто нажимая Enter, что изрядно ускоряет пошаговое выполнение.

Остановив выполнение программы (в том числе после команд step или next), отладчик обычно показывает строку исходного текста, которой соответствует текущая точка выполнения, что в большинстве случаев позволяет сориентироваться и понять, где мы. Если выданной строки недостаточно, можно воспользоваться командой list, которая выдаст на экран окрестности текущей строки — пять строчек перед ней, её саму и пять строчек после. Если и этого не хватает, можно указать команде list номер строки, с которого начать; например, “list 120” выдаст десять строк, начиная со 120-й. При желании можно увидеть следующие десять строк, нажав Enter вместо ввода следующей команды, и так до конца файла.

Если “шагов”, выполняемых программой, оказывается слишком много для пошагового выполнения, мы можем запустить программу на обыкновенное исполнение, в ходе которого она не будет останавливаться после каждого шага; при начальном запуске, как уже было сказано, для этого используется команда run, если же программа была приостановлена, продолжить её выполнение можно командой cont (от слова continue). Обычно перед тем, как воспользоваться одной из этих команд, в программе расставляют так называемые точки останова; дойдя до такой точки, программа остановится, а отладчик выдаст нам приглашение, испрашивая дальнейших указаний. Точки останова устанавливаются с помощью команды break, которой необходим параметр; это может быть либо номер строки исходного текста (а при отладке программы, состоящей из нескольких исходных текстов — имя файла и номер строки, разделённые двоеточием), либо имя подпрограммы (процедуры или функции). В первом случае программа будет остановлена, дойдя до указанной строки (но до того, как эта строка будет выполнена), во втором случае программа остановится, как только будет вызвана указанная подпрограмма.

Отметим, что, поскольку Паскаль не различает в идентификаторах заглавные и строчные буквы, на уровне объектного кода (и в том числе отладочной информации) компилятор приводит все имена к верхнему регистру. Это означает, что при использовании отладчика имена процедур и функций придётся писать заглавными буквами, иначе отладчик нас не поймёт. Формально говоря, то же самое касается и имён переменных, но, например, версии компилятора и отладчика, которые использовал автор этих строк при подготовке книги, прекрасно понимали имена локальных переменных, написанные в любом регистре.

При создании новой точки останова отладчик показывает её номер, которым можно воспользоваться для более гибкой работы с остановками. Например, команда “disable 3” временно выключит точку останова № 3, а команда “enable 3” включит её обратно. Команда “ignore 3 550” укажет отладчику, что точку останова № 3 следует “проследовать без остановки” (проигнорировать) 550 раз, а остановиться лишь после этого — то есть если когда-нибудь до неё дойдёт дело в 551-й раз. Наконец, команда cond (от слова conditional) позволяет задать условие останова в виде логического выражения. Например,

указывает, что останавливаться на точке № 5 следует лишь в том случае, если значение переменной i окажется меньше ста. Команда “info breakpoints” позволяет узнать, какие у вас имеются точки останова, каковы установленные для них условия, счётчики игнорирования и т. п.

Просмотреть значения переменных, когда программа остановлена, можно с помощью команды inspect. При необходимости команда “set var” позволяет изменить значение переменной, хотя это используется сравнительно редко; например, “set var х=50” принудительно занесёт в переменную х значение 50.

В программе, активно использующей подпрограммы, очень полезна может оказаться команда bt (или, если полностью, backtrace). Эта команда показывает, какие подпрограммы были вызваны (но ещё не завершились), с какими параметрами они были вызваны и из каких мест программы. Например, в ходе отладки программы hanoi2 (см. § 2.14.2) команда bt могла бы выдать:

Это означает, что сейчас активна процедура MOVELARGER (в тексте программы она называется MoveLarger), текущая строка — 52-я в файле hanoi2.pas; процедура MoveLarger была вызвана из процедуры SOLVE (Solve), вызов расположен в строке 91. Наконец, Solve вызвана из главной части программы (обозначается словом main; дело тут в том, что gdb ориентирован в основном на язык Си, а в нём вместо главной программы используется функция с именем main), вызов находится в строке 110.

Первое число в каждой строке выдачи команды bt — это номер фрейма. Используя этот номер, мы можем переключаться между контекстами перечисленных подпрограмм; например, чтобы просмотреть значения переменных в точках, где были вызваны нижестоящие подпрограммы. Например, в нашем примере команда “frame 1” позволит нам заглянуть в то место процедуры Solve, где она вызывает MoveLarger. После команды frame можно воспользоваться командами list и inspect, они будут выдавать информацию, относящуюся к текущей позиции выбранного фрейма.

Ещё одна полезная команда — call; она позволяет в любой момент вызвать любую из ваших подпрограмм с заданными параметрами. К сожалению, здесь имеются определённые ограничения; gdb ничего не знает, например, о паскалевских строках, так что, если ваша подпрограмма требует строки в качестве одного из параметров, вы можете вызвать её, указав в качестве параметра какую-нибудь подходящую переменную, но конкретное строковое значение вы задать не можете.

Выход из отладчика производится командой quit, или вы можете устроить ситуацию “конец файла”, нажав Ctrl-D. Кроме того, полезно знать, что в отладчике есть команда help, хотя работать с ней не так просто.

Как было сказано в начале параграфа, gdb может использоваться в разных режимах. Так, если вы уже запустили вашу программу, она ведёт себя неправильно, но вам не хочется повторять действия, которые вызвали такое поведение, либо вы не уверены, что вообще сможете воссоздать имеющуюся ситуацию, можно подключить отладчик к существующему процессу. Для этого нужно, естественно, узнать номер процесса; как это делается, мы рассказывали в § 1.4.7. Далее gdb запускается с двумя параметрами: именем исполняемого файла и номером процесса, например:

Имя исполняемого файла необходимо отладчику, поскольку только из него можно взять отладочную информацию, то есть сведения об именах переменных и номерах строк. После успешного подключения отладчик приостанавливает процесс и ждёт ваших указаний; вы можете с помощью команды bt узнать, где находитесь и как туда попали, воспользоваться командой inspect для просмотра текущих значений переменных, использовать команды break, cont, step, next и т. д. После выхода из отладчика процесс продолжит выполнение, если, конечно, в ходе отладки вы его не убили.






Для любых предложений по сайту: [email protected]