ПРОГРАММИРОВАНИЕ НА VISUAL C++

Выпуск No. 39 от 1 апреля 2001 г.

Добрый день, уважаемые подписчики! С праздником вас!

/ / / / СТАТЬЯ / / / / / / / / / / / / / / / / / / / / / /

Диагностические средства MFC

Автор: Олег Быков

Библиотека MFC предоставляет программисту мощный набор средств для отладки приложений любой сложности. Данная статья ставит своей целью последовательное рассмотрение диагностических средств для помощи начинающим MFC-программистам в выборе и более полном их использовании.

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

Иными словами, в код помещаются проверки, которые выполняются только в отладочной версии программы, и не включаются в код при окончательной сборке (Release build). За время работы с отладочной версией программы (в идеале) выясняются и устраняются все возможные ошибочные ситуации и надобность в замедляющих работу проверках отпадает (здесь не имеются в виду ошибки, на которые программа должна реагировать определенными действиями. В частности, не стоит проверять таким образом результаты работы API-функций, так как нельзя гарантировать корректность возвращаемых ими значений и в отладочной сборке, и в окончательной). Чтобы стало понятней, рассмотрим несколько диагностических макроопределений.

ASSERT и VERIFY

ASSERT - пожалуй, один из самых часто употребляемых макросов. Принимая в качестве аргумента булево значение, ASSERT продолжает работу программы, если это значение равно TRUE, и прерывает работу программы в ином случае. При этом ASSERT выводит информационное окно с именем исходного файла и номером строки, содержащей сработавший макрос, и предоставляет разработчику выбор - окончательно прервать работу программы (Abort), переключиться в окно отладчика (Retry) или продолжить работу (Ignore).

В качестве примера использования ASSERT можно привести проверку входного значения функции:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200)); // сработает при любых x, меньших 0
   m_nAge = x;			    // или больших 199
};

При срабатывании макроса (то есть, при передаче неверного nAge) у разработчика есть возможность переключиться в окно отладчика и через список вызовов (Call Stack) определить, откуда был передан ошибочный параметр.

ПРИМЕЧАНИЕ
ASSERT развернется в код только при Debug-сборке. Чтобы обеспечить вычисление параметра и в окончательной версии проекта (в случае, когда в ASSERT вызывается нужная функция), используйте макроопределение VERIFY. При Debug-сборке этот макрос полностью идентичен ASSERT, но, в отличие от него, при Release-сборке VERIFY разворачивается в код и вычисляет значение своего аргумента, хотя при этом никак не влияет на ход выполнения программы.

В MFC определен вспомогательный макрос DEBUG_ONLY, который служит для обеспечения выполнения своего параметра только при Debug-сборке. В Release-версии приложения выражение внутри DEBUG_ONLY будет проигнорировано.

ASSERT_KINDOF и ASSERT_VALID

Эти макросы предназначены для диагностики состояния объектов. ASSERT_KINDOF принимает два параметра - имя класса и указатель на объект - и срабатывает (прерывая выполнение программы подобно ASSERT) в случае, когда объект, переданный по указателю, не является объектом данного класса или одного из потомков данного класса. Пример использования макроса:

CPerson::CPerson(CPerson &newPerson)
{
   ASSERT_KINDOF(CPerson, &newPerson); // сработает, если в конструктор
                                      // был передан объект не того класса
};

ASSERT_KINDOF полностью идентичен следующей конструкции (для нашего примера):

ASSERT(newPerson.IsKindOf(RUNTIME_CLASS(CPerson)));

Для того, чтобы получить информацию о классе в процессе исполнения, этот класс должен быть унаследован от CObject (или одного из его потомков), и для него должны быть использованы макросы DECLARE_DYNAMIC(classname) и IMPLEMENT_DYNAMIC(classname,baseclass) (иначе обращение к ASSERT_KINDOF приведет к ошибке нарушения защиты). Это относится и к проверяемому объекту, и к классу.

ASSERT_VALID служит для проверки внутреннего состояния объектов. Этот макрос принимает один параметр - указатель на проверяемый объект - и проделывает с ним следующее: проверяет валидность указателя, проверяет его на равенство NULL, и вызывает функцию объекта AssertValid.

AssertValid реализована почти во всех классах MFC (унаследованных от CObject), но разработчик может реализовать ее и в своем классе, соблюдая определенные правила. Во-первых, AssertValid должна быть переопределенной виртуальной функцией класса CObject. Эта функция описана как const, поэтому внутри нее нельзя изменять данные класса. Во-вторых, для индикации факта невалидности объекта функция должна использовать макрос ASSERT. И в-третьих, в AssertValid желательно вызвать эту же функцию класса-родителя.

Таким образом, разработчик может использовать ASSERT_VALID для реализации любых алгоритмов проверки состояния объекта. Например, вот так:

void CPerson::AssertValid() const
{
   CObject::AssertValid(); // подразумевается, что CPerson унаследован
  			   // от CObject
   ASSERT((m_nAge>=0) && (m_nAge<200));
};
ПРИМЕЧАНИЕ
ASSERT_KINDOF и ASSERT_VALID развернутся в код только при Debug-сборке.

В MFC определены два вспомогательных макроса для тестирования указателей: ASSERT_POINTER и ASSERT_NULL_OR_POINTER. Оба они принимают в качестве параметров два значения - указатель и его тип. ASSERT_POINTER сначала проверяет указатель на NULL, затем тестирует память по этому указателю на валидность. По непрохождении хотя бы одной проверки макрос срабатывает и останавливает работу программы. ASSERT_NULL_OR_POINTER также проверяет память, на которую ссылается указатель, но не прерывает выполнение программы, если тестируемый указатель равен NULL (хотя указатель при этом и является невалидным).

Работа с отладочной информацией (Output window)

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

В Windows предусмотрен еще один способ получить информацию от программы во время ее исполнения - функция OutputDebugString. Функция принимает LPCTSTR-строку и посылает ее отладчику, под управлением которого исполняется приложение. В случае запуска приложения из Visual C++ посланная строка попадает в окно Output последнего (закладка Debug). Преимущество данного способа в том, что он не требует прерывать работу программы для отслеживания значений (что иногда критично - например, при отлаживании обработчика сообщения WM_TIMER или асинхронного выполнения функций), и не требует убирать лишний код после отлаживания нужного участка (в отличие, скажем, от метода с MessageBox).

OutputDebugString можно использовать следующим образом:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200)); 
   m_nAge = x;
   CString str;
   str.Format(_T("New age = %d\n"),nAge);
   OutputDebugString(str);
};

MFC упрощает вывод отладочной информации, определяя глобальный объект afxDump класса CDumpContext. Информация при этом выводится таким образом:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200));  
   m_nAge = x;
   afxDump << _T("New age = ") << nAge << _T("\n");
};

Отладочную информацию можно также выводить макросами TRACE, TRACE0, TRACE1, TRACE2 и TRACE3 (для таких целей обычно их и используют). Все они выводят переданную им информацию через afxDump, при этом принимают те же параметры, что и функция printf:

void CPerson::SetPersonAge(int nAge)
{
   ASSERT((nAge>=0) && (nAge<200));  
   m_nAge = x;
   TRACE(_T("New age = %d\n"),nAge);
};

Макросы TRACEn аналогичны макросу TRACE, с той лишь разницей, что их первый параметр имеет тип LPCSTR (а не LPCTSTR), и они принимают не произвольное число параметров, а определенное цифрой в их имени. Длина первого параметра всех TRACE-макросов (после всех подстановок) не должна превышать 512 символов, иначе макрос сгенерирует ASSERT.

Макросы TRACEn оставлены в MFC для обратной совместимости, при написании приложений рекомендуется пользоваться макросом TRACE.

ПРИМЕЧАНИЕ
Вывод отладочной информации и через afxDump, и через TRACE-макросы работает только в Debug-версии приложения.

В поставку Visual Studio 6.0 входит утилита для настройки вывода информации через TRACE-макросы - "MFC Tracer" (tracer.exe). С ее помощью можно отключить вывод отладочной информации (даже при Debug-сборке), заставить MFC выводить перед каждым сообщением в окне Output имя сгенерировавшего это сообщение проекта (полезно при отладке проекта, использующего DLL или состоящего из нескольких приложений), включить вывод уведомлений MFC об обработке определенных оконных сообщений и т.д.

Заканчивая обсуждение отладочной информации, нельзя не упомянуть утилиту TRACEWIN, написанную Paul DiLascia, также доступную в исходных текстах в апрельском номере MSJ за 1997 год. Эта утилита внедряется во все запускаемые процессы, и для MFC Debug-проектов перенаправляет весь TRACE-вывод в отдельное окно (причем перенаправление автоматически включается даже для уже запущенных приложений). Очень удобный инструмент. Более того, в той же статье Paul DiLascia доходчиво разъясняет принципы внедрения DLL в чужой процесс и приводит C++-класс, облегчающий эту задачу.

Вывод информации о внутреннем состоянии объектов

afxDump позволяет выводить в окне отладчика не только переменные, но и целые объекты (порожденные от CObject). Конструкция afxDump << &myPerson (или afxDump << myPerson) развернется в вызов myPerson.Dump(afxDump) (виртуальная функция класса CObject). Разрабатывая собственный класс, программист может переопределить эту функцию, реализовав свой метод вывода внутренней информации объекта, например, вот так:

// CPerson унаследован от CObject
void CPerson::Dump(CDumpContext &dc) const
{
   CObject::Dump(dc);
   dc << T("Age = ") << m_nAge;
};

Вызов родительской функции CObject::Dump(dc) выведет на контекст имя класса, в случае, если для реализации этого класса (CPerson в примере выше) используется связка макросов DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC (или DECLARE_SERIAL/IMPLEMENT_SERIAL). Обратите внимание, что посылка символа перевода строки в переопределенной Dump не требуется.

ПРИМЕЧАНИЕ
Вывод отладочной информации об объекте через функцию Dump будет работать только в Debug-версии приложения, но здесь разработчик должен сам позаботиться о выполнении этого ограничения - и объявление, и реализацию, и вызовы функции Dump следует обрамлять проверками на Debug-сборку проекта:
class CPerson : public CObject
{
   . . .
public:
#ifdef _DEBUG
   void Dump(dc) const;
   void AssertValid() const; // это же касается объявления AssertValid
  			     // (но не использования макроса ASSERT_VALID !)
#endif
   . . . 
};

После выполнения всех этих действий остается только скинуть в afxDump указатель на наш объект, и изучать полученную информацию в Output-окне отладчика. Многие классы MFC реализуют функцию Dump для диагностики их внутреннего состояния, что особенно полезно при отладке работы с классами-коллекциями C*Array, C*List и C*Map. Чтобы получить и состояние объектов, содержащихся в коллекции, нужно установить глубину вызова Dump-функции, отличную от 0, функцией SetDepth(int newDepth) класса CDumpContext:

CArray<CPerson,CPerson> arrPersons;
WorkWithArray(arrPersons); // здесь идет работа с массивом
#ifdef _DEBUG
   afxDump.SetDepth(1);    // вывести информацию не только о коллекции,
   afxDump << &arrPersons; // но и о всех ее членах (будет вызвана
		           // CPerson::Dump для каждого элемента массива)
#endif
Диагностика ошибок работы с памятью

