Г Л А В А VIII. НЕКОТОРЫЕ ВОПРОСЫ МЕТОДОЛОГИИ ОТЛАДКИ ПРОГРАММ Я услышал и забыл. Я увидел и запомнил. Я сделал и понял! Гленн М. Клейман. О т л а д к а = О б н а р у ж е н и е ошибки + ее И с п р а в л е н и е └─────────────▲─────────────┘ │ Т е с т и р о в а н и е + Л о к а л и з а ц и я ┌─────────────────────────────────────────────────────┐ │ ┌ ┐ ┌ ┐ │ │ │ Процесс │ │ Процесс │ │ │ Процесс отладки = ┤обнаружения├ + ┤исправления├ │ │ │ ошибки │ │ ошибки │ │ │ └ ▲ ┘ └ ┘ │ │ │ │ │ Тестирование + Локализация │ │ программы ошибки │ └─────────────────────────────────────────────────────┘ VIII.1 ОШИБКИ ПРИ ПРОГРАММИРОВАНИИ Какую бы программу вы ни писали, Какую бы программу вы ни писали, любая ошибка, которая может в любая ошибка, которая не может в нее вкрасться, - вкрадется! нее вкрасться, - тоже вкрадется! Следствие Еще одно следствие первого закона Чизхолма первого закона Чизхолма Будем говорить, что "в программе имеется о ш и б к а, если ее выполне- ние не оправдывает ожиданий пользователя" [50, 51]. Напомним, что при решении задач с использованием компьютера под о т- л а д к о й программ понимается обычно один из этапов решения, во время которого с помощью компьютера происходит обнаружение и исправление оши- бок, имеющихся в программе ; в ходе отладки программист хочет добиться определенной степени уверенности в том, что его программа соответствует своему назначению и не делает того, для чего она не предназначена. Все мы делаем ошибки при программировании. Даже программисты с двадца- тилетним опытом работы допускают их десятками. Разница между хорошим и плохим программистом заключается не в том, что первый не делает ошибок, а скорее в том, что он делает значительно меньше п р о с т ы х ошибок. Начинающий программист, как правило, переоценивает свои возможности и, разрабатывая программу, исходит из того, что в его программе ошибок не бу- дет. А говоря про только что составленную программу, готов уверять, что она на 99% правильна, и ее остается только для большей уверенности один(!) раз выполнить на компьютере с какими-нибудь(!) исходными данными. Естест- венно, что каждый неверный результат, каждая найденная ошибка вызывают у него изумление и считаются, к о н е ч н о, последними. Вследствие такого подхода получение с помощью компьютера надежных результатов по составлен- ной программе откладывается на длительный и неопределенный срок. Только приобретя достаточный опыт, программист понимает справедливость древнего высказывания: "Человеку с в о й с т в е н н о о ш и б а т ь с я!". Оказывается, что практически невозможно для достаточно сложной програм- мы быстро найти и устранить все имеющиеся в ней ошибки. Трудности програм- мирования и отладки подчеркивает следующий популярный в среде программис- тов афоризм: "В к а ж д о й программе есть по крайней мере одна ошибка". Поэтому можно сказать, что наличие ошибок в только что разработанной про- грамме - вполне нормальное и закономерное явление. А совсем ненормальным, из ряда вон выходящим фактом является отсутствие ошибок в программе, кото- рая не была еще подвергнута тщательному тестированию и отладке (конечно, речь здесь идет о достаточно сложных программах!). Поэтому разумно уже при разработке программы на этапах алгоритмизации и программирования г о т о в и т ь с я к о б н а р у ж е н и ю о ш и - б о к на стадии отладки и принимать профилактические меры для их преду- преждения. Ошибки можно объединить в следующие группы [50, 51]: 1) ошибки обращения к данным - использование переменных с неустановлен- ными значениями, ошибки индексации, несоответствие структур данных; 2) ошибки описания данных - отсутствие явного описания или неполное описание данных, отсутствие или неправильное присвоение начальных значе- ний, несогласованность инициализации переменной с ее описанием; 3) ошибки вычислений - наличие в последовательных вычислениях данных недопустимых типов, несогласованность масштабов,приводящая к переполнению или потере точности, возможность деления на нуль; 4) ошибки при сравнениях - использование при операциях сравнения вели- чин несовместимых типов, искажение смысла операций отношения (>, =, <) и логических операций (NOT, AND, OR), сравнение чисел с фиксированной и пла- вающей запятой; 5) ошибки в передачах управления - ошибки организации циклов, приводя- щие к возможности зацикливания или неправильного выполнения цикла, нали- чие неполного числа выходов в операторах-переключателях; 6) ошибки программного и н т е р ф е й с а - несоответствие количест- ва, типов или размерности фактических и формальных параметров подпро- грамм при их вызове, несоответствие описаний переменных требованиям на вы- ходе модуля, несогласованность описаний глобальных переменных и их интер- претации операторами программы (м о д у л ь - это замкнутая программа, ко- торую можно вызвать из любого другого модуля в программе и можно отдельно компилировать). Напомним, что программный и н т е р ф е й с определяет совокупность допустимых процедур или операций и их параметров, список об- щих переменных, областей памяти или других объектов; 7) ошибки ввода-вывода - неполное или неправильное описание атрибутов файлов или оператора обращения к файлу, несогласованность размера записи и выделенной памяти, неполный контроль и регистрация операций с файлами; 8) помехозащита - отсутствие контроля входных данных, отсутствие сохра- нения исходных данных и возможности повторного запуска модуля при сбоях. 9) никогда не считайте, что Вы точно знаете причину ошибочного выполне- ния программы; очень часто в этом виновна ошибка, встретившаяся гораздо раньше (иногда ее называют о т л о ж е н н о й ошибкой). 10) и, наконец, ошибки могут быть также следствием неверной работы обо- рудования - это так называемые а п п а р а т н ы е ошибки.Если в регистр памяти компьютера на одном из этапов работы программы занесено число 12, а при чтении из этого же регистра оно прочиталось как 11, то и дальнейшие результаты, разумеется, будут неверными. Возможен случай, когда из-за та- кой ошибки результат вовсе не будет получен(процесс решения задачи аварий- но прекратится). Разработаны надежные методы борьбы с аппаратными ошибками и их послед- ствиями - п о в т о р н ы е вычисления с последующим сравнением резуль- татов, хранение нескольких экземпляров данных с целью их защиты от искаже- ния и т.д. Поэтому среди встречающихся на практике случаев выдачи компью- терами неверных результатов или невыдачи их вообще доля ошибок, порожден- ных аппаратными средствами, составляет ничтожный процент. Так, согласно одному из определений "ПЭВМ - это вычислительная машина с надежностью военной аппаратуры и ценой изделия бытовой электроники"[58]. ┌───────────────────────────────────────────────────────────────────────┐ │Таким образом,в ошибочных ответах компьютера виноваты,как правило,люди!│ └───────────────────────────────────────────────────────────────────────┘ Приведенная классификация полезна тем, что для каждой из перечисленных групп ошибок и для каждого типа ошибки в группе можно выделить операторы каждого конкретного языка программирования, потенциально допускающие дан- ный тип ошибок. Некоторые ошибки являются с и с т е м а т и ч е с к и м и; они возни- кают всякий раз при выполнении программы. Если в команде на вычисление по заданной формуле вместо плюса поставить минус, то и ответ, скорее всего, будет ошибочным. Такие ошибки в программах обычно живут недолго: их быст- ро обнаруживают и исправляют. Приведем интересный пример систематической ошибки в программном интерфейсе. 19 июня 1985 года команда американского космоплана многоразового ис- пользования ("Шаттла") должна была развернуть свой корабль так, чтобы зер- кало на его борту могло отражать луч лазера,находившегося на горе высотой 10023 фута. Навигационная система пыталась развернуть "Шаттл" так, чтобы принимать луч с вершины несуществующей горы высотой в 10023 морских миль над уровнем моря. Это произошло из-за того, что один из пары взаимосвязан- ных компонентов программно-аппаратного комплекса передавал высоту в футах, а другой - интерпретировал эту величину в милях. Другие ошибки носят с л у ч а й н ы й характер; при каждом выполне- нии программы они будут приводить к разным результатам, либо программа мо- жет выполняться в большинстве случаев правильно, но время от времени нео- жиданно давать неверный результат. Такие случайные ошибки следует старать- ся выявить на этапе ручной проверки, потому что при машинном выполнении программы они могут "исчезнуть" лишь для того,чтобы снова появиться позже. В некотором смысле достаточно сложная программа напоминает карточный домик: тот факт что домик стоит, еще не гарантирует, что он не рассыплет- ся в следующее мгновение. Программы опровергают опыт нашей жизни. Обычно, если что-то работает, то оно работает! Если новый стул выдержал Вас, он выдержит Вас и в следующий раз; если сошедший с конвейера автомобиль про- ехал один километр, он сможет проехать еще сотни километров; если здание простояло 5 минут, то, как уверяют строители и архитекторы, оно простоит еще сто лет. ┌───────────────────────────────────────────────────────────────────────┐ │ Однако на основании того факта, что часть программы работает, ничего │ │ нельзя сказать о работоспособности остальной части программы! │ └───────────────────────────────────────────────────────────────────────┘ Показательны в этом отношении результаты первых попыток запуска "Шат- тла".Программное обеспечение этого космического корабля состояло примерно из полумиллиона программных строк,над которыми трудился большой коллектив разработчиков. Корабль не смог оторваться от Земли из-за нарушения синхро- низации всех его компьютеров. Оказалось, что программная ошибка,явившаяся причиной неудачи, была внесена нечаянно при исправлении другой ошибки, об- наруженной двумя годами раньше, и могла проявляться в среднем только в од- ном из 67 полетов. Одна из наиболее типичных случайных ошибок возникает, если Вы забыли инициализировать переменную, т.е. присвоить ей начальное значение. В этом случае начальное значение переменной зависит от того, какая программа (на- зовем ее Р) выполнялась на компьютере перед тем,как была загружена отлажи- ваемая программа. Если Ваша переменная имеет адрес α,то программа Р может оставить после своего выполнения в ячейке по адресу α "все, что угодно" - код команды, значение переменной, значение адреса переменной (м у с о р, т.е. произвольное, непредсказуемое значение). Еще одной часто встречающейся случайной ошибкой является запись дан- ных в массив, когда значение индекса вышло за допустимые пределы. Если,на- пример, Вы присваиваете начальное значение элементу массива T(J), а значе- ние индекса J должно находиться в границах от 1 до 100, но J случайно ока- залось в какой-то момент больше 100, либо меньше 1,то Вы, разумеется,полу- чите не тот результат, который хотели. Если Вы часто совершаете ошибку та- кого рода, Вам полезно написать несколько операторов, которые будут прове- рять значения каждого индекса перед его использованием и фиксировать его выход из установленного диапазона. Конечно,это увеличивает время выполне- ния программы, но заметно ускоряет процесс отладки. В заключение - п о л е з н ы й совет: ┌───────────────────────────────────────────────────┐ │ старайтесь вести список допущенных Вами ошибок │. └───────────────────────────────────────────────────┘ Каждый раз, когда Вы обнаруживаете ошибку, заносите ее в этот список и обязательно проверяйте, чтобы она не повторилась в дальнейшем! ┌───────────────────────────────────────────────────────────────┐ │ "Чтобы избегать о ш и б о к, надо набираться о п ы т а, │ │ чтобы набираться о п ы т а , надо делать о ш и б к и". │ │ Принцип Компетентности по Питеру │ └───────────────────────────────────────────────────────────────┘ VIII.2. НЕКОТОРЫЕ КЛАССИЧЕСКИЕ ПРИЕМЫ ТЕСТИРОВАНИЯ ПРОГРАММ Самая коварная уловка дьявола состоит в том, чтобы убедить нас, будто его не существует. Шарль Бодлер Известно, что в процессе разработки программы работы по доказательству (д е м о н с т р а ц и и) правильности разрабатываемой программы равно- значны работам по ее изготовлению (алгоритмизации и написанию), что можно выразить следующей формулой: ┌──────────────────────────────────────────────────────┐ │ Разработка программы = Изготовление + Доказательство │ . └──────────────────────────────────────────────────────┘ Поэтому п р о г р а м м о й следовало бы называть только такую про- грамму, которая выдает правильные результаты, а то, что еще не прошло ста- дию доказательства правильности, является не программой, а ее п о л у- ф а б р и к а т о м. Изготовление такого полуфабриката, конечно, являет- ся делом несравнимо более легким, чем разработка н а с т о я щ е й про- граммы (особенно если программист и не думает о предстоящей отладке!). VIII.2.1. Р у ч н а я п р о в е р к а Если вам кажется, что ситуация улучшается, значит, вы чего-то не заметили! Второе следствие второго закона Чизхолма Отладку любой программы никогда не следует начинать с прогона програм- мы на компьютере, т.к. экспериментально установлено, что "ручными" метода- ми (т.е. без помощи компьютера) удается обнаруживать от 30 до 70% програм- мных и аналитических ошибок из общего числа ошибок,допущенных при програм- мировании! Вначале обязательно проведите р у ч н у ю п р о в е р к у ("desk che- cking"),которая есть не что иное, как тщательная проверка Вашей програм- мы за письменным столом. При ручной проверке программы (или алгоритма) программист по тексту программы мысленно старается восстановить тот вычис- лительный процесс, который определяет программа, после чего сверяет его с требуемым процессом. Самое главное, о чем всегда следует помнить, это то, что ошибки в про- веряемой программе о б я з а т е л ь н о есть и, чем больше их будет об- наружено за с т о л о м, тем легче и быстрее пройдет предстоящий этап отладки программы на компьютере. На время проверки нужно постараться "за- быть" о том, что должен делать проверяемый участок программы, и "узнавать" об этом по ходу его проверки.Только после окончания проверки участка и вы- явления тем самым его действительных функций можно "вспомнить" о том, что он должен делать, и сравнить реальные действия программы с требуемыми. Полезно, найдя какую-либо специфическую ошибку,отойти от последователь- ной проверки и сразу узнать,нет ли таких же ошибок в аналогичных,в особен- ности уже проверенных, местах. Приведем примерный список вопросов, на которые отвечает программист при р у ч н о й п р о в е р к е программы. 1. Есть ли обращения к переменным, которым не присвоены значения? "Присваивайте начальные значения переменным перед тем, как они будут использованы. Убедитесь в том, что переменным в программах и во внутрен- них циклах присваиваются соответствующие значения при каждом входе в них" [56]. 2. Не выходит ли значение индекса элемента массива за границы, опреде- ленные для соответствующего измерения, при всех обращениях к массиву? "Проверьте, чтобы индексы при обращении к элементам массива не выходи- ли за границы"[56]. 3. Принимает ли каждый индекс целые значения при всех обращениях к мас- сиву? Нецелые индексы не являются ошибкой для многих языков программирова- ния, но представляют практическую опасность. 4. Все ли переменные описаны явно? Отсутствие явного описания не обяза- тельно является ошибкой, но обычно служит источником беспокойства. 5. Есть ли переменные со сходными именами (например, VOLT и VOLTS)? На- личие сходных имен не обязательно является ошибкой, но служит признаком того, что имена могут быть перепутаны где-нибудь внутри программы. 6. Возможны ли переполнение или исчезновение порядка во время вычисле- ния значения выражения? Это означает, что конечный результат может казать- ся правильным, но промежуточный результат может быть слишком большим (пе- реполнение) или слишком малым (исчезновение порядка) для машинного пред- ставления данных. 7. Учтено ли, что делитель при делении может обратиться в нуль? 8. Может ли значение переменной выходить за пределы установленного для ее типа диапазона? 9. Сравниваются ли величины различных типов? 10. Корректны ли операции сравнения? Обычно часто путают такие операции отношения, как "не меньше, чем", "не больше, чем". 11. Каждоe ли логическое выражение сформулировано так, как это предпола- галось? Программисты часто делают ошибки при написании логических выраже- ний, содержащих операции NOT, AND, OR. 12. Если в программе содержится оператор-переключатель ON k GOTO m1,m2,...,mn , то может ли значение переменной k превысить n? Например, всегда ли k бу- дет принимать значения 1, 2 или 3 в операторе ON k GOTO 10,20,30 ? 13. Будет ли каждый цикл в конце концов завершен? Придумайте неформаль- ное доказательство или аргументы, подтверждающие их завершение. "Не используйте числа с плавающей запятой в качестве значений счет- чиков. Не надейтесь, что для дробных величин с плавающей запятой справед- ливы известные правила арифметики - это не так"[56]. 14. Будет ли программа или подпрограмма в конечном счете завершена? 15. Возможно ли, что некоторый цикл никогда не сможет выполниться? Если это так, то является ли это оплошностью? "Проверьте, могут ли циклы в программе при определенных обстоятельствах выполняться нулевое число раз"[56]. 16. Совпадают ли количество и тип формальных и фактических параметров используемых подпрограмм? 17. Совпадают ли единицы измерения значений соответствующих фактических и формальных параметров? Например, нет ли случаев, когда значение фактиче- ского параметра выражено в градусах,а в подпрограмме все расчеты проводят- ся с формальным параметром, выраженным в радианах. 18. Не изменяет ли подпрограмма значения переменной, которая использует- ся только как входная величина? 19. Все ли файлы открыты перед их использованием? 20. Существуют ли смысловые или грамматические ошибки в тексте, выводи- мом программой на печать или на экран дисплея? VIII.2.2. Р у ч н а я п р о к р у т к а. М е т о д и ч е с к и е у к а з а н и я п о е е п р о в е д е н и ю Программисту не всегда нужна ЭВМ, иногда по- лезнее удобное кресло и спокойная обстановка. А.Архангельский После окончания ручной проверки проведите несколько раз р у ч н у ю п р о к р у т к у ("walkthrough" - "сквозной контроль") отдельных частей Вашей программы. Иногда ее называют "с у х о й" п р о к р у т к о й ("dry running"-"пробный прогон") в отличие от метода прокрутки, использующего компьютер. Основой п р о к р у т к и является имитация программистом про- цесса выполнения программы (алгоритма) компьютером с целью более конкрет- ного и наглядного представления о процессе, определяемом т е к с т о м проверяемой программы. Прокрутка дает возможность приблизить последовате- льность проверки программы к последовательности ее выполнения, что позво- ляет проверять программу как бы в динамике ее работы, проверять элементы вычислительного процесса, задаваемого проверяемой программой, а не только статичный текст программы. Для выполнения прокрутки обычно приходится задавать какие-то конкрет- ные исходные данные и производить над ними необходимые вычисления, исполь- зуя текст программы. Для программ со сложной логикой, в которых, например, характер работы одного участка программы зависит от результатов работы других ее участков,необходимо осуществлять ручную прокрутку программы для ряда специально подобранных исходных данных и параметров. Прокрутка дает программисту возможность найти более хитрые ошибки в программе, чем при ручной проверке. Трудность применения прокрутки - большой объем ручной работы при попыт- ке точного моделирования работы программы. Поэтому успех применения про- крутки заключается в выборе такой необходимой степени детализации модели- рования, чтобы, с одной стороны, выявить максимальное количество ошибок,а с другой - затратить на это минимальные усилия. Приведем несколько соображений, которые могут помочь уменьшить время, затрачиваемое на прокрутку. Прокрутку следует применять лишь для контроля логически сложных про- грамм или б л о к о в (под б л о к о м будем понимать некоторую группу операторов, объединяемых по какому-либо признаку, например: арифметиче- ский блок - выполняемая последовательно группа операторов, производящих вычисления в программе, логический блок - группа операторов, управляющих последовательностью вычислений в программе). А р и ф м е т и ч е с к и е блоки нужно проверять обычным способом,не задаваясь конкретными исходными данными. Вычислять числовые значения нуж- но лишь для тех величин, от которых зависит последовательность выполнения блоков (операторов) программы, и эта последовательность является очень су- щественной. Поэтому во время прокрутки программы при всякой возможности, когда позволяет характер прокручиваемого блока программы,нужно переходить на обычную ручную проверку и возвращаться на режим прокрутки при начале проверки логически сложных блоков. Исходные данные, влияющие на логику программы, должны выбираться так, чтобы минимизировать прокрутку программы.Но,с другой стороны, данные долж- ны быть такими, чтобы в прокрутку вовлеклось большинство ветвей программы и чтобы прокрутка отразила типичный характер ее работы. Кроме того, в ходе прокрутки необходимо проверить работу программы и для особых случаев (например, для экстремальных значений параметров). Многократные повторные прокрутки какого-либо участка программы можно не производить, если в логике его выполнения ничего не изменяется по срав- нению с предыдущими проходами. Например, тело цикла можно прокрутить лишь для п е р в ы х двух-трех проходов (проверка входа в цикл) и для п о- с л е д н и х одного-двух (проверка выхода из цикла). Прокрутка бывает необходимой и в том случае, когда программист не в со- стоянии вполне четко представить себе логику проверяемой программы, осо- бенно ┌────────────────────────────────────────────────────────────┐ │ если программа написана не им и нет хорошего описания │ . └────────────────────────────────────────────────────────────┘ При первом же пробном запуске вычислительной машины МЭСМ (первая в СССР ЭВМ, 1951 г.) произошел показательный случай. Первую программу для МЭСМ перед запуском прокрутили вручную два квалифицированных математика и получили одинаковые результаты. А машина выдала другой результат. Инжене- ры долго искали неисправность и не смогли ее найти. Тогда академик С.А.Ле- бедев, главный конструктор МЭСМ, сам взялся за ручную прокрутку. Прорабо- тав всю ночь, он обнаружил, что оба математика ошиблись в одном и том же месте, а машина оказалась права! Поговорим теперь о м е т о д и к е проведения ручной прокрутки. Нарисуйте на листе бумаги "начальную обстановку",а затем - "исполняйте" операторы по одному, отмечая все изменения, происходящие при этом в запо- минающем устройстве. Поэтому для ручной прокрутки нужно уметь изображать на бумаге начальную обстановку в Оперативном Запоминающем Устройстве и происходящие в нем изменения. Разумеется, нас интересуют не все б л о к и (участки) памяти, а только те из них, которые используются в программе. Удобнее всего рисовать блоки и их значения мелом на школьной доске: ведь для того, чтобы поместить в блок новое содержимое,нужно "уничтожить" (сте- реть) старое - на доске это сделать очень просто. Каждый блок можно рисо- вать в виде "домика", на "крыше" которого записано имя блока,а внутри раз- мещается содержимое. Форма крыши может говорить о типе переменной. ┌─────────┐ │ ГОД │ Например, на рисунке изображен блок, имя которого │─────────│ ГОД, содержимое блока - 1987, тип - строковый, т.к. кры- │ 1987 │ ша прямоугольная. └─────────┘ Если доски нет, ручную прокрутку можно вести и на бумаге. При этом таб- лицу имен (блоков) приходится изображать немножко по-другому потому, что стирать старые значения на бумаге неудобно, лучше их зачеркивать, а рядом писать новые значения. Ручную прокрутку лучше всего проводить в д в о е м с приятелем. Вы- полняйте строку за строкой программы (алгоритма) и старайтесь независимо друг от друга обнаружить ошибки. Однако избегайте такой методики в том случае, если выяснится, что Вы испытываете искушение переложить на своего приятеля (или он на Вас) заботу о тестировании программы! В заключение необходимо отметить,что использование прокрутки весьма по- лезно еще и потому, что она содействует глубокому осознанию программистом логики составленной им программы и того реального вычислительного процес- са, который ею задается. Ведь быстрота ориентирования в отлаживаемой про- грамме и в выдаваемых отладочных результатах всецело зависит от способно- сти программиста мысленно представить себе во всех деталях алгоритм рас- сматриваемой программы,что невозможно без глубокого и крепкого знания его в течение всего длительного времени проведения отладки.