Techniki testowania MFC

Jeśli debugujesz program MFC, te techniki debugowania mogą być przydatne.

W tym temacie

AfxDebugBreak

Makro TRACE

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.

W tym temacie

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.

W tym temacie

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_NEWusługi , musisz użyć DEBUG_NEW jawnie lub #define nowe , jak pokazano powyżej.

W tym temacie

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 lub free 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;
    

    W tym temacie

Tworzenie migawek pamięci

  1. Utwórz obiekt CMemoryState i wywołaj funkcję elementu członkowskiego CMemoryState::Checkpoint. Spowoduje to utworzenie pierwszej migawki pamięci.

  2. Po wykonaniu przez program operacji alokacji pamięci i cofania przydziału utwórz inny CMemoryState obiekt i wywołaj Checkpoint dla tego obiektu. Spowoduje to pobranie drugiej migawki użycia pamięci.

  3. Utwórz trzeci CMemoryState obiekt i wywołaj jego funkcję składową CMemoryState::D ifference , podając jako argumenty dwóch poprzednich CMemoryState 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ć.

    W tym temacie

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::Differencepolecenia 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ą newpolecenia . 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.

W tym temacie

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 CObjectobiektu 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.

W tym temacie

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, intlub 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;
}

W tym temacie

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

W tym temacie

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:

  1. 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.

  2. 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.

  3. Utwórz własną aplikację przy użyciu informacji debugowania dla wybranych modułów tylko zgodnie z poniższym opisem.

    W tym temacie

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).

  1. W Eksplorator rozwiązań wybierz projekt.

  2. Z menu Widok wybierz pozycję Strony właściwości.

  3. Najpierw utworzysz nową konfigurację projektu.

    1. <W oknie dialogowym Strony właściwości projektu> kliknij przycisk Configuration Manager.

    2. W oknie dialogowym Configuration Manager znajdź projekt w siatce. W kolumnie Konfiguracja wybierz pozycję <Nowy...>.

    3. W oknie dialogowym Nowa konfiguracja projektu wpisz nazwę nowej konfiguracji, taką jak "Debugowanie częściowe", w polu Nazwa konfiguracji projektu.

    4. Z listy Kopiuj Ustawienia wybierz pozycję Wydanie.

    5. Kliknij przycisk OK , aby zamknąć okno dialogowe Nowa konfiguracja projektu.

    6. Zamknij okno dialogowe Configuration Manager.

  4. Teraz ustawisz opcje dla całego projektu.

    1. W oknie dialogowym Strony właściwości w folderze Właściwości konfiguracji wybierz kategorię Ogólne.

    2. W siatce ustawień projektu rozwiń węzeł Ustawienia domyślne projektu (w razie potrzeby).

    3. 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).

    4. 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".

    5. 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".

    6. Kliknij przycisk OK , aby zapisać nowe opcje kompilacji i zamknąć okno dialogowe Strony właściwości.

  5. W menu Kompilacja wybierz pozycję Skompiluj. Spowoduje to usunięcie wszystkich informacji debugowania z modułów, ale nie ma wpływu na bibliotekę MFC.

  6. 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:

    1. W Eksplorator rozwiązań otwórz folder Pliki źródłowe znajdujące się w projekcie.

    2. Wybierz plik, dla którego chcesz ustawić informacje debugowania.

    3. Z menu Widok wybierz pozycję Strony właściwości.

    4. W oknie dialogowym Strony właściwości w folderze Konfiguracja Ustawienia otwórz folder C/C++, a następnie wybierz kategorię Ogólne.

    5. W siatce właściwości znajdź pozycję Format informacji debugowania.

    6. Kliknij ustawienia Format informacji debugowania i wybierz odpowiednią opcję (zazwyczaj /ZI), aby uzyskać informacje o debugowaniu.

    7. 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).

  7. 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.

    W tym temacie