учебники, программирование, основы, введение в,

 

Технология программирования и отладки

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

  • правильно оценить эффективность выбранного алгоритма (например, если в процессе работы будет производиться постоянная перестройка структуры, хранящей данные, то лучше остановить выбор на списочном алгоритме);
  • убедиться в том, что он корректно обрабатывает весь спектр входных данных (например, алгоритм Дейкстры нельзя применять, если в графе есть ребра с отрицательным весом);
  • оценить вероятные трудозатраты на его создание (например, гораздо легче написать простую, а не улучшенную сортировку, и если объем входных данных позволяет, именно так и следует поступить) и т.п.

Если же на этапе выбора алгоритма все-таки была допущена ошибка, то программу придется писать заново. При этом некоторые блоки старого варианта (например, ввод/вывод) могут не зависеть от выбора алгоритма, тогда переписывать их не нужно. Но основной алгоритм все-таки необходимо исправить.
При коренной переделке программы особенно важно аккуратно отслеживать все вносимые в текст программы изменения.
Далее мы перечислим общеизвестные правила, которые помогут вам минимизировать затраты времени и сил при написании программы.
Несмотря на то что некоторые из этих правил могут показаться вам ненужными и занудными, следование им в процессе создания сложной программы очень облегчает и, главное, ускоряет ее отладку. В общем, если перефразировать известное выражение, "овчинка стоит выделки".
Имена, имена, имена...
Советы, которые касаются различных идентификаторов, используемых в программе, можно кратко изложить так.
Совет 1. Создание любой программы (по выбранному алгоритму решения) начинайте с описания переменных. Разумеется, невозможно сразу же предусмотреть все переменные, которые могут потребоваться в ходе написания программы, однако самые важные - те, ради которых вся работа и затевается, - должны быть описаны с самого начала.
Описывая переменные, размещайте их по одной на каждой строке (впрочем, однотипные переменные можно размещать и группами) и в обязательном порядке снабжайте комментариями, разъясняющими смысл этих переменных. В противном случае вы обречены будете в процессе работы раз за разом сверяться с форматом ввода или, еще хуже, рыскать по уже написанному тексту, вспоминая, что же именно должна была хранить в себе каждая переменная. Потери времени и нервов при этом могут оказаться значительными.
Совет 2. Старайтесь давать основным переменным говорящие имена. Ясно, что безликие rez1 и rez2 несут в себе гораздо меньше информации, чем, скажем, dlina и shirina. Однако не стоит заводить очень длинные - более 6-7 символов - имена, поскольку это, во-первых, затормозит написание программы, а во-вторых, повысит вероятность возникновения "очепяток". Стоит использовать сокращения, короткие синонимы, русские слова (русские по смыслу и прочтению, однако латинские по написанию).
Следите за тем, чтобы имена переменных заметно отличались друг от друга. Имена, которые различаются только одним символом, следует считать крайне неудачными. Однако во всем нужно знать меру и, разумеется, шесть вспомогательных переменных, отражающие шесть последовательных состояний одного и того же процесса, стоит назвать а1, а2, ... а6. Тем самым вы подчеркнете их единую сущность.
При выборе имен переменных стоит также пользоваться интуитивно знакомыми вам условностями. Например, i и j обычно служат вспомогательными счетчиками, m и n чаще всего хранят размерность, а x и y - координаты. Лучше не отступать от привычных вам сокращений и обозначений, даже если они и не являются общепринятыми.
Совет 3. Имена функциям и процедурам также нужно давать говорящие, причем здесь ограничения на длину гораздо мягче: до 10-15 символов. Вряд ли ваша программа будет обращаться к процедурам и функциям столь же часто, как к переменным, поэтому здесь на первый план выходит наполнение имен подпрограмм объяснением выполняемых ею операций. Лучше не полениться и набрать два-три раза, например schet_min_zatrat, чем путаться во время отладки между schet_z и schet_p. Удобнее всего, если имена подпрограмм соответствуют выполняемым ими шагам алгоритма той или иной степени детализации: в этом случае легче выявить ошибки процесса кодирования и разработки (неправильный порядок операций, преждевременная остановка и т.п.).
Совет 4. Не вводите лишних переменных. Раздел описания должен содержать в себе только те переменные, которые затем будут встречаться в тексте программы. Это требование вызвано соображениями экономии, причем не столько места в памяти, сколько ваших усилий по вылавливанию непонятных ошибок.
Если в процессе написания программы вы решите отказаться от некоторой переменной, поймете, что она лишняя - обязательно удалите ее не только из текста программы, но и из раздела описаний. Этим вы застрахуетесь от непреднамеренного ее использования где-нибудь, откуда из-за невнимательности вы забыли ее выкинуть. Если вы уже выбросили "убиваемую" переменную из раздела описаний, но случайно оставили ее в паре мест в тексте программы - не беда: первая же компиляция исправленной программы найдет эти неописанные переменные.
Особенно внимательно нужно выполнять это правило, когда переменные не уничтожаются, а превращаются из глобальных в локальные. В этом случае забытое описание может стать причиной серьезных и при этом трудно локализуемых ошибок.
Совет 5. Всегда проверяйте, верно ли вы указали способ подстановки аргументов в параметры подпрограммы (см. лекцию 8). Особенно важно не забывать об этом правиле, если вы создаете рекурсивную подпрограмму, где одна переменная (параметр-переменная) должна возвращать самой себе какое-то значение, а другая переменная (параметр-значение), наоборот, при возвращении выполнения в данный экземпляр подпрограммы должна восстанавливать то значение, какое она имела до рекурсивного вызова.
Совет 6. Всегда отслеживайте области действия локальных переменных. Лучше всего, если в разных подпрограммах, пользующихся одними и теми же данными, параметры будут называться по-разному. Это избавит вас от такой трудно вылавливаемой ошибки, как изменение значения переменной там, где предполагается, что оно останется неизменным (так называемый "побочный эффект"). Особенно много подводных камней таится в бесконтрольном использовании одинаковых счетчиков в телах вызывающих друг друга процедур или функций. Синтаксис подобное использование разрешает, поэтому компилятор не распознает ошибку и не просигнализирует о ней. Стоит безобидной переменной-счетчику оказаться глобальной, и вам будет очень сложно понять, отчего же это она ведет себя самым загадочным образом, меняя свое значение совсем не так, как предписывает ей заголовок цикла.

