Поиск ошибок при работе с памятью

Автор: Александр Шаргин
Источник: RSDN Magazine #0
Опубликовано: 27.01.2002
Версия текста: 1.0
Устройство отладочной кучи
Использование отладочной кучи
Настройка DCRT
Имена файлов и номера строк
Проверка памяти
Обнаружение записи за пределы блока
Обнаружение записи в освобождённый блок
Обнаружение утечек памяти
Поиск распределения по серийному номеру
Создание пользовательских отладочных дампов
Стресс-тестирование
Мгновенные снимки памяти
Отладочные макросы

Ошибки при работе с памятью принадлежат к числу самых распространённых ошибок, с которыми приходится сталкиваться программисту на языке C++. Поэтому в Visual C++ включено специальное средство для поиска ошибок подобного рода – отладочная библиотека времени выполнения (Debug CRT, DCRT). Она включает в себя функции для работы с отладочной кучей (debug heap), которая поможет вам обнаружить:

Прежде чем изучать функции, предназначенные для работы с отладочной кучей, кратко рассмотрим принципы её работы.

Устройство отладочной кучи

В DCRT существуют специальные функции для распределения памяти из отладочной кучи - _malloc_dbg, _free_dbg и некоторые другие. В отладочной версии программы всё распределение памяти идёт через них, так как через них реализуются стандартные функции malloc/free и операторы new/delete. Функция _malloc_dbg выделяет блоки памяти следующего вида (рис. 15).


Рисунок 15. Блок в отладочной куче

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

Запрещённые области располагаются по обе стороны от области данных. В Visual C++ 6.0 каждая из них имеет длину 4 байта. При распределении блока эти области заполняются предопределённым значением _bNoMansLandFill (в данный момент оно равно 0xFD). Если программа перезапишет эти значения, будет зафиксирована ошибка записи за границы выделенного блока. Сама область данных изначально заполняется значением _bCleanLandFill (сейчас равно 0xCD). Благодаря этому можно легко отличить неинициализированные области блока от инициализированных.

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

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

Теперь рассмотрим различные типы блоков.

Использование отладочной кучи

Настройка DCRT

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

Первая функция, которую мы рассмотрим, называется _CrtSetDbgFlag. Она позволяет задать те проверки отладочной кучи, которые вам нужны. По умолчанию большая часть проверок отключена: утечки памяти не отслеживаются, а запись в освобождённый блок не контролируется. Работает только проверка записи за границы массива. Чтобы изменить ситуацию, вызовите _CRTSetDbgFlag и передайте ей нужные флаги из следующего набора (таблица 6).

ФлагРежим по умолчаниюОписание
_CRTDBG_ALLOC_MEM_DFВключёнВключает использование отладочной кучи. Если этот флаг не установлен, все выделяемые блоки получают тип _IGNORE_BLOCK и не участвуют в проверках.
_CRTDBG_DELAY_FREE_MEM_DFВыключенПредотвращает фактическое освобождение памяти. Освобождаемые блоки остаются в памяти и получают тип _FREE_BLOCK.
_CRTDBG_CHECK_ALWAYS_DFВыключенПредписывает проверять целостность отладочной кучи при каждом выделении или освобождении памяти. Этот режим следует оставлять выключенным, только если он слишком сильно замедляет выполнение программы. В остальных случаях он позволит вам найти ошибки как можно быстрее.
_CRTDBG_CHECK_CRT_DFВыключенВключает анализ блоков памяти, выделяемых CRT (имеющих тип _CRT_BLOCK). Этот режим не нужен практически никогда.
_CRTDBG_LEAK_CHECK_DFВыключенАктивизирует режим отображения утечек памяти после завершения программы.
Таблица 6. Режимы работы отладочной кучи

Функция _CrtSetDbgFlag возвращает набор флагов, который использовался до её вызова. Вы можете вызывать эту функцию в любом месте программы, включая и выключая различные режимы по мере необходимости. Можно также не менять текущие настройки, а только получить текущий набор флагов - для этого следует передать _CrtSetDbgFlag специальное значение _CRTDBG_REPORT_FLAG. Например:

