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_ALLOC
o 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;
};
int 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 del debug_new.cpp.
Nota
Non è consigliabile creare una macro del preprocessore denominata new
o 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
Impostare un punto di interruzione all'inizio dell'app e avviare il debug.
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).
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.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.
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.