Кусочки, куски и кусищи...
Во время написания текста программы всегда следуйте правилам структурного программирования. Их придумали умные люди как раз для того, чтобы облегчить процесс конструирования правильных, надежных и удобочитаемых программ.
Совет 7. Не нуждается в особой пропаганде главное правило: на каждой строке должен находиться только один оператор. А сложные операторы лучше и вовсе размещать на нескольких последовательных строках. Тогда при пошаговой прогонке легче будет локализовать искомую ошибку.
Совет 8. Не нужно полагать, что если вы являетесь автором программы, то никаких трудностей ее чтение во время отладки у вас не вызовет. Невозможно выучить непрерывно изменяемую программу настолько, чтобы ориентироваться в ней "с закрытыми глазами", поэтому не скупитесь на краткие, но дельные комментарии. Следите, чтобы они верно отражали происходящее: если, прочитав комментарий, вы будете думать, что поясняемый им блок программы "делает шаг на север", в то время как на самом деле производится "шаг на юг", то понять причину ошибки вы вряд ли сможете.
Совет 9. Пишите программы "лесенкой", позволяющей наглядно увидеть, где кончается один блок и начинается другой. Разные блоки должны иметь различные отступы, а все операторы одного блока - одинаковые; причем чем глубже уровень вложенности блока, тем дальше отстоит он от левого края экрана. Совсем без отступа можно записывать только разделы описаний головной программы, слова begin...end., ограничивающие основное тело программы, а также заголовки процедур и функций. Все остальные операторы, даже строки-комментарии, должны иметь отступ.
Исключение из этого правила составляют лишь операторы отладочной печати, которые затем будут удалены из окончательного варианта программы. Их лучше всего начинать с первой позиции строки, невзирая на отступы текущего блока. Так они будут сильнее выделяться.
Совет 10. Большие блоки, отвечающие за крупные шаги алгоритма, отделяйте друг от друга строками-комментариями, облегчающими ориентирование по тексту программы в процессе ее отладки.
Совет 11. Операторные скобки begin...end всегда расставляйте парами, причем каждый end должен начинаться с той же позиции строки, что и открывший его begin, а заключенный между ними операторный блок должен быть сдвинут еще как минимум на одну позицию вправо, чтобы скобки не сливались с остальным текстом программы.
Порядок написания любого блока должен быть следующим:

  1. begin
  2. <...> {вложенный блок}
  3. end;

