Добавил:
github.com Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Технология программирования / 2_1_otladka_v_konsoli_30_08_19_-_2_Chasa.doc
Скачиваний:
4
Добавлен:
30.09.2023
Размер:
3.56 Mб
Скачать

Лабораторная работа №2 (1 часть) Отладка консольного приложения.

Четыре категории программных ошибок

Существует четыре основных категории ошибок, с которыми можно столк­нуться в процессе разработки программы:

  • Синтаксические, или времени компиляции. Ошибки, возникающие во время компиляции.

  • Компоновщика. Ошибки, возникающие на этапе компоновки, которая производится для получения исполняемого файла.

  • Времени выполнения. Ошибки, возникающие при выполнении програм­мы.

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

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

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

Синтаксические ошибки

Самым частым типом ошибок являются синтаксические ошибки, когда в программе встречается оператор, нарушающий какое-либо из грамматических или синтаксических правил C/C++. Эти ошибки автоматически детектируются при компиляции программы (рис. 3.1).

Хотя почти всех программистов синтаксические ошибки раздражают, они на самом деле являются самыми безобидными. Для устранения синтаксиче­ской ошибки нужно просто найти ту точку, где программа нарушает правила грамматики C/C++, и исправить это нарушение.

Когда компилятор C/C++ транслирует исходный код в исполняемую программу, он пытается обнаружить и сообщить о возможно большем числе синтаксических ошибок, а также предупреждает об операторах, которые формально законны, но могут вызвать ошибки в дальнейшем. Компилятор Visual C++ может сообщать об объявлениях переменных, которые никогда не ис­пользуются, и о чтении значения переменной до ее инициализации. Он может даже определить, что логический поток управления программы не до­пускает исполнения некоторых сегментов кода, — в этом случае компилятор сообщает о «невозможности построения кода».

Рис. 3.1. Синтаксические ошибки.

Ошибки компоновщика

Другой источник ошибок связан с этапом компоновки программы с библио­теками, которые она использует. Типичным примером может служить ошибка в программах Unix, вызывающих функцию квадратного корня sqrt() матема­тической библиотеки. Функция объявляется в файле <math.h> (для нестан­дартных приложений) или <cmath> (для приложений, использующих стан­дартное пространство имен) и для правильной компиляции программы требу­ет препроцессорной директивы #include <[math.h|cmath]>.

Однако такая программа все равно не будет компоноваться без ошибок, по­тому что компоновщик Unix не подключает математическую библиотеку авто­матически. Нужно явным образом дать ему такое указание посредством ключа “lm. Устранение ошибок этапа компоновки подчас требует таких знаний о компьютерной системе, которыми большинство программистов не обладает.

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

Рис. 3.2. Ошибка компоновщика.

Здесь программист случайно ошибся в написании имени функции main(). Обратите внимание, что идентификатору main в диагностическом сообщении предшествует символ подчеркивания: _main. Подчеркивание на самом деле говорит нечто о характере ошибки. Внимательное изучение сообщения показывает, что программа успешно компилировалась; фатальная ошибка произошла при компоновке. Компоновщик сообщает о ненайденном декорированном имени функции (иногда называемом сигнатурой) и соответствующим образом определенной библиотечной процедуре.

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

А здесь программист объявляет 2 функции:

Но, по какой-либо из причин компилятор не видит их определения.

Тогда получаем ошибку компоновки.

Ошибки времени выполнения

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

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

Ошибки времени выполнения включают в себя следующее:

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

  • системные ошибки, например, неудачная файловая операция или переполнение очереди сообщений;

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

  • исключения или ошибки, специфичные для приложения, такие как неверный формат ввода.

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

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

Отыскивать такие ошибки проще всего с помощью символического отладчика. Отладчик Visual C++ позволяет проследить выполнение программы, исполняя ее по одному оператору за раз, пока не встретится оператор, генерирующий ошибку. Как только ошибка идентифицирована, ее нужно исправить, заменив некорректный оператор корректным, а затем заново компилировать, компоновать и запустить программу.

Ниже приведена типичная ошибка времени выполнения: выход за границы массива.

Логические ошибки

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

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

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

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

Даже когда весь цикл разработки завершен и приложение передано пользо­вателю в рабочем, по мнению разработчиков, состоянии, все равно могут ос­таться ошибки. Было много примеров тому, как программное обеспечение ра­ботало, но не делало того, что ему полагалось делать. Это значит, что оно не со­ответствовало спецификациям задачи.

Может встречаться и несколько иная ситуация, когда заказчик программы неправильно формулирует спецификацию задачи. Это может происходить, если он и сам не знает, что ему нужно. В спецификации может быть упу­щено нечто, тривиальное или критическое, требования могут быть неясно сформу­лированы или, как это часто случается, заказчик может передумать уже после того, как разработка началась.

Явная логическая ошибка приведена ниже: условия будет выполняться всегда, так как программист вместо «==» ( сравнение) использовал «=» (сравнение и присваивание в одном операторе)

Расшифровка сообщений об ошибках

Кода вы компилируете и компонуете программу в Visual C++, автоматиче­ски открывается окно Output, отображающее информацию о статусе процесса построения, включая все предупреждения и сообщения об ошибках. Это окно может помочь вам найти и исправить причины сообщения об ошибке двумя способами: автоматически проследив строку кода, генерировавшую сообщение, либо отобразив страницу контекстно-зависимой справки, содержащую дополнительную информацию о данном сообщении.

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

Чтобы получить справку о данном сообщении, нужно сначала щелкнуть на номере сообщения в окне Output, а затем нажать клавишу F1. В окне Help откроется страница справочника, соответствующая выбранному номеру ошибки (см. рис. 3.3).

Часто в этих справочных файлах приводится объяснение ошибки в синтаксисе или логике, которое помогает исправить ошибочный оператор. По мере эволюции Visual C++ совершенствовались и его справочные файлы; теперь они часто содержат примеры кода, поясняющие причины генерации ошибок.

Рис. 3.3. Страница справочника.

Проектирование с обработкой исключений

Важным аспектом программирования является обработка ошибок, поэтому вполне естественно, если этот аспект отражен в специальной конструкции языка. Комитет ANSI C++ в свое время признал, что исключения существенно отличаются от выходов из цикла и от других управляющих структур, и реко­мендовал специальный языковой механизм для управления программой в ис­ключительных ситуациях. C++ предусматривает специальные конструкции управления исключениями для:

  • определения исключений;

  • сигнализации о возникшем исключении;

  • определения процедур обработки для каждой категории исключений.

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

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

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