_CrtSetDbgFlag(
    _CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) |
    _CRTDBG_CHECK_ALWAYS_DF |
    _CRTDBG_DELAY_FREE_MEM_DF |
    _CRTDBG_LEAK_CHECK_DF
);

Теперь немного поговорим о диагностических сообщениях, которые будет выдавать вам DCRT. Они разделяются на три типа: предупреждения (warnings, тип _CRT_WARN), ошибки (errors, тип _CRT_ERROR) и неверные утверждения (assertion failures, тип _CRT_ASSERT). Для каждого из этих типов можно настроить режим отображения. По умолчанию предупреждения выдаются в окне Debug, а ошибки и неверные утверждения - в отдельном модальном окне. Чтобы изменить это поведение, используется функция _CrtSetReportMode. Первый параметр этой функции - тип сообщений (_CRT_WARN, _CRT_ERROR или _CRT_ASSERT). Второй параметр задаёт режим отображения:

Для режима вывода в файл требуется дополнительно указать хэндл этого файла. Для этого используется функция _CrtSetReportFile. Она принимает два параметра - тип сообщений (_CRT_WARN, _CRT_ERROR или _CRT_ASSERT) и хэндл файла. Можно указать реальный хэндл или одно из предопределённых значений:

Рассмотрим небольшой пример. Следующий фрагмент перенаправляет все сообщения о неверных утверждениях в поток stderr.

_CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE);
_CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);

Имена файлов и номера строк

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

О замене функций malloc, calloc и т. п. их отладочными эквивалентами заботится заголовочный файл crtdbg.h. Нужно только определить перед его включением макрос _CRTDBG_MAP_ALLOC, который делает активным следующий код:

#ifdef  _CRTDBG_MAP_ALLOC

#define malloc(s)      _malloc_dbg(s,_NORMAL_BLOCK,__FILE__,__LINE__)
#define calloc(c,s)    _calloc_dbg(c,s,_NORMAL_BLOCK,__FILE__,__LINE__)
#define realloc(p,s)   _realloc_dbg(p,s,_NORMAL_BLOCK,__FILE__,__LINE__)
#define _expand(p,s)   _expand_dbg(p,s,_NORMAL_BLOCK,__FILE__,__LINE__)
#define free(p)        _free_dbg(p,_NORMAL_BLOCK)
#define _msize(p)      _msize_dbg(p,_NORMAL_BLOCK)

#endif  /* _CRTDBG_MAP_ALLOC */

Об операторе new придётся позаботиться самостоятельно. В DCRT реализована отладочная версия оператора new:

void *operator new(
    unsigned int cb,
    int nBlockUse,
    const char *szFileName,
    int nLine
);

Необходимо перенаправить все обращения к new на эту версию. Для этого включите в программу следующий фрагмент.

#ifdef _DEBUG
#ifdef _CRTDBG_MAP_ALLOC
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)
#endif /* _CRTDBG_MAP_ALLOC */
#endif /* _DEBUG */

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

Глобальная замена оператора new в коде программы обычно является безопасной операцией. Но если в области действия приведённого выше макроса окажется класс, реализующий собственную версию оператора new, Visual C++ откажется его компилировать и выдаст множество странных ошибок. Будьте внимательны, чтобы не допустить такой ситуации.

Проверка памяти

В DCRT есть несколько функций для проверки памяти. Поскольку они полностью исчезнут из финального построения, я рекомендую использовать их как можно чаще. Все проверочные функции возвращают TRUE (если проверка прошла успешно) или FALSE (если были обнаружены ошибки), поэтому их можно использовать в макросах утверждений (таких как ASSERT в MFC и ATLASSERT в ATL). Описание этих функций приведено в таблице 7.

ФункцияОписание
int _CrtIsValidPointer(const void *address, unsigned int size, int access);Проверяет, разрешён ли доступ к области памяти по адресу address с размером size. Если access равен TRUE, проверяется возможность как чтения, так и записи, а если FALSE – только чтения.
int _CrtIsValidHeapPointer(const void *userData);Проверяет, что адрес userData принадлежит локальной куче, то есть одному из выделенных блоков памяти.
int _CrtIsMemoryBlock(const void *userData, unsigned int size, long *requestNumber, char **filename, int *linenumber);Проверяет, существует ли по адресу userData выделенный блок памяти размером size байт. Дополнительно функция _CrtIsMemoryBlock может записать информацию об этом блока - серийный номер, имя файла и номер строки - по адресам requestNumber, filename и linenumber соответственно. Если какие-то данные вас не интересует, передайте в соответствующих параметрах значение NULL.
Таблица 7. Функции для проверки памяти

Обнаружение записи за пределы блока

По умолчанию проверка запрещённых областей осуществляется при освобождении блока (в тот момент, когда вы вызываете free или delete). Можно также в любой момент проверить все выделенные блоки, используя функцию _CrtCheckMemory. Эта функция не имеет параметров и возвращает FALSE, если были обнаружены какие-то нарушения. Если флаг _CRTDBG_CHECK_ALWAYS_DF установлен, DCRT сама вызывает _CrtCheckMemory при каждом выделении или освобождении памяти.

Если в одном из блоков обнаружен перезаписанный запрещённый блок, в окно Debug выводится информация следующего вида:

memory check error at 0x00301BFC = 0x00, should be 0xFD.
memory check error at 0x00301BFD = 0x00, should be 0xFD.
memory check error at 0x00301BFE = 0x00, should be 0xFD.
memory check error at 0x00301BFF = 0x00, should be 0xFD.

Кроме этого, вы получите сообщение об ошибке:

DAMAGE: before Normal block (#44) at 0x00301C00.

Это сообщение может выдаваться как предупреждение (если оно возникает в процессе проверки всех блоков функцией _CrtCheckMemory) или как ASSERT (неверное утверждение), если проверяется только один блок в функции _free_dbg. Обратите внимание на число в скобках с предшествующим символом "#" – это серийный номер блока.

Обнаружение записи в освобождённый блок

Чтобы можно было обнаружить запись в уже освобождённый блок памяти, необходимо включить режим _CRTDBG_DELAY_FREE_MEM_DF.

Записи в освобождённый блок обнаруживает функция _CrtCheckMemory. Как уже говорилось, её может вызывать как отлаживаемая программа, так и сама DCRT (в режиме _CRTDBG_CHECK_ALWAYS_DF). Если ошибка обнаружена, выдаётся диагностическая информация и предупреждение следующего вида:

memory check error at 0x00301C04 = 0x01, should be 0xDD.
memory check error at 0x00301C05 = 0x00, should be 0xDD.
memory check error at 0x00301C06 = 0x00, should be 0xDD.
memory check error at 0x00301C07 = 0x00, should be 0xDD.
DAMAGE: on top of Free block at 0x00301C00.
DAMAGED allocated at file D:\MemDbg\MemDbg.cpp(17).
DAMAGED located at 0x00301C00 is 40 bytes long.

Обнаружение утечек памяти

Чтобы получить отчёт об утечках памяти, достаточно вызвать перед завершением программы функцию _CrtDumpMemoryLeaks (без параметров). Более того, если включить режим _CRTDBG_LEAK_CHECK_DF, DCRT вызовет её за вас. Отчёт об утечках выглядит примерно так.

Detected memory leaks!
Dumping objects ->
D:\MemDbg\MemDbg.cpp(20) : {47} normal block at 0x00301970, 400 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
D:\MemDbg\MemDbg.cpp(19) : {46} normal block at 0x00301B30, 160 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
D:\MemDbg\MemDbg.cpp(18) : {45} normal block at 0x00300030, 3 bytes long.
 Data: <   > CD CD CD 
Object dump complete.

Для каждого блока выводится: имя файла и номер строки (если доступны), серийный номер (в фигурных скобках), базовый адрес и длина, а также содержимое первых 16 байтов этого блока. Для клиентских блоков можно написать функцию, которая будет выдавать содержимое блока в более осмысленном виде. Скоро мы увидим, как это делается.

Поиск распределения по серийному номеру

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

Как я уже говорил, блок уникально идентифицируется серийным номером. Причём, если программа выполняется по одному и тому же сценарию, этот номер не меняется от запуска к запуску. Оказывается, на распределение блока с заданным серийным номером можно установить точку останова. Для этого используется функция _CrtSetBreakAlloc, которая принимает в качестве параметра серийный номер интересующего вас блока. Её нужно вызывать как можно раньше, чтобы случайно не пропустить нужное распределение. Лучше всего сделать это в самом начале функции main (или WinMain). Как только программа дойдёт до распределения блока с заданным серийным номером, её выполнение будет прервано посредством DebugBreak.

У этого подхода есть один недостаток: для установки точки останова приходится вставлять в программу вызов _CrtSetBreakAlloc и собирать её заново. Было бы гораздо удобнее делать это прямо в процессе отладки. Оказывается, это возможно. На самом деле, функция _CrtSetBreakAlloc просто записывает серийный номер в переменную _crtBreakAlloc. Это можно сделать и вручную прямо из отладочного окна Watch. Здесь возникает единственный нюанс: если вы используете DLL-версию DCRT, для доступа к переменной _crtBreakAlloc необходимо указать полный контекст:

{,,msvcrtd.dll}_crtBreakAlloc

Серийные номера не всегда позволяют найти потерянные блоки. Если память выделяется из разных потоков или в зависимости от неконтролируемых внешних воздействий, обеспечить неизменность серийных номеров от одного сеанса отладки к другому не представляется возможным. Такая ситуация часто встречается в серверных приложениях. В статье "Устранение утечек памяти в ATL-проектах" (в этом же номере) описан более пригодный для такого случая способ отладки.

Создание пользовательских отладочных дампов

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

void DumpClientFunction(void *userPortion, size_t blockSize);

Первый параметр - это указатель на блок памяти, второй - размер блока. Подключить написанную вами функцию дампа к DCRT можно следующим образом.

_CrtSetDumpClient(DumpClientFunction);

Рассмотрим небольшой пример. Допустим, в программе есть класс CPerson, содержащий поля Name (имя) и DateOfBirth (дата рождения). Требуется выводить значения этих полей в отчёте об утечках памяти. Чтобы этого добиться, необходимо:

С учётом этих требований реализация класса CPerson будет выглядеть так.

class CPerson
{
private:
    char Name[255];
    time_t DateOfBirth;

public:
    CPerson(const char *_n, time_t _d) : DateOfBirth(_d)
    {
        strcpy(Name, _n);
    }

    void Dump()
    {
        _RPT2(
            _CRT_WARN,
            " Person: %s; Date of Birth: %s",
            Name,
            ctime(&DateOfBirth)
        );
    }

    void *operator new(
        size_t size,
        int blockType,
        const char *filename,
        int linenumber
    )
    {
        return _malloc_dbg(size, _CLIENT_BLOCK, filename, linenumber);
    }
};

void DumpClientFunction(void *userPortion, size_t blockSize)
{
    CPerson *pPerson = (CPerson *)userPortion;
    pPerson->Dump();
}

Если теперь выполнить программу, вызывающую утечки памяти, например:

_CrtSetDumpClient(DumpClientFunction);
new CPerson("Vasya Pupkin", time(NULL));

то отчёт об утечках будет выглядеть так.

Detected memory leaks!
Dumping objects ->
E:\Ash\Projects\Vc6\MemDbg\MemDbg.cpp(57) : {45} client block at 0x002F1000, subtype 0, 260 bytes long.
 Person: Vasya Pupkin; Date of Birth: Wed Jan 16 00:00:20 2002
Object dump complete.

Обратите внимание, что для вывода дампа используется макрос _RPT3. Это один из целого семейства макросов _RPTx, где x - число от 0 до 4 - задаёт количество дополнительных параметров. Макросы _RPTx описаны в файле crtdbg.h и используются для диагностического вывода всеми функциями DCRT. Их работа напоминает работу функции printf. Разница состоит в том, что каждый их них получает в качестве первого параметра тип сообщения (_CRT_WARN, _CRT_ERROR или _CRT_ASSERT) и выводит сообщение в окно Debug, в файл или в отдельное окно, в зависимости от режима, заданного функцией _CrtSetReportMode.

У отладочной библиотеки есть одна странная особенность. Как я уже говорил выше, каждому клиентскому блоку можно назначить подтип. Эти подтипы было бы очень удобно использовать в функции дампа, чтобы выдавать различную информацию для блоков различных подтипов. Но никакого способа определить подтип блока DCRT не предоставляет. Чтобы решить эту проблему, придётся использовать недокументированные сведения об устройстве отладочной кучи. Вспомним, как выглядит заголовок блока в отладочной куче. Если записать его в виде C-структуры, он будет выглядеть так (описание структуры позаимствовано из файла dbgint.h.

typedef struct _CrtMemBlockHeader
{
        struct _CrtMemBlockHeader * pBlockHeaderNext;
        struct _CrtMemBlockHeader * pBlockHeaderPrev;
        char *                      szFileName;
        int                         nLine;
        size_t                      nDataSize;
        int                         nBlockUse;
        long                        lRequest;
        unsigned char               gap[4];
        /* дальше идёт:
         *  unsigned char           data[nDataSize];

         *  unsigned char           anotherGap[4];

         */
} _CrtMemBlockHeader;

Имея указатель на область данных, мы можем получить указатель на заголовок:

_CrtMemBlockHeader *pHeader = ((_CrtMemBlockHeader *)userPortion)-1;

Теперь осталось обратиться к полю типа (nBlockUse) и выделить из него подтип:

int subtype = _BLOCK_SUBTYPE(pHeader->nBlockUse)

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

void DumpClientFunction(void *userPortion, size_t blockSize)
{
    _CrtMemBlockHeader *pHeader = ((_CrtMemBlockHeader *)userPortion)-1;
    int subtype = _BLOCK_SUBTYPE(pHeader->nBlockUse);

    switch(subtype)
    {
        // Выводим дамп в зависимости от подтипа.
    }
}

Стресс-тестирование

Иногда требуется выяснить, как программа будет работать в условиях нехватки памяти. Смоделировать такую ситуацию также можно с помощью функций DCRT. Функция _CrtSetAllocHook позволяет вам задать функцию хука, которая будет вызываться перед каждым распределением памяти:

_CrtSetAllocHook(AllocHook);

Функция хука имеет следующий прототип.

int AllocHook(
    int allocType,
    void *userData,
    size_t size,
    int blockType,
    long requestNumber,
    const unsigned char *filename,
    int lineNumber
);

Первый параметр содержит тип операции с памятью, которая привела к вызову хука. Его возможные значения: _HOOK_ALLOC (распределение блока), _HOOK_REALLOC (перераспределение блока) и _HOOK_FREE (освобождение блока). Остальные параметры просто описывают блок, над которым выполняется данная операция. Функция хука возвращает TRUE, чтобы разрешить продолжение операции, или FALSE, чтобы отменить её. Тем самым моделируется неудача при вызове функции управления памятью.

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

int AllocHook(
    int allocType,
    void *userData,
    size_t size,
    int blockType, 
    long requestNumber,
    const unsigned char *filename,
    int lineNumber
)
{
    static size_t AvailableMemory = 10000;

    switch(allocType)
    {
    case _HOOK_ALLOC:
    case _HOOK_REALLOC:
        if(AvailableMemory < size)
            return FALSE; // недостаточно памяти
        else
        {
            AvailableMemory -= size;
            return TRUE;
        }

    case _HOOK_FREE:
        // Для освобождаемого блока параметр size всегда равен 0, поэтому
        // используем _msize для определения размера блока.
        AvailableMemory += _msize(userData);
    }

    return TRUE;
}

Если установить этот хук и выполнить тестовую программу, она будет вести себя, как будто в системе мало памяти:

_CrtSetAllocHook(AllocHook);

char *p1, *p2, *p3;
p1 = new char[5000];
p2 = new char[5000];
p3 = new char[5000]; // неудача

delete [] p2;
p3 = new char[5000]; // теперь блок будет выделен успешно

Мгновенные снимки памяти

Мгновенный снимок памяти запоминает текущее состояние отладочной кучи в специальной структуре _CrtMemState. Под состоянием понимается набор блоков, выделенных на данный момент. Чтобы сделать мгновенный снимок, используется функция _CrtMemCheckpoint:

_CrtMemState state;
_CrtMemCheckpoint(&state);

После этого над структурой _CrtMemState можно выполнять целый ряд операций. Во-первых, можно выдать информацию об использовании памяти в момент снимка в удобной для восприятия форме. Для этого используется функция _CrtMemDumpStatistics:

_CrtMemDumpStatistics(&state);

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

0 bytes in 0 Free Blocks.
859 bytes in 6 Normal Blocks.
5104 bytes in 43 CRT Blocks.
0 bytes in 0 Ignore Blocks.
520 bytes in 2 Client Blocks.
Largest number used: 6483 bytes.
Total allocations: 9103 bytes.

Во-вторых, можно выдать информацию обо всех блоках, которые были выделены с момента мгновенного снимка памяти. Этим занимается функция _CrtMemDumpAllObjectsSince. По умолчанию вывод также направляется в окно Debug. Наконец, два мгновенных снимка можно сравнить. Для этого предназначена функция _CrtMemDifference. Она сравнивает две структуры _CrtMemState и сохраняет результат сравнения (то есть все блоки, выделенные в промежутке между двумя снимками) в третьей. Например:

_CrtMemState st1, st2, diff;
_CrtMemCheckpoint(&st1);

// Работаем с памятью.

_CrtMemCheckpoint(&st2);
_CrtMemDifference(&diff, &st1, &st2); // сравниваем снимки

Далее результат сравнения можно анализировать, распечатывая статистику и полный список блоков, выделенных между двумя мгновенными снимками. Благодаря этой возможности можно узнать, как использует память некоторый фрагмент отлаживаемой программы. Например, можно узнать объём памяти, используемый некоторой функцией, или количество памяти, потребляемой CRT на выполнение некоторой операции.

Отладочные макросы

В заключение я хочу немного подробнее остановиться на отладочных макросах. Они широко используются функциями DCRT, но вы также можете применять их в своих программах. Семейство макросов _RPTx мы рассмотрели выше. Они осуществляют диагностический вывод в файл, в окно Debug или в отдельное окно (в зависимости от режима, заданного с помощью _CrtSetReportMode). Макросы _ASSERT и _ASSERTE позволяют проверить истинность некоторого выражения, которое передаётся им в качестве параметра. Если выражение ложно, они выдают ошибку типа _CRT_ASSERT (неверное утверждение). _ASSERTE отличается от _ASSERT тем, что включает в сообщение об ошибке проверяемое выражение.

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

Макрос TRACE позволяет MFC-приложениям выводить диагностическую информацию в окно Debug. Этот макрос используется аналогично функции printf. Суммарная длина строки, получающаяся после форматирования, не должна превышать 512 символов. Макрос TRACE принимает в качестве параметра строки в формате LPCTSTR, поэтому для правильной работы и UNICODE, и ANSI необходимо применять макрос (или _TEXT). Существуют также устаревшие макросы TRACEx (где x – число от 0 до 3), принимающие фиксированное число параметров и работающие со строками в кодировке ANSI.

В ATL для проверки выражений принято использовать макрос ATLASSERT. По большому счёту он ничем не отличается от _ASSERTE, но с точки зрения стиля лучше использовать именно его. Макрос ATLTRACE позволяет выводить в окно Debug диагностическую информацию. Он используется аналогично макросу TRACE из MFC. Однако удобнее использовать макрос ATLTRACE2, благодаря возможности фильтрации.

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


Эта статья опубликована в журнале RSDN Magazine #0. Информацию о журнале можно найти здесь