В этом случае вы надежно застрахуетесь от долгих поисков того места, в которое, судя по алгоритму, нужно поставить забытый end.
Если в вашей программе несколько (хуже - много) вложенных или просто очень длинных блоков (для одного или двух коротких это еще не так актуально), то возле каждого оператора end ставьте комментарий-пометку, какой именно begin он закрывает. Не забывайте, что простое совпадение количества begin-ов и end-ов не может служить гарантией того, что все они расставлены в нужных местах. А если среди нескольких идущих подряд end-ов, закрывающих обычные begin-ы, встречается и end, замыкающий оператор case, то возле него обязательно нужно поставить указание {end_case}, иначе вы можете потратить немало времени на поиски якобы недостающего оператора begin. Дело в том, что когда мозг человека настраивается на поиск какого-либо конкретного слова, то не похожие на него слова он имеет обыкновение пропускать, не обращая на них никакого внимания, а поскольку слово case даже отдаленно не напоминает begin , то при таком поиске-просмотре оно, скорее всего, будет отброшено.
Совет 12. Желательно, чтобы каждый оператор if имел не только then-, но и else-ветвь, даже если она останется пустой. Это позволит вам избежать двусмысленных структур вроде
if ...
then if ...
then ...
else ...
которая на самом деле воспринимается компилятором совершенно иначе:
if ...
then if ...
then ...
else ...
Такие ошибки наиболее опасны при удалении из текста уже правильно работающей программы отладочных операторов. Например, если полностью удалить из конструкции
if ...
then if ...
then ...
else <один отладочный оператор>
else ...
якобы ненужную четвертую строку, то сохранить работоспособность программа не сможет. Хорошо еще, если вам повезет и результат либо вообще не будет выдан, либо окажется настолько неправдоподобным, что не заметить этой странности будет нельзя. В противном случае получится якобы правильная программа, которая на самом деле правильной не является.
Спасением от такой напасти могут послужить и добавочные операторные скобки - нужно только следить, чтобы они не слишком засоряли текст блока:
if ...
then begin if ...
then ...
else <один отладочный оператор>
end
else ...
Если теперь мы удалим ту же строку, никакого вреда программе это не нанесет. Поэтому:
Совет 13. Ненужные с точки зрения алгоритма, однако не нарушающие структуры программы операторные скобки еще никому не мешали. Как говорится, "кашу маслом не испортишь"!
Совет 14. Никогда не пользуйтесь оператором goto! Язык Pascal предоставляет достаточное количество операторов, позволяющих легко структурировать любую программу. Единственным исключением из этого правила является ситуация нескольких вложенных циклов, описанная в лекции 3. И в любом случае оператор goto не должен передавать управление назад по тексту программы.
Спасение утопающих - дело рук самих утопающих
Дадим еще один важный
Совет 15. Не полагайтесь на надежность техники, производите время от времени сохранение написанного. Простое нажатие клавиши F2 может спасти от многих неприятностей в случае "зависания" задачи, не говоря уж о внезапном отключении электроэнергии (конечно, большинство серьезных организаций сейчас имеют источники бесперебойного питания, позволяющие в экстренных случаях спасти содержимое оперативной памяти, однако особенно рассчитывать на это не стоит). По крайней мере, если вдруг когда-нибудь вы попадете в такую неприятную ситуацию, не говорите, что вас не предупреждали.
Отладка и тестирование
Если вы уже пробовали программировать, то вам, без сомнения, знакома ситуация, когда программа, которая вроде бы должна работать правильно, почему-то упорно не желает этого делать. Но даже если вы еще только начинаете приобщаться к таинствам этого нелегкого искусства - программирования, то рано или поздно вы обязательно столкнетесь с такой проблемой.
Программа написана, но не работает так, как надо...
Кажется, исправлены уже все возможные ошибки и неточности, много раз проверен и перепроверен алгоритм, и тем не менее результат выдается совсем не тот, каким он должен быть... Вот тут-то и начинается собственно процесс отладки, то есть поиска логических ошибок в программе.
Если предположить, что алгоритм для решения задачи вы избрали верно, то остается согласиться с тем, что ошибки были сделаны на этапе кодирования. Значит, некоторые части вашей программы выполняют совсем не то, чего вы от них хотите. Поэтому необходимо:

  • во-первых, найти этих делинквентов;
  • во-вторых, призвать их к порядку.

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

  • Нажатие клавиши F4 выполнит программу до той строки, на которой установлен курсор, затем приостановит выполнение.
  • Нажатие клавиши F7 или F8 позволяет выполнять программу пошагово: по одному оператору (различие между этими функциями состоит в том, производится или нет трассировка текстов подпрограмм).
  • Использование окна отладочной выдачи Debug | Watch позволяет в процессе выполнения программы следить за тем, как изменяются значения переменных (нажатие клавиши Insert добавляет в это окно новую переменную).

