Нахождение утечек памяти с помощью библиотеки CRT

Утечки памяти представляют собой наиболее незаметные и сложные для обнаружения ошибки в приложениях C/C++. Утечки памяти появляются в результате неправильного освобождения выделенной памяти. Небольшая утечка памяти сначала может остаться незамеченной, но постепенно может приводить к различным симптомам: от снижения производительности до аварийного завершения приложения из-за нехватки памяти. Приложение, в котором происходит утечка памяти, может использовать всю доступную память и привести к аварийному завершению других приложений, в результате чего может быть непонятно, какое приложение отвечает за сбой. Даже безобидная утечка памяти может быть признаком других проблем, требующих устранения.

Отладчик Visual Studio и библиотека времени выполнения C (CRT) помогают обнаруживать и выявлять утечки памяти.

Включение обнаружения утечек памяти

Основными средствами обнаружения утечек памяти являются отладчик C/C++ и функции отладочной кучи CRT.

Чтобы включить все отладочные функции кучи, вставьте в программу C++ следующие операторы в следующем порядке:

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

Оператор #define сопоставляет базовые версии функций кучи CRT соответствующим отладочным версиям. Если оператор #define не используется, дамп утечки памяти будет менее подробным.

Включая crtdbg.h , сопоставляет malloc и free функции с их отладочными версиями, _malloc_dbg а также _free_dbgотслеживает выделение памяти и распределение сделки. Это сопоставление используется только в отладочных построениях, в которых определен _DEBUG. В окончательных построениях используются первоначальные функции malloc и free .

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

_CrtDumpMemoryLeaks();

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

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

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

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

_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_DEBUG );

В следующем примере показана простая утечка памяти и отображается информация об утечке памяти с помощью _CrtDumpMemoryLeaks();.

// debug_malloc.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_malloc.cpp
#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>
#include <iostream>

int main()
{
    std::cout << "Hello World!\n";

    int* x = (int*)malloc(sizeof(int));

    *x = 7;

    printf("%d\n", *x);

    x = (int*)calloc(3, sizeof(int));
    x[0] = 7;
    x[1] = 77;
    x[2] = 777;

    printf("%d %d %d\n", x[0], x[1], x[2]);

    _CrtSetReportMode(_CRT_WARN, _CRTDBG_MODE_DEBUG); 
    _CrtDumpMemoryLeaks();
}

Интерпретация отчета об утечке памяти

Если приложение не определяет _CRTDBG_MAP_ALLOC, _CrtDumpMemoryLeaks отображает отчет об утечке памяти, аналогичный следующему:

Detected memory leaks!
Dumping objects ->
{18} normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

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

Detected memory leaks!
Dumping objects ->
c:\users\username\documents\projects\leaktest\leaktest.cpp(20) : {18}
normal block at 0x00780E80, 64 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.

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

Независимо от того, определен ли _CRTDBG_MAP_ALLOC, в отчете об утечке памяти отображается следующее.

  • Номер выделения памяти, в этом примере — 18.
  • Тип блока, в примере — normal.
  • Расположение памяти в шестнадцатеричном формате, в этом примере — 0x00780E80.
  • Размер блока, в этом примере — 64 bytes.
  • Первые 16 байт данных в блоке, в шестнадцатеричном формате.

Типы блоков памяти: обычные, клиентские или CRT. Обычный блок — это обыкновенная память, выделенная программой. Клиентский блок — особый тип блока памяти, используемой программами MFC для объектов, для которых требуется деструктор. Оператор new в MFC создает либо обычный, либо клиентский блок, в соответствии с создаваемым объектом.

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

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

Предыдущие способы выявляют утечки памяти для памяти, выделенной с помощью стандартной функции malloc библиотеки CRT. Однако если программа выделяет память с использованием оператора new C++, то в отчете об утечке памяти вы увидите только имя файла и номер строки, где operator new вызывает _malloc_dbg. Чтобы создать более полезный отчет об утечке памяти, можно написать макрос следующего вида, и в отчете будет указываться строка, в которой было выполнено выделение:

#ifdef _DEBUG
    #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
    // Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
    // allocations to be of _CLIENT_BLOCK type
#else
    #define DBG_NEW new
#endif

Теперь можно заменить оператор new с помощью макроса DBG_NEW в коде. В отладочных сборках DBG_NEW используется перегрузка глобального уровня operator new , которая принимает дополнительные параметры для типа блока, файла и номера строки. Перегрузка new вызывает _malloc_dbg для записи дополнительных сведений. Отчеты об утечке памяти показывают имя файла и номер строки, в которой были выделены утечки объектов. Сборки выпуска по-прежнему используют newпо умолчанию. Вот пример этого метода:

// debug_new.cpp
// compile by using: cl /EHsc /W4 /D_DEBUG /MDd debug_new.cpp
#define _CRTDBG_MAP_ALLOC
#include <cstdlib>
#include <crtdbg.h>

#ifdef _DEBUG
    #define DBG_NEW new ( _NORMAL_BLOCK , __FILE__ , __LINE__ )
    // Replace _NORMAL_BLOCK with _CLIENT_BLOCK if you want the
    // allocations to be of _CLIENT_BLOCK type
#else
    #define DBG_NEW new
#endif

struct Pod {
    int x;
};

void main() {
    Pod* pPod = DBG_NEW Pod;
    pPod = DBG_NEW Pod; // Oops, leaked the original pPod!
    delete pPod;

    _CrtDumpMemoryLeaks();
}

При выполнении этого кода в отладчике Visual Studio вызов _CrtDumpMemoryLeaks создает отчет в окне вывода, который выглядит аналогичным образом:

Detected memory leaks!
Dumping objects ->
c:\users\username\documents\projects\debug_new\debug_new.cpp(20) : {75}
 normal block at 0x0098B8C8, 4 bytes long.
 Data: <    > CD CD CD CD
Object dump complete.

Эти выходные данные сообщают о том, что утечка памяти находилась на строке 20 файла debug_new.cpp.

Примечание.

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

Задание точек останова для номера выделения памяти

Номер выделения памяти сообщает, когда был выделен утекающий блок памяти. Например, блок с номером выделения памяти 18 — это 18-й блок памяти, выделенный во время выполнения программы. Отчет CRT учитывает все выделения блоков памяти во время выполнения, включая выделение памяти библиотекой CRT и другими библиотеками, такими как MFC. Поэтому блок с номером выделения памяти 18 может не быть 18-м блоком памяти, выделенным вашим кодом.

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

Установка точки останова для выделения памяти с помощью окна контрольных значений

  1. Установите точку останова рядом с началом приложения и запустите отладку.

  2. Когда приложение приостанавливается в точке останова, откройте окно Контрольные значения, последовательно выбрав пункты Отладка>Windows>Контрольные значения 1 (или Контрольные значения 2, Контрольные значения 3 или Контрольные значения 4).

  3. В окне Контрольные значения введите _crtBreakAlloc в столбце Имя.

    Если используется многопоточная версия DLL-библиотеки CRT (параметр /MD), добавьте контекстный оператор: {,,ucrtbased.dll}_crtBreakAlloc

    Убедитесь, что отладочные символы загружены. _crtBreakAlloc В противном случае сообщается как неопознанный.

  4. Нажмите ВВОД.

    Отладчик выполнит оценку вызова и поместит результат в столбец Значение . Это значение равно -1 , если вы не установили точки останова для выделения памяти.

  5. В столбце Значение замените отображаемое значение номером выделения памяти, на котором нужно приостановить выполнение.

После задания точки останова для номера выделения памяти можно продолжить отладку. Убедитесь, что соблюдаются те же условия, чтобы номер выделения памяти не изменился. Когда выполнение программы будет приостановлено на заданном выделении памяти, с помощью окна Стек вызовов и других окон отладчика определите условия выделения памяти. Затем можно продолжить выполнение программы и проследить, что происходит с этим объектом и почему выделенная ему память освобождается неправильно.

Иногда может быть полезно задать точку останова по данным на самом объекте. Для получения дополнительной информации см. раздел Использование точек останова.

Точки останова для выделения памяти можно также задать в коде. Можно установить следующие значения:

_crtBreakAlloc = 18;

или:

_CrtSetBreakAlloc(18);

Сравнение состояний памяти

Другая технология для обнаружения утечек памяти включает получение "снимков" состояния памяти приложения в ключевых точках. Чтобы получить снимок состояния памяти в заданной точке приложения, создайте структуру _CrtMemState и передайте ее функции _CrtMemCheckpoint.

_CrtMemState s1;
_CrtMemCheckpoint( &s1 );

Функция _CrtMemCheckpoint поместит в структуру снимок текущего состояния памяти.

Чтобы вывести содержимое структуры _CrtMemState, передайте ее функции _ CrtMemDumpStatistics:

_CrtMemDumpStatistics( &s1 );

Функция_CrtMemDumpStatistics выводит дамп состояния памяти, который выглядит примерно таким образом:

0 bytes in 0 Free Blocks.
0 bytes in 0 Normal Blocks.
3071 bytes in 16 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 3071 bytes.
Total allocations: 3764 bytes.

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

_CrtMemCheckpoint( &s1 );
// memory allocations take place here
_CrtMemCheckpoint( &s2 );

if ( _CrtMemDifference( &s3, &s1, &s2) )
   _CrtMemDumpStatistics( &s3 );

Функция_CrtMemDifference сравнивает состояния памяти s1 и s2 и возвращает результат в (s3), представляющий собой разницу между s1 и s2.

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

Ложные срабатывания

_CrtDumpMemoryLeaks может дать ложные признаки утечки памяти, если библиотека помечает внутренние выделения как обычные блоки, а не блоки CRT или клиентские блоки. В таком случае функция _CrtDumpMemoryLeaks не может различать пользовательские выделения и внутренние выделения библиотеки. Если глобальные деструкторы для выделений библиотеки выполняются после точки вызова функции _CrtDumpMemoryLeaks, каждое внутреннее выделение библиотеки принимается за утечку памяти. Версии библиотеки стандартных шаблонов, предшествовавшие Visual Studio .NET, приводили к тому, что функция _CrtDumpMemoryLeaks сообщала о таких ложных утечках.

См. также