close

Вход

Забыли?

вход по аккаунту

?

682.Средства отладки в Visual С++ Лаврентьев И В

код для вставкиСкачать
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Министерство образования и науки Российской Федерации
Федеральное агентство по образованию
Ярославский государственный университет им. П. Г. Демидова
Кафедра компьютерной безопасности
И. В. Лаврентьев
В. А. Никулин
Н. Б. Чаплыгина
Средства отладки
в Visual C++
Методические указания
Рекомендовано
Научно-методическим советом университета для студентов,
обучающихся по специальностям Математика
и Прикладная математика и информатика
Ярославль 2010
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
УДК 519.72
ББК З 973.2–018.2я73
Л 13
Рекомендовано
Редакционно-издательским советом университета
в качестве учебного издания. План 2009/10 года
Рецензент
кафедра компьютерной безопасности Ярославского
государственного университета им. П. Г. Демидова
Лаврентьев, И. В. Средства отладки в Visual C++ :
метод. указания / И. В. Лаврентьев, В. А. Никулин,
Л 13
Н. Б. Чаплыгина; Яросл. гос. ун-т им. П. Г. Демидова. –
Ярославль : ЯрГУ, 2010. – 51 с.
Методические указания разработаны для ознакомления
со специальными средствами, помогающими правильно
организовать и ускорить процесс отладки, а также освоить режим отладки приложений в интегрированной среде
MS Visual C++.
Предназначены для студентов, обучающихся по специальностям 010101.65 Математика, 010501.65 Прикладная
математика и информатика (дисциплины «Информатика», «Практикум по ЭВМ», блок ЕН), очной формы обучения.
УДК 519.72
ББК З 973.2–018.2я73
 Ярославский
государственный университет
им. П. Г. Демидова, 2010
2
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Под отладкой понимается процесс выявления и исправления
ошибок в программе. Каждый программист, начинающий или
опытный, так или иначе делает ошибки. Практически даже самая
простая программа нуждается в исправлениях, прежде чем начнет выполняться так, как задумал ее создатель. Этап отладки
наиболее трудоемкий в жизненном цикле программы и требует
немалых интеллектуальных затрат, на него уходит не менее половины времени создания программы. Он заключается в устранении логических ошибок, обнаруженных во время тестирования
работающей программы, т. е. после устранения ошибок, не позволяющих создать исполняемый модуль. Во время отладки продолжается процесс изучения задачи и уточняется алгоритм ее
решения, и зачастую процесс отладки в корне меняет взгляд на
выбор алгоритма решения и тогда программа переписывается заново.
При создании программы для решения некоторой поставленной задачи программисту следует предварительно разработать
список тестовых примеров для обнаружения возможных ошибок
в работе программы. Таким образом, процесс тестирования и
процесс отладки программы тесно связаны, т. е. в процессе отладки непременно используется этап тестирования. После обнаружения ошибки ее устранение производится в два этапа: определяется возможная причина и локализуется место ошибки, а затем ошибка исправляется. И в этот момент можно использовать
другие специально созданные тестовые примеры, которые направлены не на проверку работы всей программы, а нацелены на
поиск данной ошибки.
Поиск обнаруженной ошибки, установление ее причин могут
быть осуществлены аналитическими методами, порой без использования программных средств, путем тщательного анализа
хода выполнения программы. Как вспомогательное средство в
этом случае может быть использовано направленное тестирование, которое может дать дополнительную информацию об ошибке. Аналитический метод наиболее предпочтителен, так как требует четкого представления о логике выполнения программы и
может выявить другие ошибки, которые себя пока не проявили.
Если же этот метод не дает результатов, то для локализации та3
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ких «трудных» ошибок прибегают к методу «грубой силы», инспектированию программы с помощью автоматических средств.
Зачастую программисты сразу прибегают к таким средствам, минуя аналитический этап, так как применение автоматических
средств не требует значительного внимания и такого умственного
напряжения, как использование аналитического метода. Однако
применение автоматических средств не освобождает совсем от
анализа конкретных ситуаций, возникающих в ходе выполнения
программы. Использование автоматических отладочных средств
без анализа всего хода исполнения алгоритма, взаимосвязей его
частей таит в себе опасность получения новой ошибки, может
быть, совсем иной природы, взамен только что исправленной.
Причины одних логических ошибок, обнаруженных в ходе
выполнения, для программистов, имеющих некоторый опыт, выясняются довольно быстро, для поиска других требуется существенная работа, и в этом может помочь среда программирования
Microsoft Visual C++, предоставляя удобные автоматические инструменты для поиска ошибок. К таковым автоматическим отладочным средствам относятся получение последовательности выполненных операторов программы или ее фрагмента, установление контрольных точек останова, предоставление возможности
инспектирования значений переменных или областей памяти в
процессе выполнения программы. Предлагаемые методические
указания знакомят с составом и архитектурой отладочных
средств в интегрированной среде разработки приложений
Microsoft Visual C++ Express Edition, а также рассмотрены шаги
построения программ: от исходного кода до исполняемого файла.
Вся терминология и обозначения могут быть в точности перенесены и на среду разработки MS Visual Studio .NET.
4
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
I. Этапы создания программ на С/C++
Каждая программа на языке C++ есть последовательность препроцессорных директив, описаний и определений глобальных
функций, переменных, пользовательских типов. Препроцессорные
директивы управляют преобразованием текста программы до ее
компиляции. Определения вводят функции и объекты. Объекты
необходимы для представления в программе обрабатываемых данных. Функции содержат потенциально возможные действия программы. Описания уведомляют компилятор о свойствах и именах
тех объектов и функций, которые определены в других частях программы (например, ниже по ее тексту или в другом файле).
Задача препроцессора – преобразование программы до её компиляции. Правила препроцессорной обработки устанавливает сам
программист с помощью директив #define и #include, которые
предписывают заменить некоторое имя, встречающееся в тексте
программы, на постановочную строку замены и вставить фрагмент
текста из указанного файла соответственно. Список заголовочных
файлов, которые содержат интерфейсы уже созданных объектов:
констант, макросов, функций, используемых для подключения директивой #include, есть в директории по умолчанию
C:\Program Files\Microsoft Visual Studio .NET 2003\Vc7\include.
При просмотре содержимого этой папки можно увидеть имена
заголовочных файлов: с расширением .h и без него, что позволит
правильно указать имя в директиве #include. В свою очередь,
каждый такой файл можно открыть и познакомиться с его содержимым, узнать, описания каких функций в нем содержатся.
Препроцессор сканирует исходный текст программы в поисках строк, начинающихся символом #. Такие строки воспринимаются препроцессором как команды, которые определяют действия по преобразованию текста программы.
После выполнения препроцессорной обработки в программе
не остается ни одной препроцессорной директивы. На этом этапе
программа представляет собой набор описаний и определений
имен и функций.
5
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Если создается исполняемый код, то среди этих функций
должна
присутствовать
функция
с
сигнатурой
<type> main(…)., которая называется главной функцией
программы. Точка входа в эту функцию определяет точку входа
всей программы.
В самом простом случае текст программы может представлять собой такую конструкцию:
Листинг 1
<препроцессорные директивы>
void main()
{
<инструкции тела программы>
}
После обработки препроцессором программа становится
полностью готовой к переводу на язык машинных команд, т. е. к
этапу компиляции. В результате компиляции создается объектный код программы. Получение объектного кода – это предфинальный этап создания готового приложения. На последнем этапе
обработки программы – компоновке (сборке) – к объектному
файлу подключаются стандартные библиотеки, и на выходе компоновщика получается полностью готовый к исполнению файл
(exe).
Условно схему построения программного модуля из исходного
кода можно представить так, как показано на рис. 1.
*.c
*.cpp
*.h
RTL
Препроцессор
Компилятор
Компоновщик
*.exe
Рис. 1. Построение выполняемого модуля
6
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
1. Создание консольного приложения
В данном параграфе будет небольшое отступление от рассмотрения отладочных средств. Оно посвящено созданию консольного приложения в среде Microsoft Visual C++ .
При открытии программной среды Microsoft Visual C++ появляется окно, в котором одна из вкладок Projects имеет кнопки
New Project – для создания нового проекта и
Open Project – для открытия ранее созданного.
«Кликаем» первую из них, вследствие чего появляется диалоговое окно (рис. 2).
Рис. 2. Окно создания нового проекта
В поле Name следует ввести имя проекта, например, pro, и в
правом окне Templates выбрать тип проекта Win32 Console
Project. При вводе имени проекта по умолчанию появляется такое же имя в поле New Solution Name в качестве имени решения, в который войдет наш проект. В поле Location указываем
путь к папке, в которой будут размещаться файлы проекта. После
7
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
нажатия клавиши OK появляется окно мастера создания приложения, представленное на рис. 3.
Рис. 3. Окно мастера создания приложения
На левой панели «кликаем» пункт Application Settings (установки приложения) и выбираем опции: Console application
(она выбрана уже по умолчанию, так как мы ранее и указывали
на создание консольного приложения) и Empty project (пустой
проект). После нажатия кнопки Finish среда сама создаёт заготовку консольного приложения и появляется окно собственно рабочей области проекта, в правой части отражена структура созданной папки проекта с пустыми пока папками для исходных, заголовочных файлов, файлов ресурсов и ссылок.
Наполнять созданный пустой проект предстоит нам самим.
Для добавления файла с исходным текстом программы, который
собираемся создать, выбираем пункт Project главного меню в
верхней части экрана и пункт ниспадающего меню
Add New Item (быстрые клавиши Ctrl+Schift+A),
8
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
в случае добавления файла, созданного ранее, с исходным
текстом программы – пункт того же ниспадающего меню
Add Existing Item (быстрые клавиши Schift+Alt+A).
Те же пункты можно выбрать и в ниспадающем меню File.
В открывшемся окне
Рис. 4. Добавление в проект файла с исходным текстом
в поле Name вводим имя, например, main, создаваемого исходного файла, выбрав на правой панели его тип: C++ File (.cpp) и
нажимаем кнопку Open. На правой панели в дереве структуры
проекта отражается имя добавленного файла в папке Sours Files
и открывается вкладка с пустым рабочим полем файла main.cpp.
Теперь в этом поле редактирования файла можно набрать исходный текст нашей программы.
Для выполнения программы нужно выбрать пункт Build
главного меню и попытаться откомпилировать и построить исполняемый файл программы с помощью пунктов Build (Rebuild)
ниспадающего меню. Пункт Compile позволит лишь откомпилировать исходный текст.
9
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
2. Ошибки компиляции и ошибки компоновки
При написании исходного текста программы может возникать масса ошибок, причем их природа порой бывает весьма
сложна и необычна. Большинство ошибок относятся к так называемым синтаксическим ошибкам, они возникают из-за неправильного или неточного использования конструкций языка. Многие из них возникают вследствие опечаток в именах переменных,
пропущенных разделителей или скобок в исходном тексте.
Практически любые синтаксические ошибки способен выявить синтаксический анализатор языка в среде программирования, и при попытке запуска приложения с синтаксически неверным исходным текстом в окне протоколирования компиляции
будут выведены соответствующие ошибки с их кратким описанием.
Например, попытка построения исполняемого файла по следующему исходному файлу:
Листинг 2
#include<stdio.h>
void main (void)
{ int k,max;
char a;
k=0;
max=0;
scanf("%c",&a);
while(!feof(stdin)
{ if ((a<='9')&&(a>='0'))
{ k++;
if (k>max) max=k;
}
else k=0;
scanf("%c",&a);
}
printf("%d",max);
return;
}
приведет к неудаче. Построение исполняемого файла будет прервано уже на этапе компиляции исходного текста с выводом со10
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
общения, начинающегося словом «error», компилятора о синтаксической ошибке в программе:
error C2143: syntax error : missing ')' before '{'
с указанием номера 9 строки, при анализе которой была зафиксирована синтаксическая ошибка. Идентификаторы сообщений
компилятора начинаются с символа 'C'. Если «кликнуть» на сообщении, то строка, в которой была замечена ошибка, будет помечена соответствующим маркером. В данном случае это строка
с кодом
{ if ((a<='9')&&(a>='0')).
В сообщении говорится, что ожидается круглая скобка '('
раньше, чем встретилась скобка '{'. Поскольку скобка фигурная
является первым символов в указанной строке, то ожидаемая
круглая скобка должна закрывать некоторую конструкцию предыдущей строки. Замечаем, что в предыдущей строке
while(!feof(stdin)
действительно не хватает закрывающей скобки, что не позволило
компилятору разобрать программу до конца. Но такого рода
ошибку можно было заметить на этапе создания исходного текста, если внимательно следить за шрифтом вводимых скобок.
Выводом сообщений компилятор реагирует не только на
ошибки, но и на подозрительные места исходного текста, выдавая
свои подозрения в виде предупреждений. Например, фрагмент
исходного кода:
int k; float max;
max=k;
вызовет предупреждение
warning C4244: '=' : conversion
'float', possible loss of data,
from
'int'
to
в котором сообщается, что в момент присваивания произойдет
преобразование значения типа int в тип float, при котором возможна потеря данных.
Предупреждения начинаются словом «warning», они не прерывают процесса обработки исходного кода, но на них следует
обратить внимание, поскольку следствием ситуации, о которой
предупреждает компилятор, может стать логическая ошибка на
этапе выполнения.
11
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Ошибки, возникающие на следующем этапе, этапе компоновки, искать несколько труднее, но сигнал о такой ошибке также будет получен в виде сообщения компоновщика. Приведем
пример такой ошибки, обнаруженной на этапе сборки откомпилированной программы с простейшим исходным кодом, состоящим из двух файлов:
Листинг 3
файл sub.cpp
файл main.cpp
void subpr(int
&k)
{ k++;
}
#include<stdio.h>
#include <conio.h>
void subpr(int);
int (void)
{ int t;
scanf("%d",&t);
subpr(t);
printf("%d",t);
getch();
return 0;
}
Попытка построения исполняемого кода заканчивается выводом двух сообщений об ошибках:
 emp error LNK2019: unresolved external symbol
"void __cdecl subpr(int)" (?subpr@@YAXH@Z) referenced
in function _main
 emp fatal error LNK1120: 1 unresolved externals .
Оба сообщения получены не на этапе компиляции, а на этапе
компоновки программы, об этом свидетельствует их идентификация, начинающаяся со слова LNK. Анализируя сообщения, понимаем, что не найдено определение функции subpr(), описание которой приводится перед функцией main() в таком виде
void subpr(int).
Функция subpr() определена в другом файле, совпадает имя
функции, однако различаются типы параметров: int – при вызове, int& – при определении. Следовательно, для исправления
ошибки нужно привести в соответствие сигнатуры функций при
12
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
вызове и при определении. Таким образом, сообщения помогли
раскрыть причины ошибок и исправить их.
Сама интегрированная среда MS Visual C++ на этапе создания и редактирования программы старается помочь программисту избежать синтаксических ошибок, изменяя в процессе ввода
исходного текста цвет и шрифт символов в зависимости от смысла, позволяя визуально различить директивы препроцессора,
ключевые слова, комментарии, следит за соответствием открывающих и закрывающих скобок. Отличие цвета или шрифта вводимого текста от ожидаемого может быть сигналом о синтаксической ошибке.
Гораздо сложнее справиться с другими типами ошибок – логическими, возникающими при работе программы, или полусинтаксическими, сообщений о которых мы не получим, а обнаружим их по неверному выполнению программы. Именно выявление таких ошибок, о которых не будет сообщений на этапе
создания приложения, и является целью процесса отладки. Но к
нему можно подойти лишь в том случае, когда в программе будут
исправлены все ошибки этапов компиляции и компоновки.
Рассмотрим их более подробно. В качестве примера полусинтаксической ошибки можно привести неправильное использование (а точнее, неиспользование) операторных скобок, например,
в теле цикла, в обязанности которого заложено выполнять не одну, а сразу несколько инструкций. Сравним следующие фрагменты:
Листинг 4
for(i=0; i<n; i++)
A[i]=C[i];
B[i]=D[i];
и
Листинг 5
for(i=0; i<n; i++)
{
A[i]=C[i];
13
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
B[i]=D[i];
}
В первом случае в цикле выполняется лишь одна инструкция, следующая сразу после заголовка, а во втором – все инструкции, которые заключены в операторные скобки цикла, объединяющие эти выполняемые инструкции в один составной оператор или блок. Чаще всего такие ошибки возникают не по
причине «незнания», а по причине забывчивости программиста.
Например, в начале в цикле выполнялась одна инструкция, а
после пересмотра алгоритма программистом были дописаны
дополнительные строки кода, которые он забыл заключить в
операторные скобки.
Логические ошибки – это самая большая опасность, кроющаяся в исходном тексте программ. Такие ошибки не только не
видны сразу "невооруженным" глазом, в отличие от синтаксических, но и трудно различимы даже при внимательном изучении текста программы. По трудоемкости этап отладки не уступает этапу разработки и создания программы.
При исправлении любых ошибок программист изменяет исходный код программы, используя окно редактирования текста.
Во время работы с исходным текстом можно воспользоваться
удобными средствами для анализа кода программы. Одно из таковых средств при работе с большими программными текстами
– установка закладок на тех фрагментах программы, которые
будут активно анализироваться в процессе создания приложения. Установить закладку можно с помощью пункта
Edit/Bookmarks/Toogle Bookmark ниспадающего меню Edit
(быстрые клавиши Ctrl+K, Ctrl+K). При этом строка с закладкой маркируется голубым квадратиком в левой колонке. Снимается закладка тем же самым пунктом, который работает как
переключатель. Имеется возможность снять все установленные
закладки в тексте с помощью Edit/Bookmarks/Clear Bookmarks (быстрые клавиши Ctrl+K, Ctrl+L).
14
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 5. Меню работы с закладками
В тексте с установленными закладками легче ориентироваться и находить нужные фрагменты, переходя от закладки к закладке с помощью пунктов Next Bookmark и Previous Bookmark. Но
следует заметить, что работать с закладками можно в пределах
одного файла. Если же программа состоит из нескольких файлов
и закладки нужно расположить в разных файлах, то их роль могут выполнять контрольные точки, по которым можно следовать
с помощью окна менеджера контрольных точек (рис. 11). О работе с контрольными точками будет сказано в п. 5.
15
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
II. Отладочный интерфейс
Microsoft Visual C++
Для помощи в нахождении и исправлении скрытых ошибок и
проверки работоспособности приложения в целом существует
достаточно мощный аппарат автоматизированных средств отладки, использование которых позволяет ускорить этот трудоемкий
процесс.
Для выполнения программы необходимо построить исполняемый файл. Среда Microsoft Visual C++ предоставляет два варианта создания такого файла: с включением отладочных возможностей и без них. Для выполнения программы без использования отладочных средств нужно выбрать из ниспадающего
меню пункта Debug строку Srart Without Debuging (клавиши
Ctri+F5). Выбор строки Start (F5) приведет к выполнению программы с запуском механизма отладочных средств.
На рисунке 6 представлен фрагмент пользовательского меню
Debug в момент активного режима Debug, т. е. после запуска
программы для выполнения по второму варианту.
Таким образом, неявно для программиста в момент запуска
программы (клавиша F5) запускается безостановочный отладочный режим (безостановочный при условии отсутствия точек останова). Для такого режима характерно видоизменение главного
окна среды программирования в момент запуска отладки: появление отладочного курсора слева от листинга программы и появление в нижней части главного окна фреймов быстрого просмотра содержимого памяти, системного стека и т. п. (в зависимости
от настроек).
16
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 6. Меню Debug
Дадим описание некоторых основных возможностей режима
отладки и альтернативные им клавишные сочетания.
Пункт
меню
Альтернативное
сочетание
клавиш
Действие
Start
Debugging
F5
Запуск приложения в режиме неявной отладки на исполнение.
Stop
Debugging
Shift+F5
Выгрузка отлаживаемого приложения из
RAM, чистка мусора в памяти, переход в
режим редактирования кода.
Restart
Debugging
Ctrl+Shift+F5
Завершение текущей отладочной сессии и
начало новой.
Step Over
F10
Выполнение очередной команды под отладочным курсором, если это вызов сторонней
функции, то выполняется ее тело без захода
в него. Таблицы трассировки обновляют
свое содержимое.
17
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Step In
F11
Выполняется очередная команда под курсором, если это вызов сторонней функции, то
происходит заход в её тело. Таблицы трассировки обновляют свое содержимое в соответствии с локальными переменными.
Step Out
Shift+F11
Выполнение подпрограммы с текущей позиции до завершения без остановки и возврат
на точку вызова надпрограммы.
Quick
Watch
Ctrl+Alt+Q
Запуск окна просмотрщика для просмотра
значений интересующих переменных.
Toggle
Breakpoint
F9
Установка/снятие точки останова в заданной
курсором строке.
Для облегчения выполнения отладки можно также вызвать
панель инструментов «Debug» из меню View > Toolbars.
Во второй строке таблицы указана возможность (Shift+F5)
выхода из процесса выполнения программы. Ею можно воспользоваться для завершения выполнения программы во время пошагового выполнения или при выполнении программы с точками
останова, т. е. в тот момент, когда выполнен очередной оператор
и программа находится в состоянии приостановки, которую организовал отладочный режим. Однако нельзя с помощью активизации этого пункта меню прервать выполнение программы во время выполнения какого-либо оператора, например, при ожидании
ввода данных с клавиатуры или во время работы оператора цикла. Чтобы прервать работу во время выполнения оператора, нужно воспользоваться сочетанием клавиш Ctrl+Break или Ctrl+C в
случае ввода данных. Тогда можно закончить выполнение программы, выйдя из отладочного режима или остаться в нем для
анализа ситуации.
В строке заголовка главного окна среды программирования
отображается идентификатор открытого проекта. По умолчанию
решение имеет то же имя, что и проект, из которого оно состоит.
Кроме идентификатора проекта в квадратных скобках можно
увидеть текущее состояние, в котором находится процесс работы
над этим проектом: [design] – состояние проектирования: программа не в режиме выполнения, можно работать с исходным
18
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
текстом или просматривать отладочную информацию; [break] –
состояние отладки: в данный момент активен отладочный режим
выполнения, [run] – состояние выполнения: программа находится
в процессе выполнения оператора.
3. Пошаговая отладка
Этот тип контролируемого выполнения приложения является,
пожалуй, самым простым. Суть его заключается в том, что каждая
инструкция программы выполняется последовательно, и после каждого шага происходит приостановка выполнения. Во время приостановки можно просмотреть содержимое интересующих нас объектов. Но и сама последовательность выполняемых инструкций
программы, ее след выполнения, предоставляет порой достаточно
информации для нахождения ошибки. В режиме пошаговой отладки после каждого шага происходит обновление данных трассировочных таблиц.
Для примера рассмотрим программу, листинг которой представлен ниже
Листинг 6
#include <stdio.h>
#define N 5
void main()
{
int arr[N];
for(int i=0; i<N; i++)
scanf("%d", &arr[i]);
for(int i=0; i<N; i++)
for(int j=0; j<i; j++)
{
if(arr[i]<arr[j])
{
int t=arr[i];
arr[i]=arr[j];
arr[j]=t;
}
}
for(int i=0; i<N; i++)
printf("%d ", arr[i]);
}
19
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Представленный здесь алгоритм решает задачу сортировки
по возрастанию введенной последовательности целых чисел. Теперь попробуем проследить при помощи пошаговой отладки, что
же происходит в процессе исполнения алгоритма.
Но для начала вспомним, как осуществляется пошаговая отладка. Войти в режим пошагового исполнения можно непосредственно при помощи функциональной клавиши F10 (без захода в
подпрограммы). После первичного нажатия на эту клавишу среда
программирования найдет с тексте программы первую активную
(выполняющую действия) инструкцию, и напротив нее будет установлен отладочный курсор (рис. 8), указывающий на команду,
которая может быть выполнена на следующем шаге при помощи
клавиши F10, или же, если это оператор обращения к сторонней
функции, для пошагового выполнения ее тела, т. е. для захода в
нее, при помощи клавиши F11.
С момента начала отладки окно среды программирования
изменило свой вид – теперь в нижней его части (по умолчанию)
находится одно из трассировочных окон – это таблица локальных
переменных. Её внешний вид представлен на рис. 7.
Рис. 7. Таблица локальных переменных
Далее, нажимая F10, можно пройти по всему тексту программы, следя за содержимым отладочного окна Locals. Результаты отладки можно оформить в трассировочную таблицу, представленную ниже (таблица содержит только фрагмент трассировки сортировочного алгоритма).
20
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Шаг
i
arr[0] arr[1] arr[2] arr[3] arr[4]
1
0
0
5
4
3
2
1
2
1
0
4
5
3
2
1
3
1
1
4
5
3
2
1
4
2
0
3
5
4
2
1
5
2
1
3
4
5
2
1
6
2
2
3
4
5
2
1
7
3
0
2
4
5
3
1
8
3
1
2
3
5
4
1
9
3
2
2
3
4
5
1
10
3
3
2
3
4
5
1
11
4
0
1
3
4
5
2
12
4
1
1
2
4
5
3
13
4
2
1
2
3
5
4
14
4
3
1
2
3
4
5
15
4
4
1
2
3
4
5
При пошаговой отладке каждая следующая команда для выполнения помечается специальным маркером в виде желтой
стрелки в боковой левой колонке (слева от листинга) как показано на рис. 8.
Рис. 8. Окно программы при пошаговом выполнении
21
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
4. Пошаговая отладка с заходом
в подпрограмму
Большинство более или менее серьезных проектов, как правило, состоят не из одного модуля с главной функцией, а из
множества модулей с дополнительными функциями, описанными программистом.
В этом параграфе мы попытаемся разобрать один пример,
демонстрирующий понимание сути ошибки, возникающей из-за
специфики организации аппаратно-программного взаимодействия.
Рассмотрим следующий листинг, в котором описана главная функция main() и вспомогательная функция fact().
Листинг 7
#include <stdio.h>
long fact(int i)
{
if(!i) return 1;
else return i*fact(i-1);
}
void main()
{
int input;
long res;
scanf("%d",&input);
res=fact(input); //пусть ввод: «-5»
printf("%d",res);
}
Функция fact() вычисления факториала в рассмотренном
примере является рекурсивной, а точнее, пряморекурсивной, т. е.
обращается сама к себе.
Попытка выполнения этой программы с вводом числа –5 в
качестве требуемого значения для переменной input приведет к
ошибке, идентификатор которой будет называться «Stack
22
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Overflow», что означает переполнение стека. Попытаемся разобраться, в чем причина этого последствия.
При рекурсивном вызове функции самой собой, как и при
вызове любой функции, используется программный стек, который хранит кадр с данными о вызове функции и очищает его при
возврате из этой функции.
Стек – это структура данных, построенная на принципе LIFO
(Last In First Out – «последним пришел, первым вышел»). Сравнить его можно с трубой, имеющей один открытый конец, в которую кладутся мячи. Понятно, что первым достать можно лишь
тот мяч, который был опущен в нее последним.
Не вдаваясь в подробности, можно сказать, что при вызове
функции в программный стек кладется адрес возврата в вызвавшую подпрограмму, а также параметры, передаваемые в функцию. При вызове пряморекурсивной функции программный стек
копит в себе все точки промежуточных вызовов, собирая их в
большую «стопку», а затем, когда рекурсия дойдет до своей базы
(«дна»), то начинается обратное извлечение и свертка всего содержимого этой «стопки» к виду, как и при первичном вызове
функции.
Теперь попытаемся определить, что происходит при вызове
рекурсивной функции поиска факториала с отрицательным аргументом (как и в рассмотренном выше листинге). Построим условно программный стек для вызовов рекурсии (рис. 9).
–5
вызов
–6
вызов
–5
вызов
–7
вызов
–6
вызов
–5
вызов
...
Рис. 9. Рост стека при рекурсивных вызовах
23
–32768
вызов
...
–7
вызов
–6
вызов
–5
вызов
...
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Заполнение стека будет происходить до тех пор, пока при
уменьшении передаваемого аргумента не будет достигнуто значение 0, которое будет получено в момент переполнения при
очередном уменьшении параметра функции fact(). Значение 0
является базовым для этой рекурсивной функции, и после того,
как оно «неожиданно» будет получено, должна начаться последовательность выходов из вложенных вызовов функции и соответственно постепенное очищение стека, если памяти для стека
было достаточно. В противном случае произойдет ситуация недостатка памяти, что приведет к аварийному завершению программы. Очевидно, что в приведенном примере количество вызовов будет иметь значение порядка 64000 (изначально в качестве i
было передано значение –5, которое при рекурсивных вызовах
функции fact() уменьшается до нижней границы целых знаковых чисел «–32768», которое, в свою очередь, уменьшаясь на
единицу, дает максимальное положительное знаковое «32767» и
при дальнейших вызовах функции уменьшается до «0», базы рекурсии). Понятно, что даже при размере передаваемых при каждом вызове функции данных в 4 байта стек должен заполниться
объемом информации в 256000 байт (размер программного стека
для 16-разрядной шины, как правило, в четыре раза меньше).
Теперь попытаемся проследить за ходом представленного алгоритма в среде программирования с использованием режима отладки с заходом в подпрограммы.
До оператора вызова функции fact() достаточно проделать
отладку, используя пошаговый режим (F10). Когда отладочный
курсор установится напротив строки с вызовом функции, необходимо нажать клавишу F11 – тогда произойдет переход отладочного курсора на текст подпрограммы, по которому можно
продолжать шагать, используя вновь клавишу F10. В окне локальных переменных можно следить за значениями передаваемых в функцию аргументов.
Определив причину ошибки выполнения: нет защиты от неправильных (отрицательных) исходных данных, попытаемся исправить описанную выше функцию вычисления факториала так,
чтобы она «не воспринимала» отрицательных значений аргумента.
24
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Листинг 8
long fact(int i)
{
if(i<0) return 0;
if(!i) return 1;
else return i*fact(i-1);
}
Поскольку факториал никакого целого неотрицательного
числа не может обратиться в 0, то при возвращении функцией
нулевого значения сразу будет ясен факт передачи отрицательного аргумента.
Следует отметить еще одну полезную возможность рассматриваемого типа отладки приложения – если был произведен заход в подпрограмму, а ее тело слишком велико и все неточности
были выявлены и исправлены еще в первых ее строках, то можно
быстро «перепрыгнуть» в вызвавшую ее программу, выполнив
без остановки все оставшиеся действия подпрограммы, используя
сочетание клавиш Shift+F11.
Пошаговая отладка является довольно эффективным методом
при анализе небольших и не слишком структурированных проектов, однако она не слишком удобна при просмотре существенного числа вложенных циклов с большим количеством итераций. В
таком случае следует применять более оптимальные средства отладки – это использование контрольных точек, или точек останова.
5. Использование контрольных точек останова.
Их классификация
Точки останова (Breakpoints) – это еще один мощный отладочный инструмент. Суть их использования заключается в том,
что отладочное выполнение приложения останавливается в заданном месте программы безусловно либо при достижении определенного условия.
Рассмотрим пример. Пусть у нас есть приложение, начальная
часть которого довольно долго (например, в цикле) выполняет
ввод некоторых данных. Пусть заведомо известно, что ввод дан25
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ных верен и потому нет необходимости проверять эту часть приложения. В этом случае достаточно установить точку останова
после ввода данных, сразу перед началом вычислительной части
программы. При отладочном запуске (F5) приложение будет выполняться как обычно (без остановок) до достижения точки останова, при этом на красный маркер точки останова накладывается
изображение желтой стрелки, показывающей активную строку
программы. После этого выполнение программы приостанавливается. У разработчика появляется выбор для дальнейшего выполнения программы: можно выполнять далее пошаговую отладку (F10 или F11) или, получив и проанализировав информацию в
этой точке останова, можно выполнить с помощью F5 фрагмент
программы до следующей точки останова или до конца программы, если точек останова дальше не встретится.
Для установки или снятия точки останова используется
функциональная клавиша F9, которая работает в режиме переключателя: устанавливает либо снимает уже установленную точку напротив той строки программы, в которой находился курсор
(см. рисунок ниже). Строка исходного текста, в которой установлена точка останова, автоматически помечается маркером в виде
кружочка красного цвета. Для организации точки останова можно также кликнуть левой кнопкой мыши на том месте, где желаем
увидеть маркер точки останова, т. е. слева от желаемой строки.
Причем устанавливать точки останова можно не только до выполнения программы, но и во время выполнения ее в отладочном
режиме. Этой возможностью можно воспользоваться, чтобы
снять точки останова, если в процессе отладки уже получена исчерпывающая информация для определения причин ошибки выполнения программы. Точки останова можно устанавливать как в
главной функции программы, так и в любых вспомогательных,
причем определенных в одном исходном файле с главной функцией или в другом. Следует заметить, что точка останова приведет к приостановке в выполнении программы лишь в том случае,
когда логический ход выполнения программы встретит на своем
пути оператор, которому соответствует эта точка останова, в противном случае она не окажет никакого влияния на ход выполнения программы.
26
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 10. Установка точек Breakpoints
При активном отладочном режиме наряду с окном локальных
переменных Locals можно просмотреть также и окно Breakpoints менеджера точек останова (оно находится по умолчанию
справа внизу главной формы среды программирования).
Рис. 11. Окно менеджера точек останова
Теперь рассмотрим классификацию точек останова, они могут быть безусловными, условными и точками-счетчиками.
 Безусловные точки останова
Пример создания безусловной точки останова был только что
рассмотрен выше. Безусловная точка останова срабатывает на
27
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
прерывание приложения всегда при ее достижении (отсюда и
пошло ее название).
 Условные точки останова (Conditional Breakpoints)
Условная точка останова – это точка останова, прерывающая
выполнение программы лишь при достижении некоторых условий. Ее создают как обычную, на которую наложено определенное условие-маска для управления работой этой точки. Для того
чтобы создать условную точку останова, необходимо сначала
создать обычную, а затем при помощи контекстного меню этой
точки (щелчок ПКМ на ней) выбрать пункт задания условия (см.
рис. 12).
Рис. 12. Контекстное меню точки останова
При вызове этой команды откроется окно задания условия
(рис. 13).
28
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 13. Окно задания условия для условной точки останова
В строке условия достаточно ввести некоторое логическое
высказывание, записанное в терминах языка C++, например,
arr[3]= =1.
Точка останова при заданном условии будет срабатывать тогда и только тогда, когда это условие будет истинным (если в
указанном окне был установлен переключатель «Is True» в окне
под строкой условия), либо указанное выражение изменилось с
момента предыдущего прохода по этой точке останова (если установлен переключатель «Has Changed»).
 Точки-счетчики (Hit Counts)
При просмотре больших циклов бывает полезно увеличить
шаг просмотра и производить остановку не на каждой итерации
цикла, а через определенное число итераций. Это позволит достаточно быстро локализовать значения переменной цикла, при которых происходит сбой, а затем вернуться к этим значениям по
условию. Для этого используется команда Hit Count (Счетчик
повторов), расположенная в том же контекстном меню точки останова, при активации которой на экран выводится диалоговое
окно Breakpoint Hit Count (счетчик повторов точки останова),
изображенное на рисунке ниже.
29
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 14. Окно Breakpoint Hit Count (счетчик повторов точки останова)
Сама по себе точка останова со счетчиком не инициализирует остановки выполнения, однако с ее помощью можно остановить программу, когда количество прохождений через эту точку
будет удовлетворять одному из трех условий:
 значение повторов прохождения по точке останова в точности равно заданному числу (пункт «Break when the hit count is
equal to...» выпадающего меню);
 значение повторов прохождения по точке останова кратно
заданному числу (пункт «Break when the hit count is multiply
to...»);
 значение повторов прохождения по точке останова больше
или равно заданному числу (пункт «Break when the hit count is
greater then or equal to...») .
Есть еще и последний вариант выбора функциональности
точки-счетчика – это «Break always». В этом случае точка останова срабатывает всегда (безусловно).
Точки останова со счетчиком удобно использовать для подсчета итераций циклов с неявным заданием количества повторов,
например, циклов с заголовками
while(){}
или
do{}while().
В заключение разговора о точках останова следует обратить
внимание на удобную возможность снятия сразу всех расставленных в программе точек останова с помощью пункта Clear All
Breakpoints меню Debug или сочетанием клавиш Ctrl+Shift+F9.
30
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
6. Окно быстрого просмотра переменных
(Quick Watch)
Окно Quick Watch вызывается при помощи сочетания клавиш Ctrl+Alt+Q во время выполнения отладки или с помощью
выбора одноименного пункта меню Debug. Его вид представлен
на рис. 16.
Рис. 15. Окно быстрого просмотра переменных
Данная возможность отладки используется для того, чтобы
все интересующие программиста переменные были в поле зрения для исследования их текущих значений. Окно служит менеджером добавки переменных в дополнительное окно просмотра, расположенное там же, где и окно локальных переменных, но на другой вкладке, вкладке «Watch N». Чтобы добавить
переменную в указанное окно просмотра, достаточно в окне
менеджера добавки переменных указать её имя и нажать кнопку
«Add Watch» (см. рис. 15).
Работа с переменными в этом окне Watch N может быть
организована точно так же, как и в окне Locals локальных переменных: можно наблюдать за изменением их значений и, если нужно, вручную производить изменения значений, вводя новое значение в колонке Value в ходе выполнения программы.
Это окно будет оставаться активным на экране, и в него будут
выводиться значения переменных, выбранных для наблюдения,
при отладочном режиме выполнения программы.
31
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 16. Значения переменных в окне быстрого просмотра
III. Обработка исключительных ситуаций
Часто поведение программы при исполнении зависит от
действий пользователя. В таких случаях программист должен
предвидеть ошибки, которые может возникнуть на каком-либо
этапе исполнения в результате диалога с пользователем. В качестве примера рассмотрим алгоритм деления двух целых чисел
с их вводом:
Листинг 9
#include <stdio.h>
void main()
{
int a,b;
scanf("%d %d",&a,&b);
float r=(float)a/(float)b;
printf("%f",r);
}
Однако задумаемся над тем, что произойдет, если при вводе
будет получена пара чисел, второе из которых равно нулю. Ответ на этот вопрос неоднозначен – здесь все будет зависеть от
архитектуры машины, исполняющей приложение (ошибка де32
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
ления на нуль). Чтобы обойти такую ситуацию, можно использовать механизм обработки исключительных ситуаций, синтаксис которого представлен ниже.
Листинг 10
try
{
//...//code
throw [expression1]
throw [expression2]
//...//code
}
catch (exception1-declaration)
{
//exception1 proc.
}
catch (exception2-declaration)
{
//exception2 proc.
}
Внутри блока try помещается текст программы, в которой
может возникнуть внештатная (исключительная) ситуация, т. е.
ключевое слово try определяет контролируемый блок. Внутри
этого блока должны присутствовать операторы throw, которые
«выкидывают» (инициируют) исключение при достижении такой ситуации и передают управление обработчику исключения.
После блока try следуют сами обработчики catch для таких
брошенных исключений. Выбор необходимого обработчика
производится по соответствию типов передаваемого оператором throw объекта и параметра обработчика исключения
catch. Таким образом, нельзя определять несколько обработчиков с одинаковыми типами параметров. Механизм создания
обработчиков схож с перегрузкой одноименных функций.
В качестве примера рассмотрим версию представленной
программы с использованием исключений:
33
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Листинг 11
#include <stdio.h>
void main()
{
int a,b;
scanf("%d %d",&a,&b);
try
{
if(b==0) throw "DivideByZero!";
float r=(float)a/(float)b;
printf("%f",r);
}
catch(char* str)
{
printf("ERROR: %s",str);
}
}
Механизм действия здесь таков: при выполнении условия
равенства нулю делителя поднимается исключение, кидающее в
обработчик указатель на строку «DivideByZero!». В данном примере использован один тип исключений. Строка
«DivideByZero!» является фактическим аргументом для обработчика исключений с таким типом параметра. Как только
исключение поднято, вызывается обработчик catch() с указанной строкой в качестве параметра, который обрабатывает
это исключение, выдавая на печать сообщение «ERROR:» и
полученную в качестве параметра строку. Если условие на равенство нулю делителя выполнено не будет, то соответственно
не будет поднято исключение, и не будет выполняться его обработка.
34
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
IV. Отладочные макросы
При программировании приложения в него целесообразно
включить определенные операторы, используемые только в процессе его отладки. Включение данных операторов замедляет выполнение программы и чревато выдачей пользователю сообщений, понятных только программисту. Поэтому при создании
окончательной версии приложения эти операторы следует удалить. Однако для качественной отладки приложения количество
отладочных операторов может быть велико, и они могут находиться в большинстве функций приложения. Удаление этих операторов вручную представляет собой достаточно сложную задачу
и не дает гарантию того, что все отладочные операторы будут
удалены, и что вместе с ними не будут удалены и рабочие операторы. Кроме того, никто не может поручиться за то, что окончательную версию не придется отлаживать заново, например, при
внесении в нее изменений «по просьбам трудящихся». Поэтому в
Visual C++ предусмотрено два режима компиляции приложения:
Debug (Отладочный) и Release (Окончательная сборка) и созданы специальные функции и макросы, работающие только в отладочном режиме и игнорируемые в окончательном приложении.
Для
выбора
режима
запуска
приложения
(Отладочный/Окончательная сборка) служит выпадающий список на
Стандартной (Standart) панели инструментов, который имеет
вид, представленный на рисунке ниже.
Рис. 17. Выбор режима сборки приложения
Самыми известными отладочными макросами являются макросы _ASSERT( ) и _TRACE( ), использование которых возможно при подключении библиотеки crtdbg.h.
35
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
7. Отладочный макрос _ASSERT
Макрос _ASSERT имеет один аргумент целого типа, который интерпретируется как логическое выражение и его значение
определяется как FALSE, если параметр имеет нулевое значение,
и как TRUE в противном случае. Всякий раз, когда логическое
выражение, переданное в качестве аргумента макросу _ASSERT,
будет принимать значение FALSE (0), выполнение приложения
будет останавливаться, и на экран будет выводиться окно сообщения, аналогичное приведенному на рисунке ниже.
Рис. 18. Окно сообщения, выводимое с помощью макроса _ASSERT
В данном окне указывается имя исполняемого файла, имя исходного файла и номер строки, в которой произошла ошибка.
Нажатие кнопки Retry (Повтор) позволяет перейти в текст программы для ее отладки. Причем текущая точка останова устанавливается на строку соответствующего макроса _ASSERT. Для
использования данного средства нужно включить в программу
директиву
#include <crtdbg.h>
для подключения соответствующей библиотеки.
Пусть имеется описание массива
const int N=10;
int a[N];
36
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
При любом обращении к элементам массива можно установить проверку индекса:
_ASSERT(k>=0 && k<N);
a[k]++;
Если условие, переданное _ASSERT в качестве аргумента,
не выполнится, произойдет прерывание программы с выводом
на экран сообщения (рис. 18).
В окончательной версии приложения (Release), в которой
константа _DEBUG не определена (не подключена библиотека
crtdbg.h), которая по сути отвечает за разрешение/запрещение выполнения приложения в режиме с отладки
(эмуляция режима выполнения с остановками), макрос
_ASSERT не выполняет никаких действий. Это позволяет оставлять его в тексте программы, не опасаясь, что он замедлит
выполнение окончательной версии приложения или будет выдавать конечному пользователю непонятные сообщения. При
компиляции приложения в окончательном варианте (как
Release) макрос _ASSERT принимает пустое тело, т.е. сильного вклада в увеличение размера исполняемого файла (либо места под программу в RAM) не вносит.
Упрощенно определение макроса _ASSERT можно представить такими строками:
Листинг 12
#ifdef _DEBUG
#define ASSERT(x) if( (x) == 0)
report_assert_failure()
#else
#define ASSERT(x)
#endif
На самом деле определение это намного сложнее, однако в
данном контексте это не играет особой роли.
37
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Более подробные сведения по использованию макросов отладки можно получить в динамической справке среды программирования или же в базе знаний MSDN (www.msdn.com).
8. Отладочный макрос _RPTn
Этот отладочный макрос может выполнять достаточно широкий круг функций по отслеживанию процесса отладки приложений. Для использования макроса необходимым является подключение заголовочного файла библиотеки crtdbg.h. Всего существует шесть модификаций указанного макроса, которые
отличаются лишь числом предусмотренных в них аргументов,
указанным в конце названия каждого: _RPT0, _RPT1, ...,
_RPT5.
Использование макроса напоминает использование функции
стандартного консольного вывода printf() из библиотеки
stdio.h, однако указанный макрос отличается от нее тем, что
количество параметров строки ограничено пятью, и вывод может
осуществляться в трех различных режимах (в три различных диагностических потока):
1. В окно протоколирования компиляции и отладки «Output»
(см. рис. 19), которое автоматически становится активных в момент запуска отладочного режима. Также это окно можно вызвать из меню View – Output после запуска режима отладки.
Рис. 19. Область протоколирования компиляции и отладки
(внутренняя консоль)
2. В диалоговое окно с результатом «Ошибка» (см. рис. 20)
38
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рис. 20. Сообщение о возникновении ошибки
3. В диалоговое окно с сообщением, как и при использовании
макроса _ASSERT (см. рис. 21).
Рис. 21. Вывод отладочного сообщения в диалоговое окно
На примере макроса _RPT0 (он не принимает ни одного параметра для выводимой строки) укажем способы направления сообщений.
Пример использования
_RPT0(_CRT_WARN, “message”)
39
Результат
Вывод сообщения “message” в
окно протоколирования (чаще
применяется для вывода отладочных
конструкцийпредупреждений о нефатальных
ситуациях)
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
_RPT0(_CRT_ERROR, “message”) Вывод сообщения “message” в
диалоговое окно ошибки с предложенийм прервать отладку, либо
продолжить ее.
_RPT0(_CRT_ASSERT,”message”) Вывод осуществляется в полном
соответствии, как и при использовании макроса _ASSERT
Помимо трех режимов использования, макросы _RPT1,
..., _RPT5 используют соответствующее число параметров
для вставки в строку подобно функции printf().
Рассмотрим пример в листинге ниже
Листинг 13
...
double x = 3.14159265358 ,y =
2.718281828459045;
_RPT2(_CRT_WARN,”Now X equal to %f, Y equal to
%f!”,x,y);
...
Выбор макроса _RPT2 обусловлен необходимостью использовать в выводе два аргумента (x и y).
Результат выполнения данного листинга будет выведен в окно протоколирования (см. рис. 22).
Рис. 22. Пример вывода отладочного сообщения
с двумя параметрами в окно протоколирования
40
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
9. Создание макроса отладочной печати
Работа отладочных макросов, как правило, регулируется определением некоторой константы, определяемой директивой
#define специально для этой цели. В качестве примера создадим макрос DebugPrint(arg) отладочной печати.
Листинг 14
#include<iostream>
#ifdef DBG_PRN
#define DebugPrint(arg)
cout<<#arg<<"="<<arg<<endl
#else
#define DebugPrint(arg)
#endif
Обращение к макросу DebugPrint(arg) позволит увидеть на экране имя аргумента и его значение, если при компиляции определить константу DBG_PRN.
Поместим
фрагмент
с
определением
макроса
DebugPrint(arg)
в заголовочный файл, например,
dbgprn.h. И, если в программе потребуется отладочная печать, подключим созданную библиотеку dbgprn.h, предварительно определив макрос-константу DBG_PRN:
Листинг 15
#include<stdio.h>
#include<conio.h>
#define DBG_PRN
#include"dbgprn.h"
using namespace std;
void main (void)
{ .....................
char a;
int k;
41
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
......................
a='t';
DebugPrint(a);
......................
k=0;
DebugPrint(k);
......................
getch();
return;
}
При выполнении программы, представленной на листинге 15,
конструкция DebugPrint(a) будет заменена на оператор
cout<<"a"<<"="<<a<<endl,
который выведет на экран сообщение a=t. Аналогично отработает и второй вариант обращения к макросу DebugPrint(k),
на экране увидим сообщение k=0. После окончания отладки
программы следует заменить определение константы DBG_PRN
на отмену определения:
#undef DBG_PRN.
В таком случае все обращения к макросу отладочной печати
при повторной компиляции заменятся на пустой оператор (см. листинг 14). Это позволяет быть уверенным в том, что никакой отладочной печати в окончательном исполняемом модуле не останется.
42
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
V. Автоматические тесты
Тестирование программ может
использоваться для демонстрации
наличия ошибок, но оно никогда
не покажет их отсутствие.
Дейкстра, 1970 г.
10. Тестирование
В процессе отладки программы используется тестирование.
Тестирование представляет собой не только часть отладочной работы, но и самостоятельный этап. После создания программы перед ее эксплуатацией она тестируется на специально созданных
тестах, примерах входных данных, для которых заранее известен
результат работы программы. Тест может быть пройден успешно
при совпадении результатов работы с ожидаемыми либо неуспешно в противном случае. Последнее обстоятельство говорит о
том, что программа не готова к эксплуатации, процесс ее создания не закончен. Естественно, создатели программы ожидают успешного тестирования, в таком случае оно может восприниматься как демонстрация корректной работы программы, хотя это
восприятие не всегда верно. Для большинства программ можно
сочинить тест, на котором она будет работать правильно. Но, несмотря на желаемый результат, тест должен быть направлен не на
демонстрацию правильной работы программы, а на выявление
возможных ошибок в ней. Поэтому правильней было бы назвать
тестирование, показавшее ошибки в программе, удачным: с этой
целью тест и был создан, и наоборот, тестирование, прошедшее
«гладко» – нерезультативным, неудачным. Но это вопросы терминологии.
Создание удачного набора тестов, способствующего обнаружению ошибок, требует высокой квалификации и немалых затрат
труда и времени. Представим себе фрагмент программы с блоксхемой управляющих конструкций, представленной на рис. 19.
43
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
1
3
2
4
5
7
9
6
8
Рис. 19. Блок-схема некоторого алгоритма или его части
Запуск одного теста для данного фрагмента проверит выполнение одного из путей в представленном графе. В таком случае
говорят, тест покрывает некоторый логический путь. Сколько
возможных путей в этом, далеко не самом сложном по взаимосвязям, фрагменте? Если не принимать во внимание цикл из блоков 2 и 4, то получим семь путей, соответствующих семи различным возможным сценариям работы программы. Учитывая же наличие цикла, понимаем, что таковых путей неограниченное
число.
Набор тестов, заставляющих программу пройти по всем возможным путям (покрывающий всю логику), является исчерпывающим. Но такой набор далеко не всегда можно создать, т. к.
любое разветвление в программе может увеличить число путей, а
соответственно, и тестов вдвое. Однако исчерпывающее тестирование также не может дать гарантии отсутствия ошибок в случае
их необнаружения при прогоне тестов, поскольку выполнение
программы зависит не только от управляющих конструкций, но и
от многих иных факторов, например, от формата входных данных, типов данных, объема доступной памяти и т. п. (см. эпиграф).
Если создавать тесты с учетом истории поведения программы в каждом случае вручную, то потребуется выполнить достаточно большой объем работ. В крупных программных системах
внесение изменений в одну из подсистем часто влечет за собой
изменение работы связанных с ней подсистем. В таких случаях
разработчику очень сложно гарантировать корректность работы
44
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
всей системы целиком. Ошибки, которые возникают при таких
изменениях, часто проявляются спустя длительное время работы
после произведенных изменений исходного кода.
Для уменьшения количества ошибок в крупных системах
применяются автоматические тесты. Этот метод позволяет производить тестирование всей системы с наименьшими затратами.
Для его осуществления создаются специальные тестирующие
сценарии, которые периодически запускаются для проверки корректности работы подсистем и системы в целом.
Такие сценарии обычно вызывают тестируемую функцию/подсистему с заранее известным набором входных параметров и автоматически проверяют результат работы на совпадение
с ожидаемым.
Приведем простой пример. Пусть мы хотим покрыть автотестом функцию, суммирующую элементы массива из целых чисел:
int CalcSum(const int *array, int count);
Для этого создадим скрипт (фрагмент исходного кода программы) с определением функции TestCalcSUm() следующего
вида:
Листинг 16
bool TestCalcSUm() {
const int A[] = {1, 2, 3, 4, 5};//исходный
массив
const int ARes = 15,
ACnt = sizeof(A)/sizeof(A[0]);
std::cerr << «Testing CalcSum» << std::endl;
int res = CalcSum(A, Acnt);
if (res != Ares) {
std::cerr << «Error:» << std::endl;
std::cerr << «Expected result: » << ARes <<
std::endl;
std::cerr << «Received result: » << res <<
std::endl;
return false;
}
std::cerr << «Ok» << std::endl;
return true;
}
45
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Данная функция создана для проверки работы исследуемой
функции CalcSum() для конкретного исходного массива
const int A[] = {1, 2, 3, 4, 5};
и потому ей известен результат
const int ARes = 15,
который и должна вернуть исследуемая функция после вызова
int res = CalcSum(A, Acnt);
для входного массива A[] .
В случае если TestCalcSum() возвращает true, тест считается пройденным, иначе — провалившимся.
Заметим, что возможны ситуации, когда тестируемая функция проходит тест, но, тем не менее, работает некорректно. Чтобы избежать таких ситуаций, необходимо делать как можно
больше тестов, которые покроют все «крайние случаи». Естественно, число тестов должно увеличиваться не за счет однотипных
тестов.
11. Организация
автоматического тестирования
Как правило, автоматические тесты запускаются с определенной периодичностью – например, раз в сутки. В большой системе тестирование может достигать нескольких часов, поэтому
его удобнее проводить в то время, когда оно никому не помешает, например, ночью. В результате на следующее утро появляется
список проваленных тестов и программисты, ответственные за
соответствующие подсистемы, выясняют причины возникновения проблем.
Каждый разработчик после изменения исходного кода может
запустить часть автотестов, которые, по его мнению, могли быть
затронуты изменениями. В случае если после внесенных изменений тесты пройдены без ошибок, разработчик публикует сделанные изменения.
46
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
12. Принципы создания тестов
Приведем несколько основных принципов создания тестов.
 Тестами должно быть покрыто как можно больше кода, в
идеале весь код. Последнее для более-менее серьезных программных систем невозможно, в таком случае существуют варианты покрытия тестами различных структур: операторов, переходов, условий, комбинированный подход.
 Должны быть рассмотрены все «крайние», граничные, вырожденные случаи, например, минимальные входные данные,
максимальные входные данные, неверные входные данные. Полезно составить тесты, приводящие к граничным или выходящим
за границы результатам.
 Желательно, чтобы тесты реализовывал человек, который
НЕ разрабатывал тестируемый модуль. Если ошибки в программе
связаны с неверным пониманием задачи, то и тесты, создаваемые
автором программы, будут основываться на этом неверном понимании. Автору программы труднее создать наиболее эффективные тесты, нежели специалисту со «свежим» взглядом.
 Результаты каждого тестирования должны быть тщательно
изучены со всех сторон с целью обнаружения максимального
числа ошибок и недостатков в работе программы.
13. Достоинства и недостатки автотестов
Автоматические тесты позволяют минимизировать человеческий фактор при проверке корректности работы программного
комплекса. Как правило, разработчик конкретной подсистемы
проверяет только тот логический сценарий работы комплекса,
который кажется ему наиболее вероятным при использовании,
наиболее типичным. Автотесты составляются несколькими специалистами по мере обнаружения новых типов ошибок, поэтому
они покрывают гораздо большее количество возможных сценариев работы системы.
Основной недостаток автотестов — это увеличение времени
разработки. Помимо собственно разработки основной функциональности, разработчик вынужден составлять сценарии для тестирования. Однако этот недостаток очень скоро окупается за счет
47
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
того, что при использовании автоматического тестирования возрастает эффективность процесса нахождения ошибок в программе.
Упражнения
1. Написать программу, отыскивающую простые числа на отрезке натурального ряда [2; N]. Используя режим отладки и некоторые его дополнительные возможности, составить трассировочную таблицу.
2. Используя режим отладки и отладочные конструкции, оценить погрешность вычисления функции с помощью её шаблона
из математической библиотеки math.h и непосредственного
значения суммы ряда в зависимости от величины N. Примеры
функций и рядов:
N
à) ln(1  x) è  (1) k 1
k 1
xk
;
k
N
á) cos( x) è  (1) k
k 0
x 2k
(2k )!
3. Написать собственный отладочный макрос или функцию,
который(ая) при передаче ему(ей) в качестве аргумента ложного
логического условия выводил(а) бы в консоль соответствующие
предупреждения, предлагающие программисту выбор: продолжать отладку (выполнение) или завершить работу приложения.
48
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Рекомендуемая литература
1. Секунов, Н. Самоучитель Visual C++ .NET / Н. Секунов.
– СПб.: БХВ-Петербург, 2002.
2. Майерс, Г. Искусство тестирования программ / Г. Майерс. – М.: Финансы и статистика, 1982.
3. Бейзер, Б. Тестирование «чёрного ящика». Технологии
функциионального тестирования программного обеспечения и
систем / Б. Бейзер. – СПб.: Питер, 2004.
4. www.msdn.com (база знаний Microsoft. Раздел «C++»).
49
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Оглавление
I. Этапы создания программ на С/C++ ......................................................... 5
1. Создание консольного приложения ...................................................... 7 2. Ошибки компиляции и ошибки компоновки .....................................10 II. Отладочный интерфейс Microsoft Visual C++ ...................................... 16
3. Пошаговая отладка ...............................................................................19 4. Пошаговая отладка с заходом в подпрограмму ................................22 5. Использование контрольных точек останова. Их классификация ...25 6. Окно быстрого просмотра переменных (Quick Watch) .....................31 III. Обработка исключительных ситуаций ................................................ 32
IV. Отладочные макросы .............................................................................. 35
7. Отладочный макрос _ASSERT ............................................................36 8. Отладочный макрос _RPTn .................................................................38 9. Создание макроса отладочной печати ................................................41 V. Автоматические тесты .............................................................................. 43
10. Тестирование ......................................................................................43 11. Организация автоматического тестирования ..................................46 12. Принципы создания тестов ................................................................47 13. Достоинства и недостатки автотестов ..............................................47 Рекомендуемая литература ........................................................................... 49
50
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
Учебное издание
Лаврентьев Иван Викторович
Никулин Вадим Андреевич
Чаплыгина Надежда Борисовна
Средства отладки
в Visual C++
Методические указания
Редактор, корректор И. В. Бунакова
Верстка Е. Л. Шелехова
Подписано в печать 21.01.10. Формат 6084 1/16.
Бум. офсетная. Гарнитура "Times NewRoman".
Усл. печ. л. 3,02. Уч.-изд. л. 2,26.
Тираж 130 экз. Заказ
Оригинал-макет подготовлен
в редакционно-издательском отделе Ярославского
государственного университета им. П. Г. Демидова.
Отпечатано на ризографе.
Ярославский государственный университет им. П. Г. Демидова.
150000, Ярославль, ул. Советская, 14.
51
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
52
Copyright ОАО «ЦКБ «БИБКОМ» & ООО «Aгентство Kнига-Cервис»
И. В. Лаврентьев
В. А. Никулин
Н. Б. Чаплыгина
Средства отладки
в Visual C++
53
Документ
Категория
Без категории
Просмотров
71
Размер файла
645 Кб
Теги
visual, отладка, 682, средств, лаврентьев
1/--страниц
Пожаловаться на содержимое документа