Занятие третье
Продолжение. Начало см. в # 10, 11/97.
Этим занятием мы завершаем наш учебный курс для пользователей системы Borland C++ Builder. Темой этого занятия станет отладка приложений. Мы рассмотрим технологию отладки событийно-управляемых приложений и возможности интегрированного отладчика C++ Builder.
Как выполняются Windows-программы
Чем отличается выполнение DOS-приложений от выполнения приложений, управляемых событиями, каковыми являются все приложения для Windows? Непредсказуемостью! Да-да, именно непредсказуемостью. В DOS ваша программа выполнялась от инструкции к инструкции, периодически обращаясь за помощью к операционной системе: где буковку на дисплей вывести, где клавишу считать, где прерывание какое-нибудь вызвать. В Windows все иначе: ваше приложение запускает себя, создает окошко (а может быть, и не одно) и отдает себя в руки Windows, которая становится главным распорядителем. С этого момента все команды, связанные с действиями пользователя, обрабатывает операционная система, и уже она решает, отдать или нет управление вашей программе. Это и является причиной непредсказуемости, называемой системными программистами асинхронностью. Ваше приложение просто не может знать, когда его "позовут к столу" и по какому поводу. Возможно, пользователь выберет пункт из меню, а может быть, сработает системный таймер или передвинется мышь, кто ж его знает... Если событие все-таки возникает, Windows ищет в вашей программе соответствующий обработчик этого события (про обработчики мы уже с вами говорили на предыдущем занятии). Если таковой имеется, то управление передается ему. Последний делает свое дело и возвращает управление Windows. Если управление не вернуть, то в окне менеджера задач напротив названия вашей программы возникнет строчка Not responding ("Не отвечает"). Ну а дальше вы сами знаете, что с такими программами нужно делать.
Отладочные команды
Познакомимся с командами для отладки из меню Run. Их довольно много, хотя в режиме компиляции и редактирования многие отключены. Перечислим интересующие нас пункты меню и их назначение:
Стратегия отладки Windows-программ
Отлаживая программу, программист обычно пользуется несколькими стандартными операциями:
Эти шаги одинаковы для отладки любых программ, выполняющихся на любых платформах. Однако техника их применения может несколько отличаться. Так, например, прогнозировать выполнение программы в DOS легко, а выполнение приложения в Windows может быть лишь смоделировано. Из этого следует, что при отладке DOS-программу можно выполнять пошагово, перебираясь от одной строки исходного текста к другой. В Windows это нереально. Нужно поставить точку прерывания на интересующем вас участке программы и запустить ее на выполнение командой Run - Run. Достигнув точки прерывания, программа приостановит свое выполнение, давая возможность программисту приступить к отладочным действиям.
Установка точек прерывания
В приложениях, выполненных с применением C++ Builder, точки прерывания удобнее всего ставить внутри обработчиков событий. В файле проекта это делать бесполезно, потому что он сгенерирован автоматически, и вряд ли вам придется вносить туда изменения, что способно вызвать ошибки.
Простейший способ установить точку прерывания - нажать на клавишу
Повторное нажатие на
Дополнительно можно задать количество проходов, после которых точка прерывания переходит в активное состояние. Это весьма удобно при отладке длинных циклов, когда необходимо пропустить некоторое количество итераций.
Как правильно устанавливать точки прерывания? Это довольно сложный вопрос. Наверняка узнать правильное местоположение трудно, но есть два простых правила, которые помогут вам при отладке.
Попытайтесь определить место потенциальной ошибки. Это не всегда можно сделать, но попытаться надо. К примеру, вы нажимаете кнопку, а в ответ ничего не происходит или происходит не то, что вы ожидали. Ясно, что ошибка возникает либо в обработчике нажатия кнопки, либо в методах и функциях, которые из него вызываются. Отсюда следует, что установить точку прерывания нужно в начале данного обработчика.
Если не удается определить место потенциальной ошибки даже примерно, нужно поставить точки прерывания в начале каждого обработчика или, если их много, в группе из нескольких обработчиков. Затем воспроизведите ошибочную ситуацию и найдите, какая из точек прерывания сработает. Остальные можно снять за ненадобностью.
Когда ошибочный обработчик найден, нужно приступать к уточнению места ошибки. Для этого подумайте, что должно происходить в каждой строке программы в соответствии с логикой ее работы, и начинайте продвигаться шаг за шагом командой Trace Into (можно просто нажимать клавишу
Ошибки в программе могут быть самыми разнообразными. Но для Windows-приложений характерны следующие потенциальные "жучки":
Отображение трассы выполнения
Во многих случаях обнаружить ошибку поможет трассировка содержимого переменных с помощью функции API OutputDebugString(), которая пересылает строку-аргумент в отладчик. В Borland C++ Builder все такие строки отображаются в отдельном окне редактора как содержимое файла OutDbgX.txt, где X - некий уникальный номер. Проанализируйте полученный поток сообщений и найдите ошибку, произошедшую по ходу выполнения. Можно выводить с помощью OutputDebugString() и значения переменных. Нужно только преобразовать их в строку. Ниже приводится фрагмент файла проекта с трассировочными сообщениями:
... try { Application->Initialize(); OutputDebugString("Initialized..."); Application->CreateForm(__classid(TForm1), &Form1); OutputDebugString( AnsiString( "Main form created " + IntToHex( int(Form1), 8) ).c_str() ); Application->Run(); OutputDebugString("Finishing..."); } ...
При прохождении каждого нового участка исходного текста отладчику посылается строка, говорящая о состоянии программы. А после создания главной формы выводится указатель на ее экземпляр. Выглядит это сложновато, потому что нужно привести указатель к типу int, чтобы затем его преобразовать в строку шестнадцатеричного формата. Затем полученная строка типа AnsiString преобразуется вызовом метода c_str() к обычной цепочке байтов с нулем на конце. Можно упростить задачу и использовать функции sprintf wsprintf библиотеки языка Cи++.
Окончательное тестирование
Предположим, все ошибки найдены и устранены. Но червь сомнения все равно не дает покоя: а не осталось ли чего еще... Это значит, что вы подошли к этапу окончательного тестирования и вам предстоит изрядно повозиться. Первейшее средство для выявления скрытых ошибок - утилита Stress из комплекта разработчика Microsoft SDK. Запустите через нее свою программу, задав "нечеловеческие" условия работы вроде нехватки оперативной памяти, подкачки на диск и т. д. Очень вероятно, что многие огрехи программирования мгновенно выплывут. Если же и в этом случае все в порядке, то следует поискать другой род ошибок: ошибки логические. На их поиске обычно седеют даже опытные разработчики. Воспользуйтесь методом крайних элементов: отдельно взятую функцию программы вызывайте с различными аргументами. Сначала это нормальные допустимые логикой аргументы, затем минимальные аргументы, потом максимальные и, наконец, аргументы, выходящие за минимальный и максимальный допустимые пределы. Получаемый результат сравните с ожидаемым. Если все нормально, функцию можно считать благополучной и переходить к следующей. Для экономии времени лучше всего начинать с функций верхнего уровня. Тогда при благоприятном исходе можно отсечь все подчиненные функции более низкого уровня.
Для удобства работы не поленитесь и напишите маленькую тестовую программу, которая сделает все необходимые вызовы функции с различными аргументами за один проход. Результат выведите с помощью функции OutputDebugString(). Такую программу достаточно будет запустить один раз и просмотреть полученную трассу выполнения. Если же этот метод для вас неприемлем, то можно воспользоваться командой Run - Evaluate/ Modify. В появившейся диалоговой панели аргументы можно задавать вручную по ходу выполнения программы. В примере, показанном на рис. 3, потребовалось узнать, что произойдет, если изменить значение переменной Editor->SelStart. Открыв диалоговую панель Evaluate/Modify, мы увидели, что первоначальное значение интересующей нас переменной равно нулю. Затем мы ввели значение 100 в поле New Value и нажали на кнопку Modify. Значение Editor->SelStart поменялось с 0 на 100. Это легко проверить командой Inspect (или нажав
И еще вариант: можно написать мини-программу, которая будет вызывать тестируемую функцию, передавая ей целую вереницу параметров и сводя результаты в таблицу. Такое средство может за считанные минуты перебрать миллионы различных комбинаций параметров, выдав такую обширную статистику, что ни одной ошибке просто не удастся ускользнуть.
Некоторые рекомендации
Эти маленькие советы касаются выбора ракурса просмотра данных или просто помогут вам сэкономить время:
И последнее: никогда не поздно попробовать запустить откомпилированное приложение без отладчика. Бывает, что вне последнего программа работает нормально.