“Не ошибается тот, кто ничего не делает.”
Русская народная мудрость.
Синтаксические ошибки
Процесс текстового программирования практически ничем не отличается от выполнения домашней работы по родному, например русскому, языку или литературе: вы просто пишете какой-то текст, руководствуясь заданными правилами. Разумеется, обычный литературный текст предназначен для прочтения его человеком, поэтому он может содержать различные литературные вольности, ошибки, неточности, опечатки и т.п. – все это любой человек в состоянии осмыслить, понять и исправить у себя в голове при прочтении. Поэтому если вы напишете “Масква” или “валисапет” вместо “Москва” и “велосипед” или забудете поставить запятую, которая не очень сильно влияет на смысл предложения (“Казнить нельзя, помиловать.”), ничего страшного (разумеется, кроме снижения оценки) не произойдет: человек в состоянии догадаться, что вы имеете ввиду даже при наличии в тексте подобных “некритичных” ошибок.
Компьютер, и в частности, программы (такие программы называют компиляторами или интерпретаторами), обрабатывающие написанные программистами тексты программ ведут себя гораздо строже по отношению к ошибкам: они в принципе не позволяют допускать ошибки, и даже не пытаются “угадывать”, что вы имели ввиду. Каждая команда имеет свой строго определенный формат (правила написания) и любое отклонение от этого формата воспринимается как ошибка. При этом как правило любая ошибка останавливает дальнейшую обработку текста программы – до тех пор, пока ошибка не будет исправлена. В конечном итоге программа запустится только в том случае если в ней вообще не будет подобных ошибок. Согласитесь, это очень удобно!
Но все это относится к ошибкам, которые возможно обнаружить, таким как “Масква” или “валисапет”. Такие ошибки называют в программировании синтаксическими так как они содержат явные нарушения форматов команд (нарушения синтаксиса языка программирования). Одной из важнейших задач полноценных компиляторов и интерпретаторов является выявление в программе всех возможных синтаксических ошибок.
Логичнеские ошибки
На самом деле синтаксические ошибки – наиболее простые из всех возможных программных ошибок, которые легко обнаружить и исправить. Гораздо более опасными и менее заметными являются ошибки другого рода: опечатки, неполный анализ ситуации в алгоритме, неверная логика работы программы (нам кажется, что мы понимаем как это должно работать, а на самом деле оно должно работать совсем по-другому) и т.п. Это так называемые логические или смысловые ошибки. Такие ошибки не поддаются проверке при обработке программы компилятором.
Например, если мы хотим написать команду, которая должна посчитать площадь квадрата со стороной 4 и записать полученное значение в переменную S, то мы можем написать следующее:
S = 4.4
Здесь нет никаких ошибок кроме той, что мы допустили опечатку: вместо знака умножения “*” мы поставили точку “.”. В результате не возникнет никаких ошибок, но программа будет работать неправильно: вместо числа 16 в переменную S будет записано число 4,4 потому что точка в программе воспринимается как разделитель целой и дробной частей в записи числа.
Такие опечатки, приводящие к логическим ошибкам можно назвать “синтаксическими ошибками второго рода”. Программист в состоянии обнаружить такие ошибки, если понимает, что и как должна делать программа и будет внимательно читать текст программы.
Кроме того, опечатки, разумеется, могут приводить и к обычным синтаксическим ошибкам, которые автоматически могут быть выявлены компилятором.
Самый сложный класс ошибок – это логические ошибки программ, возникающие в результате ошибок нашего мышления, анализа, ошибок в понимании задачи, ошибок в алгоритмах и т.п. Обнаружить такие ошибки сходу – практически невозможно. Сложность тут в том, что суть этих ошибок – у нас в голове, и пока мы сами не разберемся с источником этих ошибок, мы не то что исправить – увидеть их не сможем.
Этот класс ошибок подобен ложным или ошибочным утверждениям. Точно так же как предложение языка, не содержащее грамматических или пунктуационных ошибок, однако содержащее неверную информацию: никто не поставит вам двойку по грамматике, однако вполне может указать на ошибочность или недостоверность утверждения, содержащегося в предложении.
Например, вот абсолютно верное с грамматической точки зрения предложение:
“Великий русский поэт Александр Сергеевич Пушкин очень любил писать программы на языке программирования Go для своего компьютера PDP-11.”
Здесь нет ни одной грамматической ошибки или опечатки, однако есть масса фактических:
- во времена А.С. Пушкина вообще не существовало компьютеров;
- А.С. Пушкин никогда не писал программы;
- язык программирования Go создан в 2003 году, на 20 лет позже периода существования компьютеров серии PDP-11.
Чтобы выявить ошибки в этом предложении, не достаточно знания русской грамматики: для этого необходимо знать историю компьютеров и историю жизни А.С. Пушкина. То есть, выявить в этом предложении ошибки под силу только осведомленному человеку, простая программа анализа текста тут бессильна. То же самое и с логическими ошибками в программе: программа может не содержать ни одной синтаксической ошибки, но при этом быть абсолютно неработоспособной или работающей совсем не так, как необходимо.
Суть в том, что первоисточником таких ошибок являемся мы сами, и никакой искусственный интеллект не сможет понять, верно ли мы понимаем и решаем задачу. То есть, такие ошибки сродни нашим жизненным ошибкам, и исправить их можно только улучшая наше понимание задачи, методов ее решения и реализуя эти знания в нашем коде.
В результате логических ошибок могут также возникать самые разные проблемы, некоторые из которых будут вызывать явные нарушения в работе вашей программы, её компонентов или операционной системы в целом. Это могут быть, например, “запрещенные” математические операции, приводящие к переполнению буфера для хранения данных (например, деление на 0), также это могут быть ошибки, связанные с конечной разрядностью областей хранения данных. Кроме этого существует большое количество недопустимых – с точки зрения операционной системы – операций, которые могут приводить к аварийному завершению вашей программы или даже к краху всей операционной системы. Разумеется, до запуска программы такие ошибки найти очень сложно.
Избежать таких ошибок можно, в первую очередь, обладая фундаментальными знаниями в математике и информатике, что позволит вам понимать, что же на самом деле будет происходить в компьютере при выполнении вашего кода. Разумеется, любителей всевозможной “магии” в программировании ждет огромное разочарование: “магическим образом” – в программах появляются только труднонаходимые логические ошибки.
Кроме того, избежать подобных ошибок, возникающих во время выполнения можно на этапе тестирования вашей программы. Чем больше, сложнее и изощреннее будут тесты, тем более велика вероятность нахождения подобных ошибок. Однако, лучший способ – все-таки предыдущий: глубокое понимание того, что происходит при работе вашей программы.
Однако, самые неприятные логические ошибки могут вообще никак себя не проявлять: программа просто будет делать “что-то не то”, да и то не во всех случаях. Тут кроме фундаментальных знаний по математике, информатике и тем наукам, с которыми связана ваша программа необходимо еще и глубокое понимание решаемой задачи.
Фактически, при решении любой задачи мы сначала должны понять саму задачу. Хотя в ряде случаев у нас вообще нет задачи как таковой, а есть только пожелание клиента, которое еще нужно понять, уточнить, дополнить и только потом пытаться формулировать исходную задачу, внешние условия и т.п. А затем эту задачу надо как-то формализовать – сформулировать так, чтобы её можно было решить в программном коде. После чего нужно продумать структуру программы и данных, с которыми она будет работать и только затем все это реализовать. И тут – чем больше мы продумаем в начале, до самого этапа написания кода, тем меньше ошибок будет в нашей программе.
В конечном итоге поиск любых ошибок сводится к внимательному изучению и анализу кода программы. Только полное понимание того, что происходит в программе в каждой строке кода позволяет найти и исправить самые неочевидные логические ошибки.
Отладка
Для того чтобы лучше способствовать подобному пониманию современные средства разработки программ оснащены различными инструментами. Основное средство поиска программных ошибок – различные отладчики. Отладчики позволяют останавливать программу в любом месте, выполнять программу по шагам, проверять состояние памяти в любой момент выполнения, получать информацию о текущих значениях переменных и т.п.
Отладчики еще называют “дебагерами” – от английского “debugger”. Дело в том, что самая первая компьютерная ошибка возникла из-за попадания бабочки между контактами реле. “Bug” – английски означает “жук” или “насекомое”. Следовательно, дебагер – “средство уничтожения жуков”.
Кроме отладчиков существуют также программы глубокого анализа программного кода с целью поиска потенциально возможных логических ошибок.
Использование отладчиков не исключает тестирования программ. Более того, тестирование программ параллельно с использованием отладчиков – наиболее эффективный способ поиска ошибок или обнаружения “тонких мест” – участков кода, в которых потенциально возможно возникновение проблем, например, при очень больших объемах обрабатываемых данных.
Однако, даже без использования отладчиков возможна вполне удобная отладка программ с использованием специальных приемов:
- Создание в программе точек останова – с помощью команд ожидания нажатия на клавиатуру.
- Организация вывода на экран (в консоль) значений переменных в нужных местах кода.
- Выключение фрагментов кода – с помощью комментирования.
- Замедление работы программы – путем вставки команд временной задержки.
- Явное задание значений переменных в коде программы.
- Принудительный ввод пользователем значений переменных в процессе работы программы – с помощью операций ввода.
- Вывод на экран любой интересующей информации в процессе работы программы – с помощью операций вывода.
- Включение в программу дополнительных условных операторов, проверяющих значения переменных.
- Вывод в файл интересующих переменных и данных программы.
и т.п.
Большинство этих функций сейчас реализуют полноценные программы-отладчики, однако вполне может возникнуть ситуация, когда у вас под рукой будет только компилятор, поэтому эти приемы отладки тоже стоит знать.
Эффективными способами отладки программ (приведения их в полностью рабочее состояние в соответствии с проектом и техническим заданием) являются:
- сам процесс отладки на этапе программирования с помощью специальных программ-отладчиков;
- процесс тестрирования уже готовых программ на предмет выявления различных, как правило, логических ошибок.
Вот как, например, выглядел рабочий экран программы Borland Turbo Debugger, входящего в пакет Borland C++ для операционной системы MS DOS:
Программы-отладчики — это очень эффективные инструменты, которые помогают разработчикам выявлять и исправлять ошибки в программном коде на этапе программирования. Они предоставляют различные функции для анализа поведения программы в процессе её выполнения.
- Установка и управление точками останова.Точки останова позволяют приостановить выполнение программы на определённой строке кода. Это полезно для того, чтобы проверить состояние программы в определённый момент.Разработчик может установить точки останова в интересующих его местах и управлять ими, например, включать/выключать, удалять или изменять их.
- Пошаговое выполнение кода. Отладчик позволяет выполнять программу по шагам, строка за строкой, чтобы разработчик мог наблюдать за выполнением каждой инструкции.Существует несколько стандартных режимов пошагового выполнения:
- Перейти внутрь вызванной функции, чтобы отследить её выполнение.
- Выполнить текущую строку, не заходя внутрь вызываемых функций.
- Выйти из текущей функции и продолжить выполнение программы в вызывающей функции.
- Просмотр и изменение значений переменных. Отладчик позволяет в любой момент выполнения программы просматривать значения переменных и выражений. Можно добавлять переменные в список, чтобы постоянно отслеживать их изменения во времени. В некоторых отладчиках также можно изменять значения переменных во время выполнения программы, что помогает проверить, как внесенное изменение данных повлияет на дальнейшее выполнение.
- Анализ стека вызовов.Отладчик предоставляет доступ к стеку вызовов — списку всех активных функций, которые были вызваны до текущего момента выполнения программы.Это помогает понять, как программа попала в текущую точку, и позволяет перейти к любой из функций в стеке для её анализа.
- Просмотр памяти.Отладчик позволяет напрямую просматривать содержимое памяти, включая глобальные и локальные переменные, массивы, структуры и другие данные.Это особенно важно для отладки низкоуровневого кода или программ, работающих с указателями и адресами памяти.
- Отслеживание и анализ регистров процессора.В отладчиках для низкоуровневых языков, таких как C или ассемблер, можно отслеживать содержимое регистров процессора.Это полезно при отладке кода, который работает на уровне инструкций процессора.
- Отслеживание потока выполнения.Для многопоточных приложений отладчики позволяют отслеживать выполнение отдельных потоков, переключаться между ними и управлять их состоянием.Также можно отслеживать состояние процессов, если приложение состоит из нескольких процессов.
- Регистрация и анализ событий. Некоторые отладчики могут записывать события или трассировку выполнения программы для последующего анализа.Это полезно для выявления сложных проблем, которые возникают при определённых условиях, которые трудно воспроизвести вручную.
- Отслеживание точек останова по условию.Можно устанавливать условные точки останова, которые приостанавливают выполнение программы только тогда, когда определённое условие выполнено.Это позволяет сосредоточиться на конкретных сценариях, что особенно полезно при отладке сложных циклов или рекурсивных вызовов.
- Отладка удалённых систем.Некоторые отладчики поддерживают режим удалённой отладки, которая позволяет отлаживать программы, запущенные на другом компьютере или устройстве.Это полезно для разработки и отладки программного обеспечения для устройств типа телефонов или планшетов, встроенных систем, серверов или облачных сред.
- Просмотр ассемблерного кода. Отладчик может преобразовывать код программы в ассемблерный код, чтобы показать разработчику, какие инструкции выполняются на уровне процессора. Это полезно при отладке компилированного кода или при анализе оптимизаций, выполненных компилятором, а также для пониания, как именно выполняется та или иная команда в программе.
- Сравнение с эталонными значениями. Некоторые отладчики позволяют сравнивать текущее состояние переменных с ожидаемыми значениями и автоматически уведомлять о расхождениях.
- Сохранение и восстановление сессий отладки. Отладчик может сохранять сессии отладки, включая точки останова, значения переменных и стек вызовов, чтобы разработчик мог вернуться к отладке позже. Программы-отладчики являются важными инструментами для разработчиков, так как они значительно упрощают процесс нахождения и исправления ошибок, помогая сделать программы более надёжными и эффективными.
- Трассировка ппрограммы позволяет отслеживать её выполнение, записывая последовательность выполнения команд и изменения в данных.
- Многрие отладчики имеют режим профайлера, когда анализируется производительность программы, требуемые на каждом шаге ресурсы, сколько времени занимает выполнение различных частей кода.
- Также, отладчики имеют режим журналирования (ведение логов – от английского слова logging). В этом режиме в файл записываются все важные и выбранные программистом события и состояния программы, кроме того, можно записывать данные профайлера и значения переменных.
Эти функции делают отладчики незаменимыми инструментами для разработки и тестирования программного обеспечения на уровне разработки.
Пример работы с точками останова и с пошаговым выполнением программы в MS Visual Studio:
А вот как выглядит окно контроля и изменения значений переменных:
Когда программист пишет программу, он в первую очередь ориентируется на то, что программа должна работать правильно. Имеется ввиду, что основной алгоритм программы рассчитан на то, что все будет происходить корректно: программа штатно запустится, найдет все необходимые данные, представленные должным образом, получит на вход такие же корректные данные, корректно их обработает и отправит получателю без проблем. Но даже в цифровой реальности так не бывает. В процессе работы программы может возникнуть любое количество серьезных проблем, способных прерваль описанное выше “правильное”выполнение кода. Поэтому правильно написанная программа должна уметь обнаруживать и корректно обрабатывать различные ошибки, как свои внутренние, так и внешние, не зависящие напрямую от кода программы – связанные с входными данными, работой интерфейсов и т.п.
Элементарный пример: ваша программа должна открывать файл и считывать из него данные. Пусть штатно этот файл должен находиться в той же папке, что и сама программа. Теперь представьте себе ситуацию, что программа запущена, а файла в папке по какой-то причине нет. Если эта ситуация в коде никак не обрабатывается, произойдет аварийное завершение программы без какой-либо информации для пользователя: что произошло, что делать. Такие ошибки называются ошибками времени выполнения (runtime errors).
Разумеется, так быть не должно. Самый простой способ решения проблемы – проверить в самой программе наличие нужного файла и в случае его отсутствия сообщить пользователю, что такой-то файл не найден. Более интеллектуальный подход может позволить пользователю вручную указать место расположения нужного файла – например, с помощью диалогового окна. В зависимости от специфики вашей программы могут существовать и другие способы решения проблемы.
И таких “узких мест” в любой программе могут быть сотни. Единственный способ обходить их – это предусматрисать потенциальные проблемы и писать код, который будет их корректно обрабатывать, не вызывая ошибок и аварийного завершения программы. Поэтому при проектировании хорошей программы необходимо стараться учесть все возможные потенциальные проблемы и создать для каждой свой обработчик.
В пределе некая “идеальная” программа должна уметь “предугадывать” и предвосхищать все мыслимые и немыслимые, почти невозможные потенциальные ошибки. Для этого разумно использовать всевозможные проверки условий.
Тестирование
После того как программа прошла все испытания самим разработчиком с помощью этих интструментов, наступает последний этап – тестирование. В настоящее время тестирование – важнейший этап разработки программного обеспечения. Тестирование программного обеспечения (ПО) — это процесс проверки программного продукта на соответствие его требованиям, выявления дефектов и повышения качества продукта.
Основными направлениями тестирования являются:
- Выявление ошибок, которые могут привести к неправильному функционированию программы.
- Проверка соответствия программы требованиям ТЗ или проекта. Она включает в себя проверки того, что функциональность, производительность, безопасность и другие аспекты соответствуют ожиданиям.
- Обеспечение качества, проверяя его на предмет ошибок, производительности, безопасности и других факторов, которые могут повлиять на пользовательский опыт.
- Предотвращение дефектов на этапе раннего тестирования (например, тестирование на уровне требований и дизайна)до того, как они попадут в код.
- Тестирование производительности программы (нагрузочное, стрессовое, тестирование на отказоустойчивость) позволяет оценить, как она работает под нагрузкой и т.п.
- Проверка безопасности направлена на выявление уязвимостей, которые могут быть использованы злоумышленниками для получения несанкционированного доступа к данным или для нарушения работы системы.
- Тестирование совместимости проверяет, как программа работает в разных окружениях, операционных системах, браузерах, устройствах и сетях.
- Проверка удобства использования оценивает, насколько программа удобна и интуитивно понятна для пользователей. Это помогает внести улучшения в интерфейс и пользовательскиие функции.
- Тестирование функциональности фокусируется на проверке того, что все функции программы работают правильно, именно так, как было задумано.
- Регрессионное тестирование, которое проверяет, не появились ли новые ошибки в уже проверенных частях системы после внесения изменений или добавления нового функционала.
- Тестирование также помогает выявить и оценить риски, связанные с выпуском программного обеспечения, такие как недоработанные функции, уязвимости безопасности или проблемы с производительностью.
Тестирование сопровождает программный продукт на всех этапах его жизненного цикла: от начальных этапов разработки (еще до написания кода) до выпуска, развертывания и поддержки. Оно помогает гарантировать, что продукт остаётся работоспособным и соответствует требованиям на протяжении всего времени эксплуатации.
Эффективное тестирование помогает повысить уверенность команды разработки и заказчиков в том, что продукт готов к выпуску и будет соответствовать ожиданиям пользователей.
Разумеется, процесс тестирования обязательно сопровождается ведением подробной документации по результатам каждого шага тестирования, включая отчёты о найденных дефектах, покрытия тестами и результаты тестов, помогает команде разработчиков понимать текущее состояние продукта и принимать обоснованные решения по исправлению ошибок, доработке и дальнейшему развитию.
Фактически, задачей тестировщиков является не столько проверка, что программа работает нормально в штатных условиях, сколько попытки “сломать” программу, давая ей максимально возможное количество корректных и некорректных наборов входных данных для обработки. При этом надо понимать, что данными являются также и любые действия пользователя в отношении интерфейсов программы. И тут нужно рассматривать не только стандартные случаи, не только случаи максимально расширенные, но и нереальные, почти невозможные.
Тестирование программного обеспечения помогает повысить надёжность, качество и безопасность программных продуктов, минимизируя лишние затраты на разработку, количество дефектов и улучшая пользовательский опыт.
Также, для повышения качества и отказоустойчивости программ рекомендуется подробное комментирование кода для его более полного понимания и рисование блок-схем отдельных блоков кода. Хорошие результаты, особенно на начальных этапах обучения программированию показывает методика «Алгоритм-Комментарии-Код».
Следует помнить, что по-настоящему хорошие программы должны сами “уметь” тестировать свое окружение и стараться исправлять все сбойные ситуации, которые можно исправить.
Кстати, одна из типичных, однако крайне неприятных и очень дорогостоящих программных ошибок произошла 4 июня 1996 года на французском космодроме Куру на ракете-носителе “Ариан-5”. Эта ошибка привела к полному разрушению ракеты стоимостью в полмиллиарда долларов. Однако, это была далеко не единственная подобная ошибка.
Итак:
- Программыне ошибки бывают двух основных типов: синтаксические и логические.
- Синтаксические ошибки легко обнаруживаются в процессе написания кода программы и его компиляции или интерпретации.
- Логические ошибки – самые “трудноуловимые”. Для их поиска в процессе разработки программного обеспечения служат этапы отладки и тестирования.
- Отладка программ может выпольняться вручную – с помощью специальных приемов или с помощью специальных программ – отладчиков.
- Тестирование программ является важнейшим этапом разработки и решает множество задач, упрощающих и ускоряющих процесс разработки.
На следующем занятии речь пойдет о программных структурах, обеспечивающих автоматическое повторение блоков кода, о циклах.
Поделиться: