Individuare le perdite di memoria con la libreria CRT

Le perdite di memoria sono tra i bug più sottili e difficili da rilevare nelle app C/C++. Le perdite di memoria derivano dall'errore di deallocare correttamente la memoria allocata in precedenza. Una piccola perdita di memoria potrebbe non essere notato all'inizio, ma nel tempo può causare sintomi che vanno da prestazioni scarse a arresto anomalo quando l'app esaurisce la memoria. Un'app che usa tutta la memoria disponibile può causare l'arresto anomalo di altre app, creando confusione per quanto riguarda l'app responsabile. Anche le perdite di memoria innocue potrebbero indicare altri problemi che devono essere corretti.

Il debugger di Visual Studio e la libreria C Runtime (CRT) consentono di rilevare e identificare le perdite di memoria.

Abilitare il rilevamento della perdita di memoria

Gli strumenti principali per rilevare le perdite di memoria sono il debugger C/C++ e le funzioni heap di debug CRT.

Per abilitare tutte le funzioni heap di debug, includere le istruzioni seguenti nel programma C++, nell'ordine seguente:

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

L'istruzione #define esegue il mapping di una versione di base delle funzioni di heap CRT alla corrispondente versione di debug. Se si esce dall'istruzione #define , il dump della perdita di memoria sarà meno dettagliato.

L'inclusione di crtdbg.h esegue il mapping delle malloc funzioni e free alle versioni _malloc_dbg di debug e _free_dbg, che tiene traccia dell'allocazione della memoria e della deallocazione. Questa operazione di mapping viene eseguita solo nelle build di debug che presentano _DEBUG. Le build di rilascio usano le normali funzioni malloc e free .

Dopo aver abilitato le funzioni heap di debug usando le istruzioni precedenti, effettuare una chiamata a _CrtDumpMemoryLeaks prima di un punto di uscita dell'app per visualizzare un report di perdita di memoria quando l'app viene chiusa.

_CrtDumpMemoryLeaks();

Se l'app ha diverse uscite, non è necessario posizionare _CrtDumpMemoryLeaks manualmente in ogni punto di uscita. Per fare in modo che una chiamata automatica a _CrtDumpMemoryLeaks a ogni punto di uscita, inserire una chiamata a _CrtSetDbgFlag all'inizio dell'app con i campi di bit mostrati di seguito:

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

Per impostazione predefinita, _CrtDumpMemoryLeaks genera il report delle perdite di memoria nel riquadro Debug della finestra Output . Se si usa una libreria, è possibile che l'output venga indirizzato in un'altra posizione.

È possibile usare _CrtSetReportMode per reindirizzare il report a un'altra posizione o tornare alla finestra Output , come illustrato di seguito:

_CrtSetReportMode( _CRT_WARN, _CRTDBG_MODE_DEBUG );

Nell'esempio seguente viene illustrata una semplice perdita di memoria e vengono visualizzate informazioni sulla perdita di memoria usando _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();
}

Interpretare il report di perdita di memoria

Se l'app non definisce _CRTDBG_MAP_ALLOC, _CrtDumpMemoryLeaks visualizza un report di perdita di memoria simile al seguente:

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.

Se l'app definisce _CRTDBG_MAP_ALLOC, il report di perdita di memoria è simile al seguente:

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.

Il secondo report mostra il nome file e il numero di riga in cui la memoria persa viene allocata per la prima volta.

Indica se si definisce _CRTDBG_MAP_ALLOCo meno , viene visualizzato il report di perdita di memoria:

  • Numero di allocazione della memoria, che si trova 18 nell'esempio
  • Tipo di blocco, normal nell'esempio.
  • Percorso di memoria esadecimale, 0x00780E80 nell'esempio.
  • Dimensione del blocco, 64 bytes nell'esempio.
  • I primi 16 byte di dati nel blocco, in formato esadecimale.

I tipi di blocchi di memoria sono normali, client o CRT. Un blocco normale è dato da memoria ordinaria allocata dal programma. Un blocco client è un tipo speciale di blocco di memoria usato dai programmi MFC per oggetti che richiedono un distruttore. L'operazione MFC new crea un blocco normale o un blocco client, a seconda dell'oggetto creato.

Un blocco CRT è allocato dalla libreria CRT per il proprio uso. La libreria CRT gestisce la deallocazione per questi blocchi, quindi i blocchi CRT non verranno visualizzati nel report di perdita di memoria, a meno che non si verifichino gravi problemi con la libreria CRT.

Esistono altri due tipi di blocchi di memoria che non compaiono mai nei report delle perdite di memoria. Un blocco libero è la memoria rilasciata, quindi per definizione non viene persa. Un blocco ignore è la memoria contrassegnata in modo esplicito da escludere dal report di perdita di memoria.

Le tecniche precedenti identificano le perdite di memoria per la memoria allocata usando la funzione CRT malloc standard. Se il programma alloca memoria usando l'operatore C++ new , tuttavia, è possibile visualizzare solo il nome file e il numero di riga in cui operator new le chiamate _malloc_dbg nel report di perdita di memoria. Per creare un report di perdita di memoria più utile, è possibile scrivere una macro simile alla seguente per segnalare la riga che ha effettuato l'allocazione:

#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

È ora possibile sostituire l'operatore new usando la DBG_NEW macro nel codice. Nelle compilazioni DBG_NEW di debug usa un overload globale operator new che accetta parametri aggiuntivi per il tipo di blocco, il file e il numero di riga. Overload delle new chiamate _malloc_dbg per registrare le informazioni aggiuntive. I report di perdita di memoria mostrano il nome file e il numero di riga in cui sono stati allocati gli oggetti persi. Le build di versione usano ancora il valore predefinito new. Ecco un esempio della tecnica:

// 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();
}

Quando si esegue questo codice nel debugger di Visual Studio, la chiamata a _CrtDumpMemoryLeaks genera un report nella finestra Output simile a:

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.

Questo output segnala che l'allocazione persa era alla riga 20 di debug_new.cpp.

Nota

Non è consigliabile creare una macro del preprocessore denominata newo qualsiasi altra parola chiave del linguaggio.

Impostare punti di interruzione su un numero di allocazione di memoria

Il numero di allocazione della memoria indica quando è stato allocato un blocco di memoria di cui è stata registrata la perdita. Un blocco con un numero di allocazione di memoria pari a 18, ad esempio, è il 18° blocco di memoria allocato durante l'esecuzione dell'app. Il report CRT conta tutte le allocazioni di blocchi di memoria durante l'esecuzione, incluse le allocazioni dalla libreria CRT e altre librerie, ad esempio MFC. Pertanto, il numero di blocco di allocazione della memoria 18 probabilmente non è il 18° blocco di memoria allocato dal codice.

È possibile usare il numero di allocazione per impostare un punto di interruzione sull'allocazione di memoria.

Per impostare un punto di interruzione dell'allocazione di memoria usando la finestra Espressioni di controllo

  1. Impostare un punto di interruzione all'inizio dell'app e avviare il debug.

  2. Quando l'app viene sospesa nel punto di interruzione, aprire una finestra Espressione di controllo selezionando Debug>Windows>Watch 1 (o Espressione di controllo 2, Espressione di controllo 3 o Espressione di controllo 4).

  3. Nella finestra Espressione di controllo digitare _crtBreakAlloc nella colonna Nome.

    Se si usa la versione DLL multithreading della libreria CRT (opzione /MD), aggiungere l'operatore di contesto: {,,ucrtbased.dll}_crtBreakAlloc

    Assicurarsi che i simboli di debug siano caricati. In caso contrario, _crtBreakAlloc viene segnalato come non identificato.

  4. Premere INVIO.

    Il debugger valuterà la chiamata e ne visualizzerà il risultato nella colonna Valore . Questo valore è -1 se non sono stati impostati punti di interruzione nelle allocazioni di memoria.

  5. Nella colonna Valore sostituire il valore con il numero di allocazione dell'allocazione di memoria in cui si vuole interrompere il debugger.

Dopo aver impostato un punto di interruzione su un numero di allocazione di memoria, continuare a eseguire il debug. Assicurarsi di eseguire con le stesse condizioni, quindi il numero di allocazione della memoria non cambia. Quando il programma si interrompe con l'allocazione di memoria specificata, usare la finestra Stack di chiamate e altre finestre del debugger per determinare le condizioni in cui è stata allocata la memoria. È quindi possibile continuare l'esecuzione per osservare cosa accade all'oggetto e determinare il motivo per cui non è stato deallocato correttamente.

Anche l'impostazione di un punto di interruzione dei dati sull'oggetto può essere utile. Per altre informazioni, vedere Uso di punti di interruzione.

È anche possibile impostare i punti di interruzione dell'allocazione di memoria nel codice. Tra cui:

_crtBreakAlloc = 18;

oppure:

_CrtSetBreakAlloc(18);

Confrontare gli stati di memoria

Un'altra tecnica per l'individuazione delle perdite di memoria comporta l'esecuzione di snapshot dello stato della memoria dell'applicazione in corrispondenza di punti chiave. Per creare uno snapshot dello stato di memoria in un determinato punto dell'applicazione, creare una _CrtMemState struttura e passarla alla _CrtMemCheckpoint funzione.

_CrtMemState s1;
_CrtMemCheckpoint( &s1 );

La _CrtMemCheckpoint funzione riempie la struttura con uno snapshot dello stato di memoria corrente.

Per restituire il contenuto di una _CrtMemState struttura, passare la struttura alla _ CrtMemDumpStatistics funzione :

_CrtMemDumpStatistics( &s1 );

_CrtMemDumpStatistics restituisce un dump dello stato di memoria simile al seguente:

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.

Per verificare se si è verificata una perdita di memoria in una sezione di codice, è possibile eseguire snapshot dello stato della memoria prima e dopo la sezione, quindi usare _CrtMemDifference per confrontare i due stati:

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

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

_CrtMemDifference confronta gli stati s1 di memoria e s2 restituisce un risultato (s3) che rappresenta la differenza tra s1 e s2.

Una tecnica per trovare perdite di memoria inizia inserendo _CrtMemCheckpoint chiamate all'inizio e alla fine dell'app, quindi usando _CrtMemDifference per confrontare i risultati. Se _CrtMemDifference viene visualizzata una perdita di memoria, è possibile aggiungere altre _CrtMemCheckpoint chiamate per dividere il programma usando una ricerca binaria, fino a quando non si è isolata l'origine della perdita.

Falsi positivi

_CrtDumpMemoryLeaks può fornire false indicazioni sulle perdite di memoria se una libreria contrassegna le allocazioni interne come blocchi normali anziché blocchi CRT o blocchi client. In questo caso, _CrtDumpMemoryLeaks non è in grado di indicare la differenza tra allocazioni utente e allocazioni interne della libreria. Se i distruttori globali relativi alle allocazioni della libreria vengono eseguiti dopo il punto in cui viene chiamato _CrtDumpMemoryLeaks, ogni allocazione interna della libreria viene segnalata come perdita di memoria. Le versioni della libreria di modelli standard precedenti a Visual Studio .NET possono causare _CrtDumpMemoryLeaks la segnalazione di falsi positivi.

Vedi anche