Одна из самых распространенных ошибок при работе с памятью - выделение блока памяти (к примеру, при создании нового объекта) без его последующего освобождения (так называемые утечки памяти). Сами по себе эти утечки нефатальны ни для работы приложения, ни для работы системы (по завершению работы приложения Windows все равно освободит все занятые приложением блоки памяти), но это может привести к нехватке памяти, если приложение исполняется относительно долгое время (например, если это WinNT-сервис). Обычно для отслеживания утечек памяти используют специализированные программы, например, NuMega BoundsChecker, но и в MFC предусмотрены некоторые возможности для диагностики подобных ситуаций.

Класс CMemoryState предназначен для обнаружения динамически выделенных и не освобожденных впоследствии блоков памяти. Алгоритм работы с этим классом сводится к запоминанию списка созданных объектов функцией CMemoryState::Checkpoint, и последующим сравнением двух классов функцией CMemoryState::Difference. Например, вот так:

#ifdef _DEBUG
   CMemoryState msStart, msEnd, msDiff;
   msStart.Checkpoint();       // начало подозрительного блока
#endif
. . .
   CPerson *pPerson = new CPerson();
. . .
#ifdef _DEBUG
   msEnd.Checkpoint();         // конец подозрительного блока
   if (msDiff.Difference(msStart,msEnd)
   {
      TRACE0("Memory leaked!\n");
      msDiff.DumpAllObjectsSince(); //в Output-окне отладчика выведется
      msDiff.DumpStatistics();      //информация о созданных объектах
				    //и о динамической памяти вообще
   };
#endif
ПРИМЕЧАНИЕ
Обратите внимание на скобки #ifdef/endif - с классом CMemoryState можно работать только в Debug-версии библиотеки MFC.

Разработчики используют класс CMemoryState для проверки подозрительных кусков кода на корректность работы с динамической памятью. Библиотека MFC имитирует использование CMemoryState с помощью глобального объекта класса _AFX_DEBUG_STATE, в деструкторе которого вызывается функция _CrtDumpMemoryLeaks (подробнее об этом можно почитать в статье "Обнаружение и локализация утечек памяти").

Функции DumpAllObjectsSince и DumpStatistics выводят в окне отладчика информацию о всех выделенных объектах со времени последнего вызова Checkpoint() и информацию о состоянии динамической памяти, соответственно. Информация о памяти выводится в следующем виде:

0 bytes in 0 Free Blocks
22 bytes in 1 Object Blocks
45 bytes in 4 Non-Object Blocks
Largest number used: 67 bytes
Total allocations: 67 bytes

Первая строка показывает число блоков памяти в объектах с отложенным удалением (в MFC имеется способ сделать так, чтобы delete не удаляла объекты сразу, а откладывала бы эту процедуру до конца работы программы. Это делается для тестирования программ в условиях нехватки памяти). Вторая и третья строки показывают размер занимаемой памяти и число объектов, соответственно, порожденных и не порожденных от CОbject. Последние две строки показывают максимальный и общий размер выделенной памяти.

Для того, чтобы MFC включила в отчет о состоянии памяти имя файла и номер строки, на которой был выделен неосвобожденный объект, в программе должен присутствовать следующий код:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Эти строки MFC по умолчанию вставляет в исходные файлы при генерации нового проекта.

Надеюсь, этот обзор помог читателю сориентироваться в многообразии отладочных средств библиотеки MFC. Более подробную информацию по данной тематике можно найти в MSDN или исходных кодах примеров (в том числе исходных кодах самой MFC). Удачи!

Автор выражает благодарность Александру Шаргину за ценные советы и замечания.

/ / / / ВОПРОС-ОТВЕТ / / / / / / / / / / / / / / / /

Ну, господа, пришло время что-то решать... Так как мне опять не пришло ни одного ответа на вопрос, думаю что рубрика "Вопрос-Ответ", в том виде в каком она сейчас существует вам не интересна. Поэтому со следующего выпуска и вопросы, и ответы будут публиковаться одновременно. Это будет больше похоже на HOWTO.

/ / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / / /

Это все на сегодня. Пока!

Алекс Jenter   jenter@mail.ru
Красноярск, 2001.

Предыдущие выпуски     Статистика рассылки