Более подробно эти возможности описаны в руководствах по языку и среде Turbo Pascal.
Можно дать несколько полезных советов, касающихся локализации ошибок в программе.
Совет 16. Пользуйтесь всеми доступными инструментами отладки.
Совет 17. На всех ключевых участках, а также на границах логически самостоятельных блоков вставляйте в программу отладочные операторы печати, которые позволят вам проследить изменение состояния основных переменных. Эта процедура не всегда может быть заменена просмотром изменений, который можно осуществить с помощью окна Watch. Зачастую бывает необходимо иметь перед собой для сравнения результаты нескольких последовательных итераций, либо нужно видеть одновременно первые и последние результаты, либо в процессе прогонки возвращаться по списку выдачи назад, к предыдущим результатам. Ничего этого параллельный просмотр в Watch сделать не позволяет.
Если отладочная выдача настолько объемна, что не помещается целиком на экран, ее приходится производить в файл.
Совет 18. Следите за тем, чтобы отладочная печать выдавала именно то, что нужно. Совершенно невозможно отловить ошибку в программе, если под именем проверяемой переменной вам выдается значение другой переменной!
Самый легкий случай, - когда из-за опечатки полностью меняется вид вывода. Например, правильный оператор writeln(a+1) должен выдать число, если же вместо числа на экране вдруг появляется слово FALSE, то это явный признак того, что произошла опечатка и выдается результат сравнения переменной а с единицей: writeln(a=1).
Однако не всегда удается так легко отделаться, поэтому обязательно удостоверьтесь в том, что правильной является и конечная печать результата: выдается значение нужной переменной, значение выводится в правильном формате и т.п. Такие ошибки возникают только из-за невнимательности и бывают подчас трудноуловимыми.
Совет 19. Если вы уже локализовали небольшой участок программы, в котором находится ошибка, удалите операторы отладочной печати из остальных частей программы, а в отлаживаемом участке, наоборот, увеличьте их количество.
Совет 20. Пишите "заглушки" для еще не отлаженных процедур и функций. Это значит, что вместо того, чтобы мучиться одновременно с несколькими подпрограммами, вы вынуждаете большую их часть возвращать не тот результат, какой они реально вырабатывают, а тот, который они должны были бы возвращать при правильной работе.
Совет 21. За один раз исправляйте ровно одну ошибку. Нет ничего хуже, чем гадать, которое из внесенных исправлений оказало решающее влияние на поведение программы.
Правила составления тестов
Для того чтобы отладить программу, нужно проверить ее работоспособность на каких-то входных данных. Следовательно, эти входные данные нужно каким-то образом подобрать. Затем, после выполнения программы на этих входных данных, нужно сравнить полученный результат с тем, который должен получиться, если программа работает правильно. Этот процесс и называется тестированием.
Заметим, что если полученный результат отличается от эталонного, то тест считается удачным (!), потому что он помог обнаружить ошибку. А если полученный ответ совпал с правильным - радоваться рано. Один тест не может полностью проверить всю программу, ошибка вполне могла затаиться в той части, которая осталась на сей раз невыполненной. Для того чтобы протестировать всю программу, проверить все возможные частные случаи, составляют не один тест, а набор тестов.
И здесь существуют следующие правила.
Правило 1. Любой тест должен состоять не только из входных, но и из соответствующих им выходных данных. Ведь для того чтобы понять, верный или неверный результат выдала вам программа, необходимо самому знать правильный ответ.
Проверяйте вручную результаты тестов, с помощью которых вы отлаживаете программу. Немногого можно добиться, если выдаваемый программой правильный ответ вы считаете неверным и продолжаете поиск несуществующей ошибки. Хуже того - если вы все-таки добьетесь, чтобы программа выдавала ожидаемый вами неверный результат, объяснить ее неправильное поведение на других тестах вам будет еще сложнее.
Из двух предыдущих абзацев вытекает очень важный вывод: нельзя строить тесты при помощи генератора случайных чисел. Конечно, вы можете случайным образом составить входные данные, но как быть с правильным ответом? Откуда его взять, если вы сами понятия не имеете, что там подается на вход?
Правило 2. После того как программа начала правильно работать на одном или нескольких простых тестах, усложните задания, введите тест на граничные условия или тест, содержащий значения, выходящие за рамки формата входных данных. Иногда ошибка может скрываться в тех частях программы, которые кажутся самыми прозрачными. Например, в проверке нескольких переменных на равенство между собой. Если до сих пор все переменные в тестах были разными, попробуйте прогнать программу через тесты, в которых сравниваемые значения будут совпадать - все сразу или только некоторые из них в разных комбинациях.
Правило 3. Не ограничивайтесь только похожими тестами.
Все тесты можно разделить на три группы: регулярные, граничные и критические. Например, при заданных ограничениях на параметр 0 <= x <= 100 регулярными будут все тесты, где 1 <= x <= 99; граничными, где х = 0 и х = 100; остальные - критическими. Если ваша программа правильно работает для пяти-шести тестов из какой-либо группы, можно предположить, что она выдаст правильный результат и для всех остальных тестов из этой группы.
В тестовом наборе, составленном для проверки программ, обязательно должны присутствовать тесты первых двух групп, а для проверки работоспособности "защиты от дурака" (см. лекцию 14) - и третьей тоже. Если входных параметров несколько - комбинируйте! Пусть в одном и том же тесте первый параметр будет граничным, второй - регулярным, а третий - критическим. Всегда интересно посмотреть, достаточно ли надежна ваша программа, справится ли она с таким заданием.
В хорошей, надежной программе всегда нужно писать проверку того, что файл ввода существует, не пуст и содержит данные в правильном формате, что считываемые из входного файла данные попадают в определенные условием задачи диапазоны.
Правило 4. Исправления, вносимые в программу, могут повлиять на результаты нескольких тестов.
После того, как вы нашли и исправили ошибку, вновь выполните программу на всех тех тестах, которые раньше не были успешными (то есть выдавали правильные ответы) - а вдруг найдется новая ошибка?
Вообще же, составление исчерпывающих тестовых наборов для тестирования любой программы - задача очень нетривиальная, зачастую требующая математического доказательства полноты.
Оптимизация программ
Процесс оптимизации уже написанной программы начинается только после того, как вы убедились, что ваша программа работает правильно. Стараться оптимизировать неправильную программу бессмысленно, правильнее она от этого не станет, поскольку оптимизация - это замена некоторых программных кусков эквивалентными им с точки зрения результата, но более экономичными с точки зрения выполнения.
Основная часть процесса оптимизации программы приходится на этап выбора наилучшего алгоритма. Однако даже при правильном выборе алгоритма неграмотное кодирование может привести к неэффективно работающему результату. Чтобы такого не происходило, внимательнее относитесь к тому, что и как вы пишете, реализуя выбранный вами алгоритм. Не всегда те действия, которые кажутся оптимальными с точки зрения логики алгоритма, стоит пытаться запрограммировать "в лоб".
Если же алгоритм выбран и запрограммирован "почти" правильно, можно попытаться улучшить его, внося изменения, сокращающие его работу.
В первую очередь стоит оптимизировать часто повторяющиеся куски программы, то есть циклы. Старайтесь не допускать ситуаций, когда в теле цикла раз за разом производятся одни и те же вычисления, никак не изменяющие состояние переменных. И вообще, избегайте вносить в повторяющиеся участки "тяжелые" операции.
Например, ясно, что из двух эквивалентных кусков
k:= 0;
for i:= 1 to b*100 do
k:= k+i+1000*b+100*(b div 2);
и
a:= 1000*b+100*(b div 2);
k:= 0;
for i:= 1 to b*100 do k:= k+i+a;
второй является и более быстрым (особенно если b = 10000), и более компактным, чем первый.
Лишние вычисления, лишние действия с файлами или структурами, лишние пересылки элементов из одной ячейки памяти в другую (и список этот далеко не полон!) - все это снижает эффективность программы. Старайтесь помнить о том, что лаконичность приветствуется всегда и везде, а особенно в программировании.
Успехов вам в написании красивых и полезных программ!

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика