Techniki testowania MFC
Jeśli debugujesz program MFC, te techniki debugowania mogą być przydatne.
W tym temacie
Wykrywanie przecieków pamięci w MFC
AfxDebugBreak
MFC udostępnia specjalną funkcję AfxDebugBreak dla punktów przerwania kodowania na stałe w kodzie źródłowym:
AfxDebugBreak( );
Na platformach AfxDebugBreak
Intel tworzy następujący kod, który łamie kod źródłowy, a nie kod jądra:
_asm int 3
Na innych platformach AfxDebugBreak
tylko wywołuje metodę DebugBreak
.
Pamiętaj, aby usunąć AfxDebugBreak
instrukcje podczas tworzenia kompilacji wydania lub używania #ifdef _DEBUG
ich do otoki.
Makro TRACE
Aby wyświetlić komunikaty z programu w oknie dane wyjściowe debugera, możesz użyć makra ATLTRACE lub makra MFC TRACE. Podobnie jak asercji, makra śledzenia są aktywne tylko w wersji debugowania programu i znikają po skompilowaniu w wersji wydania.
W poniższych przykładach przedstawiono niektóre sposoby użycia makra TRACE . Podobnie jak printf
, makro TRACE może obsługiwać wiele argumentów.
int x = 1;
int y = 16;
float z = 32.0;
TRACE( "This is a TRACE statement\n" );
TRACE( "The value of x is %d\n", x );
TRACE( "x = %d and y = %d\n", x, y );
TRACE( "x = %d and y = %x and z = %f\n", x, y, z );
Makro TRACE odpowiednio obsługuje parametry char* i wchar_t*. W poniższych przykładach pokazano użycie makra TRACE wraz z różnymi typami parametrów ciągu.
TRACE( "This is a test of the TRACE macro that uses an ANSI string: %s %d\n", "The number is:", 2);
TRACE( L"This is a test of the TRACE macro that uses a UNICODE string: %s %d\n", L"The number is:", 2);
TRACE( _T("This is a test of the TRACE macro that uses a TCHAR string: %s %d\n"), _T("The number is:"), 2);
Aby uzyskać więcej informacji na temat makra TRACE , zobacz Usługi diagnostyczne.
Wykrywanie przecieków pamięci w MFC
MFC udostępnia klasy i funkcje do wykrywania pamięci przydzielonej, ale nigdy nie cofnięto przydziału.
Śledzenie alokacji pamięci
W MFC można użyć DEBUG_NEW makra zamiast nowego operatora, aby ułatwić lokalizowanie przecieków pamięci. W wersji debugowania programu DEBUG_NEW
śledzi nazwę pliku i numer wiersza dla każdego przydzielanego obiektu. Podczas kompilowania wersji programu DEBUG_NEW
jest rozpoznawana prosta nowa operacja bez informacji o nazwie pliku i numerze wiersza. W związku z tym nie płacisz żadnej szybkiej kary w wersji wydania programu.
Jeśli nie chcesz ponownie pisać całego programu DEBUG_NEW
zamiast nowego, możesz zdefiniować to makro w plikach źródłowych:
#define new DEBUG_NEW
Podczas zrzutu obiektu każdy przydzielony DEBUG_NEW
obiekt będzie wyświetlać plik i numer wiersza, w którym został przydzielony, co pozwala wskazać źródła przecieków pamięci.
Wersja debugowania platformy MFC jest używana DEBUG_NEW
automatycznie, ale kod nie jest używany. Jeśli chcesz korzystać z DEBUG_NEW
usługi , musisz użyć DEBUG_NEW
jawnie lub #define nowe , jak pokazano powyżej.
Włączanie diagnostyki pamięci
Przed rozpoczęciem korzystania z funkcji diagnostyki pamięci należy włączyć śledzenie diagnostyczne.
Aby włączyć lub wyłączyć diagnostykę pamięci
Wywołaj funkcję globalną AfxEnableMemoryTracking , aby włączyć lub wyłączyć alokator pamięci diagnostycznej. Ponieważ diagnostyka pamięci jest domyślnie włączona w bibliotece debugowania, zazwyczaj ta funkcja jest używana do tymczasowego wyłączania, co zwiększa szybkość wykonywania programu i zmniejsza liczbę danych wyjściowych diagnostyki.
Aby wybrać określone funkcje diagnostyczne pamięci za pomocą funkcji afxMemDF
Jeśli chcesz dokładniej kontrolować funkcje diagnostyczne pamięci, możesz selektywnie włączyć i wyłączyć poszczególne funkcje diagnostyczne pamięci, ustawiając wartość zmiennej globalnej MFC afxMemDF. Ta zmienna może mieć następujące wartości określone przez wyliczony typ afxMemDF.
Wartość Opis allocMemDF Włącz alokator pamięci diagnostycznej (ustawienie domyślne). delayFreeMemDF Opóźnij zwalnianie pamięci podczas wywoływania delete
lubfree
zamykania programu. Spowoduje to przydzielenie maksymalnej możliwej ilości pamięci przez program.checkAlwaysMemDF Wywołaj metodę AfxCheckMemory za każdym razem, gdy pamięć jest przydzielana lub zwalniana. Te wartości można używać w połączeniu, wykonując operację logiczną OR, jak pokazano poniżej:
afxMemDF = allocMemDF | delayFreeMemDF | checkAlwaysMemDF;
Tworzenie migawek pamięci
Utwórz obiekt CMemoryState i wywołaj funkcję elementu członkowskiego CMemoryState::Checkpoint. Spowoduje to utworzenie pierwszej migawki pamięci.
Po wykonaniu przez program operacji alokacji pamięci i cofania przydziału utwórz inny
CMemoryState
obiekt i wywołajCheckpoint
dla tego obiektu. Spowoduje to pobranie drugiej migawki użycia pamięci.Utwórz trzeci
CMemoryState
obiekt i wywołaj jego funkcję składową CMemoryState::D ifference , podając jako argumenty dwóch poprzednichCMemoryState
obiektów. Jeśli istnieje różnica między dwoma stanami pamięci,Difference
funkcja zwraca wartość niezerową. Oznacza to, że niektóre bloki pamięci nie zostały cofnięto przydziału.W tym przykładzie pokazano, jak wygląda kod:
// Declare the variables needed #ifdef _DEBUG CMemoryState oldMemState, newMemState, diffMemState; oldMemState.Checkpoint(); #endif // Do your memory allocations and deallocations. CString s("This is a frame variable"); // The next object is a heap object. CPerson* p = new CPerson( "Smith", "Alan", "581-0215" ); #ifdef _DEBUG newMemState.Checkpoint(); if( diffMemState.Difference( oldMemState, newMemState ) ) { TRACE( "Memory leaked!\n" ); } #endif
Zwróć uwagę, że instrukcje sprawdzania pamięci są w nawiasach #ifdef _DEBUG / #endif bloków, aby były kompilowane tylko w wersjach debugowania programu.
Teraz, gdy już wiesz, że istnieje przeciek pamięci, możesz użyć innej funkcji składowej CMemoryState ::D umpStatistics , która pomoże Ci go zlokalizować.
Wyświetlanie statystyk pamięci
Funkcja CMemoryState::D ifference analizuje dwa obiekty stanu pamięci i wykrywa wszystkie obiekty, które nie są cofnięty z sterty między stanami początkowymi i końcowymi. Po wykonaniu migawek pamięci i porównaniu ich przy użyciu CMemoryState::Difference
polecenia można wywołać metodę CMemoryState::D umpStatistics , aby uzyskać informacje o obiektach, które nie zostały cofnięte.
Rozważmy następujący przykład:
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!\n" );
diffMemState.DumpStatistics();
}
Przykładowy zrzut z przykładu wygląda następująco:
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
Bloki wolne to bloki, których cofanie przydziału jest opóźnione, jeśli afxMemDF
ustawiono wartość delayFreeMemDF
.
Zwykłe bloki obiektów, wyświetlane w drugim wierszu, pozostają przydzielone na stercie.
Bloki niezwiązane z obiektami obejmują tablice i struktury przydzielone za pomocą new
polecenia . W tym przypadku na stercie przydzielono cztery bloki niezwiązane z obiektem, ale nie cofnięto przydziału.
Largest number used
daje maksymalną ilość pamięci używanej przez program w dowolnym momencie.
Total allocations
daje całkowitą ilość pamięci używanej przez program.
Pobieranie zrzutów obiektów
W programie MFC można użyć CMemoryState::D umpAllObjectsSince , aby zrzucić opis wszystkich obiektów na stercie, które nie zostały cofnięty przydział. DumpAllObjectsSince
zrzuty wszystkich obiektów przydzielonych od ostatniego CMemoryState::Checkpoint. Jeśli nie Checkpoint
zostało wykonane żadne wywołanie, DumpAllObjectsSince
zrzuty wszystkich obiektów i obiektów innych niż obiekty znajdujące się obecnie w pamięci.
Uwaga
Aby można było użyć dumpingu obiektu MFC, należy włączyć śledzenie diagnostyczne.
Uwaga
MFC automatycznie zrzutuje wszystkie wycieki obiektów po zakończeniu programu, więc nie trzeba tworzyć kodu w celu zrzutu obiektów w tym momencie.
Poniższy kod testuje przeciek pamięci, porównując dwa stany pamięci i zrzuty wszystkich obiektów, jeśli zostanie wykryty wyciek.
if( diffMemState.Difference( oldMemState, newMemState ) )
{
TRACE( "Memory leaked!\n" );
diffMemState.DumpAllObjectsSince();
}
Zawartość zrzutu wygląda następująco:
Dumping objects ->
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long
Liczby w nawiasach klamrowych na początku większości wierszy określają kolejność przydzielania obiektów. Ostatnio przydzielony obiekt ma największą liczbę i pojawia się w górnej części zrzutu.
Aby uzyskać maksymalną ilość informacji z zrzutu obiektu, można zastąpić Dump
funkcję składową dowolnego CObject
obiektu pochodnego w celu dostosowania zrzutu obiektu.
Punkt przerwania można ustawić dla określonej alokacji pamięci, ustawiając zmienną _afxBreakAlloc
globalną na liczbę pokazaną w nawiasach klamrowych. Jeśli uruchomisz ponownie program, debuger przerwi wykonywanie po zakończeniu tej alokacji. Następnie możesz przyjrzeć się stosowi wywołań, aby zobaczyć, jak twój program dotarł do tego punktu.
Biblioteka języka C w czasie wykonywania ma podobną funkcję, _CrtSetBreakAlloc, której można użyć na potrzeby alokacji czasu wykonywania języka C.
Interpretowanie zrzutów pamięci
Przyjrzyj się zrzutowi tego obiektu bardziej szczegółowo:
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
{1} strcore.cpp(80) : non-object block at $00A7516E, 25 bytes long
Program, który wygenerował ten zrzut, miał tylko dwie jawne alokacje — jeden na stosie i jeden na stercie:
// Do your memory allocations and deallocations.
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
Konstruktor CPerson
przyjmuje trzy argumenty, które są wskaźnikami do char
, które są używane do inicjowania CString
zmiennych składowych. W zrzucie pamięci można zobaczyć CPerson
obiekt wraz z trzema blokami nieobiektowymi (3, 4 i 5). Przechowują one znaki zmiennych CString
składowych i nie zostaną usunięte podczas wywoływania destruktora CPerson
obiektów.
Blok numer 2 jest samym obiektem CPerson
. $51A4
reprezentuje adres bloku i następuje po nim zawartość obiektu, która była wyjściowa przez CPerson
::Dump
po wywołaniu przez DumpAllObjectsSince.
Można odgadnąć, że numer bloku 1 jest skojarzony ze zmienną ramki ze CString
względu na jego numer sekwencji i rozmiar, który odpowiada liczbie znaków w zmiennej ramki CString
. Zmienne przydzielone na ramce są automatycznie cofane, gdy ramka wykracza poza zakres.
Zmienne ramki
Ogólnie rzecz biorąc, nie należy martwić się o obiekty sterty skojarzone ze zmiennymi ramki, ponieważ są one automatycznie cofane, gdy zmienne ramki wykraczają poza zakres. Aby uniknąć bałaganu w zrzutach diagnostycznych pamięci, należy umieścić wywołania tak Checkpoint
, aby były poza zakresem zmiennych ramek. Na przykład umieść nawiasy zakresu wokół poprzedniego kodu alokacji, jak pokazano poniżej:
oldMemState.Checkpoint();
{
// Do your memory allocations and deallocations ...
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
}
newMemState.Checkpoint();
Przy użyciu nawiasów zakresu zrzut pamięci dla tego przykładu jest następujący:
Dumping objects ->
{5} strcore.cpp(80) : non-object block at $00A7521A, 9 bytes long
{4} strcore.cpp(80) : non-object block at $00A751F8, 5 bytes long
{3} strcore.cpp(80) : non-object block at $00A751D6, 6 bytes long
{2} a CPerson at $51A4
Last Name: Smith
First Name: Alan
Phone #: 581-0215
Alokacje nieobiektu
Zauważ, że niektóre alokacje są obiektami (takimi jak CPerson
) i niektóre są alokacjami nieobiektowymi. "Alokacje nieobiektu" to alokacje obiektów, które nie pochodzą z CObject
lub alokacji typów pierwotnych C, takich jak char
, int
lub long
. Jeśli klasa pochodna CObject przydziela dodatkowe miejsce, takie jak dla buforów wewnętrznych, obiekty te będą pokazywać alokacje obiektów i nieobiektów.
Zapobieganie wyciekom pamięci
Zwróć uwagę, że w powyższym kodzie blok pamięci skojarzony ze CString
zmienną ramki został automatycznie cofnięty i nie jest wyświetlany jako przeciek pamięci. Automatyczna alokacja skojarzona z regułami określania zakresu zajmuje się większością przecieków pamięci skojarzonych ze zmiennymi ramek.
W przypadku obiektów przydzielonych na stercie należy jednak jawnie usunąć obiekt, aby zapobiec wyciekowi pamięci. Aby wyczyścić ostatni wyciek pamięci w poprzednim przykładzie, usuń CPerson
obiekt przydzielony na stercie w następujący sposób:
{
// Do your memory allocations and deallocations.
CString s("This is a frame variable");
// The next object is a heap object.
CPerson* p = new CPerson( "Smith", "Alan", "581-0215" );
delete p;
}
Dostosowywanie zrzutów obiektów
W przypadku utworzenia klasy z obiektu CObject można zastąpić Dump
funkcję składową, aby podać dodatkowe informacje podczas używania funkcji DumpAllObjectsSince do zrzutu obiektów do okna Dane wyjściowe.
Funkcja Dump
zapisuje tekstową reprezentację zmiennych składowych obiektu w kontekście zrzutu (CDumpContext). Kontekst zrzutu jest podobny do strumienia we/wy. Możesz użyć operatora dołączania (<<), aby wysłać dane do elementu CDumpContext
.
Po zastąpieniu Dump
funkcji należy najpierw wywołać wersję Dump
klasy bazowej, aby zrzucić zawartość obiektu klasy bazowej. Następnie wyprowadź tekstowy opis i wartość dla każdej zmiennej składowej klasy pochodnej.
Deklaracja Dump
funkcji wygląda następująco:
class CPerson : public CObject
{
public:
#ifdef _DEBUG
virtual void Dump( CDumpContext& dc ) const;
#endif
CString m_firstName;
CString m_lastName;
// And so on...
};
Ponieważ dumping obiektu ma sens tylko podczas debugowania programu, deklaracja Dump
funkcji jest nawiasem kwadratowym z blokiem #ifdef _DEBUG/#endif .
W poniższym przykładzie Dump
funkcja najpierw wywołuje funkcję dla swojej klasy bazowej Dump
. Następnie zapisuje krótki opis każdej zmiennej składowej wraz z wartością elementu członkowskiego do strumienia diagnostycznego.
#ifdef _DEBUG
void CPerson::Dump( CDumpContext& dc ) const
{
// Call the base class function first.
CObject::Dump( dc );
// Now do the stuff for our specific class.
dc << "last name: " << m_lastName << "\n"
<< "first name: " << m_firstName << "\n";
}
#endif
Musisz podać CDumpContext
argument, aby określić miejsce, w którym zostaną wyświetlone dane wyjściowe zrzutu. Wersja debugowania MFC dostarcza wstępnie zdefiniowany CDumpContext
obiekt o nazwie afxDump
, który wysyła dane wyjściowe do debugera.
CPerson* pMyPerson = new CPerson;
// Set some fields of the CPerson object.
//...
// Now dump the contents.
#ifdef _DEBUG
pMyPerson->Dump( afxDump );
#endif
Zmniejszenie rozmiaru kompilacji debugowania MFC
Informacje debugowania dla dużej aplikacji MFC mogą zająć dużo miejsca na dysku. Aby zmniejszyć rozmiar, można użyć jednej z tych procedur:
Skompiluj biblioteki MFC przy użyciu opcji /Z7, /Zi, /ZI (Format informacji debugowania) zamiast /Z7. Te opcje tworzą pojedynczy plik bazy danych programu (PDB), który zawiera informacje debugowania dla całej biblioteki, zmniejszając nadmiarowość i oszczędność miejsca.
Ponownie skompiluj biblioteki MFC bez informacji debugowania (brak opcji /Z7, /Zi, /ZI (Format informacji debugowania). W takim przypadku brak informacji o debugowaniu uniemożliwi korzystanie z większości obiektów debugera w kodzie biblioteki MFC, ale ponieważ biblioteki MFC są już dokładnie debugowane, może to nie być problem.
Utwórz własną aplikację przy użyciu informacji debugowania dla wybranych modułów tylko zgodnie z poniższym opisem.
Kompilowanie aplikacji MFC z informacjami o debugowaniu dla wybranych modułów
Kompilowanie wybranych modułów za pomocą bibliotek debugowania MFC umożliwia korzystanie z kroków i innych obiektów debugowania w tych modułach. Ta procedura korzysta zarówno z konfiguracji debugowania, jak i wydania projektu, co wymaga zmian opisanych w poniższych krokach (a także tworzenia "ponownej kompilacji", gdy wymagana jest pełna kompilacja wydania).
W Eksplorator rozwiązań wybierz projekt.
Z menu Widok wybierz pozycję Strony właściwości.
Najpierw utworzysz nową konfigurację projektu.
<W oknie dialogowym Strony właściwości projektu> kliknij przycisk Configuration Manager.
W oknie dialogowym Configuration Manager znajdź projekt w siatce. W kolumnie Konfiguracja wybierz pozycję <Nowy...>.
W oknie dialogowym Nowa konfiguracja projektu wpisz nazwę nowej konfiguracji, taką jak "Debugowanie częściowe", w polu Nazwa konfiguracji projektu.
Z listy Kopiuj Ustawienia wybierz pozycję Wydanie.
Kliknij przycisk OK , aby zamknąć okno dialogowe Nowa konfiguracja projektu.
Zamknij okno dialogowe Configuration Manager.
Teraz ustawisz opcje dla całego projektu.
W oknie dialogowym Strony właściwości w folderze Właściwości konfiguracji wybierz kategorię Ogólne.
W siatce ustawień projektu rozwiń węzeł Ustawienia domyślne projektu (w razie potrzeby).
W obszarze Project Defaults (Wartości domyślne projektu) znajdź pozycję Use of MFC (Użycie MFC). Bieżące ustawienie jest wyświetlane w prawej kolumnie siatki. Kliknij bieżące ustawienie i zmień je na Use MFC in a Static Library (Używanie MFC w bibliotece statycznej).
W lewym okienku okna dialogowego Strony właściwości otwórz folder C/C++ i wybierz pozycję Preprocesor. W siatce właściwości znajdź definicje preprocesora i zastąp ciąg "NDEBUG" ciągiem "_DEBUG".
W lewym okienku okna dialogowego Strony właściwości otwórz folder Konsolidator i wybierz kategorię danych wejściowych . W siatce właściwości znajdź pozycję Dodatkowe zależności. W ustawieniu Dodatkowe zależności wpisz "NAFXCWD. LIB" i "LIBCMT".
Kliknij przycisk OK , aby zapisać nowe opcje kompilacji i zamknąć okno dialogowe Strony właściwości.
W menu Kompilacja wybierz pozycję Skompiluj. Spowoduje to usunięcie wszystkich informacji debugowania z modułów, ale nie ma wpływu na bibliotekę MFC.
Teraz musisz dodać informacje debugowania z powrotem do wybranych modułów w aplikacji. Pamiętaj, że można ustawić punkty przerwania i wykonywać inne funkcje debugera tylko w modułach skompilowanych przy użyciu informacji o debugowaniu. Dla każdego pliku projektu, w którym chcesz uwzględnić informacje debugowania, wykonaj następujące kroki:
W Eksplorator rozwiązań otwórz folder Pliki źródłowe znajdujące się w projekcie.
Wybierz plik, dla którego chcesz ustawić informacje debugowania.
Z menu Widok wybierz pozycję Strony właściwości.
W oknie dialogowym Strony właściwości w folderze Konfiguracja Ustawienia otwórz folder C/C++, a następnie wybierz kategorię Ogólne.
W siatce właściwości znajdź pozycję Format informacji debugowania.
Kliknij ustawienia Format informacji debugowania i wybierz odpowiednią opcję (zazwyczaj /ZI), aby uzyskać informacje o debugowaniu.
Jeśli używasz aplikacji wygenerowanej przez kreatora lub masz wstępnie skompilowane nagłówki, musisz wyłączyć wstępnie skompilowane nagłówki lub ponownie skompilować je przed skompilowaniem innych modułów. W przeciwnym razie zostanie wyświetlone ostrzeżenie C4650 i komunikat o błędzie C2855. Prekompilowane nagłówki można wyłączyć, zmieniając ustawienie Utwórz/Użyj prekompilowanych nagłówków w< oknie dialogowym Właściwości projektu> (folder Właściwości konfiguracji, podfolder C/C++, kategoria Prekompilowane nagłówki).
W menu Kompilacja wybierz pozycję Kompiluj, aby ponownie skompilować pliki projektu, które są nieaktualne.
Alternatywą dla techniki opisanej w tym temacie jest użycie zewnętrznego pliku makefile do zdefiniowania poszczególnych opcji dla każdego pliku. W takim przypadku, aby połączyć się z bibliotekami debugowania MFC, należy zdefiniować flagę _DEBUG dla każdego modułu. Jeśli chcesz używać bibliotek wydania MFC, musisz zdefiniować usługę NDEBUG. Aby uzyskać więcej informacji na temat pisania zewnętrznych plików make, zobacz dokumentację narzędzia NMAKE.