C/C++-Assertionen
Eine Assertionsanweisung formuliert eine Bedingung, die an einer bestimmten Stelle im Programm "true" lauten muss. Wenn diese Bedingung nicht den Wert TRUE aufweist, kann die Assertion nicht durchgeführt werden, die Programmausführung wird unterbrochen, und das Dialogfeld Assertionsfehler wird geöffnet.
Visual Studio unterstützt C++-Assertionsanweisungen, die auf den folgenden Konstrukten basieren:
MFC-Assertionen für MFC-Programme
ATLASSERT für Programme, die ATL verwenden.
CRT-Assertionen für Programme, die die C-Laufzeitbibliothek verwenden.
Die ANSI-assert-Funktion für andere C/C++‑Programme.
Sie können Assertionen verwenden, um logische Fehler zu erfassen, Ergebnisse einer Operation zu überprüfen und Fehlerbedingungen zu testen, die bearbeitet werden müssten.
In diesem Thema
Funktionsweise von Assertionen
Assertionen in Debug- und Versionsbuilds
Nebeneffekte der Verwendung von Assertionen
Funktionsweise von Assertionen
Wenn der Debugger ein Programm aufgrund einer Assertion von MFC oder der C-Laufzeitbibliothek anhält, navigiert er zu der Stelle in der Quelldatei (sofern vorhanden), an der die Assertion aufgetreten ist. Die Assertionsmeldung wird sowohl im Ausgabefenster als auch im Dialogfeld Assertionsfehler angezeigt. Sie können die Assertionsmeldung aus dem Ausgabefenster in ein Textfenster kopieren, wenn Sie sie zu Referenzzwecken behalten möchten. Das Ausgabefenster enthält u. U. weitere Fehlermeldungen. Überprüfen Sie diese Meldungen sorgfältig; häufig enthalten sie Hinweise auf die Ursache des Assertionsfehlers.
Verwenden Sie Assertionen, um Fehler während der Entwicklung zu erkennen. Verwenden Sie im Allgemeinen eine Assertion für die jeweilige Annahme. Wenn Sie beispielsweise annehmen, dass ein Argument ungleich NULL ist, verwenden Sie eine Assertion, um diese Annahme zu testen.
Assertionen in Debug- und Releasebuilds
Assertionsanweisungen werden nur kompiliert, wenn _DEBUG
definiert ist. Andernfalls behandelt der Compiler Assertionen als NULL-Anweisungen. Assertionsanweisungen verursachen daher keinen Mehraufwand und keine Leistungseinbußen in der endgültigen Programmversion, sodass Sie keine #ifdef
-Direktiven verwenden müssen.
Nebeneffekte der Verwendung von Assertionen
Wenn Sie Assertionen in den Code einfügen, sollten Sie sicherstellen, dass die Assertionen keine Nebeneffekte haben. Betrachten Sie beispielsweise die folgende Assertion, mit der der nM
-Wert geändert wird:
ASSERT(nM++ > 0); // Don't do this!
Da der ASSERT
-Ausdruck in der Releaseversion des Programms nicht ausgewertet wird, verfügt nM
in der Debug- und Releaseversion über unterschiedliche Werte. Um dieses Problem in MFC zu vermeiden, können Sie anstelle von ASSERT
das VERIFY-Makro verwenden. VERIFY
wertet den Ausdruck in allen Versionen aus, überprüft jedoch nicht das Ergebnis in der Releaseversion.
Da die Auswertung einer Funktion unerwartete Nebeneffekte haben kann, sollten Funktionsaufrufe in Assertionsanweisungen mit Vorsicht verwendet werden.
ASSERT ( myFnctn(0)==1 ) // unsafe if myFnctn has side effects
VERIFY ( myFnctn(0)==1 ) // safe
VERIFY
ruft myFnctn
sowohl in der Debug- als auch in der Releaseversion auf und kann somit verwendet werden. Durch die Verwendung von VERIFY
wird jedoch ein Mehraufwand aufgrund eines unnötigen Funktionsaufrufs in der Releaseversion verursacht.
CRT-Assertionen
Die Headerdatei CRTDBG.H
enthält Definitionen der Makros _ASSERT
und _ASSERTE
, die zur Überprüfung von Assertionen verwendet werden.
Makro | Ergebnis |
---|---|
_ASSERT |
Dateiname und Zeilennummer von _ASSERT , wenn der angegebene Ausdruck mit FALSE ausgewertet wird. |
_ASSERTE |
Identisch mit _ASSERT , zusätzlich jedoch eine Zeichenfolgenentsprechung des Ausdrucks, für den die Assertion ausgeführt wurde. |
_ASSERTE
ist leistungsfähiger, da der überwachte Ausdruck gemeldet wird, wenn er mit FALSE ausgewertet wurde. Auf diese Weise lässt sich das Problem u. U. bereits ohne Zuhilfenahme des Quellcodes identifizieren. In der Debugversion der Anwendung ist jedoch für jeden mit _ASSERTE
überprüften Ausdruck eine Zeichenfolgenkonstante enthalten. Bei Verwendung vieler _ASSERTE
-Makros können diese Zeichenfolgenausdrücke beträchtlichen Speicherplatz belegen. Falls dies zu Problemen führt, verwenden Sie _ASSERT
, um Speicherplatz zu sparen.
Wenn _DEBUG
definiert ist, wird das _ASSERTE
-Makro wie folgt definiert:
#define _ASSERTE(expr) \
do { \
if (!(expr) && (1 == _CrtDbgReport( \
_CRT_ASSERT, __FILE__, __LINE__, #expr))) \
_CrtDbgBreak(); \
} while (0)
Wenn der Ausdruck bei der Assertionsauswertung FALSE ergibt, wird _CrtDbgReport aufgerufen und gibt den Assertionsfehler (standardmäßig in einem Meldungsdialogfeld) aus. Wenn Sie im Meldungsdialogfeld Wiederholen auswählen, gibt _CrtDbgReport
den Wert 1 zurück, und _CrtDbgBreak
ruft den Debugger über DebugBreak
auf.
Wenn Sie alle Assertionen vorübergehend deaktivieren müssen, verwenden Sie _CtrSetReportMode.
Überprüfen des Heaps auf Beschädigungen
Im folgenden Beispiel wird _CrtCheckMemory verwendet, um den Heap auf Beschädigungen zu überprüfen:
_ASSERTE(_CrtCheckMemory());
Überprüfen der Gültigkeit von Zeigern
Im folgenden Beispiel wird anhand von _CrtIsValidPointer überprüft, ob in einen bestimmten Speicherbereich geschrieben bzw. daraus gelesen werden kann.
_ASSERTE(_CrtIsValidPointer( address, size, TRUE );
Im folgenden Beispiel wird anhand von _CrtIsValidHeapPointer überprüft, ob ein Zeiger auf Speicher im lokalen Heap zeigt. (Es handelt sich um den Heap, der von dieser Instanz der C-Laufzeitbibliothek generiert und verwaltet wird. Eine DLL kann über eine eigene Bibliotheksinstanz und daher auch über einen eigenen Heap außerhalb des Anwendungsheaps verfügen.) Diese Assertion erkennt nicht nur NULL-Adressen oder Adressen außerhalb des gültigen Bereichs, sondern auch Zeiger auf statische Variablen, Stapelvariablen und sonstigen nicht lokalen Speicher.
_ASSERTE(_CrtIsValidHeapPointer( myData );
Überprüfen eines Speicherblocks
Im folgenden Beispiel wird anhand von _CrtIsMemoryBlock sichergestellt, dass ein Speicherblock sich im lokalen Heap befindet und über einen gültigen Blocktyp verfügt.
_ASSERTE(_CrtIsMemoryBlock (myData, size, &requestNumber, &filename, &linenumber));
MFC-Assertionen
Das ASSERT-Makro für die Assertionsüberprüfung ist in MFC definiert. Sie definiert auch die MFC ASSERT_VALID
- und CObject::AssertValid
-Methoden zum Überprüfen des internen Zustands eines von CObject
abgeleiteten Objekts.
Wenn das Argument des MFC-ASSERT
-Makros mit "0 (null)" oder "false" ausgewertet wird, wird die Programmausführung durch das Makro angehalten und eine Warnung an den Benutzer ausgegeben. Andernfalls wird die Ausführung fortgesetzt.
Wenn eine Assertion fehlschlägt, werden der Name der Quelldatei sowie die Zeilennummer der Assertion in einem Meldungsdialogfeld angezeigt. Wenn Sie im Dialogfeld auf „Wiederholen“ klicken, wird AfxDebugBreak aufgerufen, die Ausführung unterbrochen und der Debugger aufgerufen. An diesem Punkt können Sie die Aufrufliste und weitere Debuggerfunktionen überprüfen, um die Ursache für den Assertionsfehler zu ermitteln. Bei Aktivierung von Just-In-Time-Debugging kann der Debugger über das Dialogfeld aufgerufen werden, sofern er nicht bereits ausgeführt wurde.
Das folgende Beispiel veranschaulicht die Verwendung von ASSERT
, um den Rückgabewert einer Funktion zu überprüfen:
int x = SomeFunc(y);
ASSERT(x >= 0); // Assertion fails if x is negative
Um eine Typüberprüfung für die Funktionsargumente vorzunehmen, können Sie ASSERT mit der IsKindOf-Funktion verwenden:
ASSERT( pObject1->IsKindOf( RUNTIME_CLASS( CPerson ) ) );
In der Releaseversion generiert das ASSERT
-Makro keinen Code. Wenn Sie den Ausdruck in der Releaseversion auswerten müssen, verwenden Sie anstelle von ASSERT das VERIFY-Makro.
MFC ASSERT_VALID and CObject::AssertValid
Die CObject::AssertValid-Methode ermöglicht es, den internen Zustand eines Objekts zur Laufzeit zu überprüfen. Es ist zwar nicht erforderlich, AssertValid
beim Ableiten der Klasse aus CObject
zu überschreiben, Sie können so jedoch die Zuverlässigkeit der Klasse erhöhen. AssertValid
sollte Assertionen für alle Membervariablen des Objekts ausführen und sicherstellen, dass diese gültige Werte enthalten. Beispielsweise sollte überprüft werden, ob alle Membervariablen in Form von Zeigern ungleich NULL sind.
Im folgenden Beispiel wird die Deklaration einer AssertValid
-Funktion erläutert:
class CPerson : public CObject
{
protected:
CString m_strName;
float m_salary;
public:
#ifdef _DEBUG
// Override
virtual void AssertValid() const;
#endif
// ...
};
Wenn Sie AssertValid
überschreiben, rufen Sie die Basisklassenversion von AssertValid
auf, bevor Sie eigene Überprüfungen vornehmen. Überprüfen Sie dann mit dem ASSERT-Makro wie folgt die Gültigkeit der Member, die nur in der abgeleiteten Klasse vorkommen:
#ifdef _DEBUG
void CPerson::AssertValid() const
{
// Call inherited AssertValid first.
CObject::AssertValid();
// Check CPerson members...
// Must have a name.
ASSERT( !m_strName.IsEmpty());
// Must have an income.
ASSERT( m_salary > 0 );
}
#endif
Wenn durch eine der Membervariablen Objekte gespeichert werden, können Sie deren interne Gültigkeit mit dem ASSERT_VALID
-Makro testen (falls die betreffende Klasse AssertValid
überschreibt).
Durch die CMyData
-Klasse wird beispielsweise CObList in einer ihrer Membervariablen gespeichert. Die CObList
-Variable m_DataList
speichert eine Auflistung von CPerson
-Objekten. Die Deklaration von CMyData
lautet in den wesentlichen Teilen wie folgt:
class CMyData : public CObject
{
// Constructor and other members ...
protected:
CObList* m_pDataList;
// Other declarations ...
public:
#ifdef _DEBUG
// Override:
virtual void AssertValid( ) const;
#endif
// And so on ...
};
Die AssertValid
-Überschreibung in CMyData
sieht wie folgt aus:
#ifdef _DEBUG
void CMyData::AssertValid( ) const
{
// Call inherited AssertValid.
CObject::AssertValid( );
// Check validity of CMyData members.
ASSERT_VALID( m_pDataList );
// ...
}
#endif
CMyData
verwendet den AssertValid
-Mechanismus, um die Gültigkeit der im Datenmember gespeicherten Objekte zu testen. Durch das AssertValid
-Überschreiben von CMyData
wird das ASSERT_VALID
-Makro für die eigene m_pDataList-Membervariable aufgerufen.
Die Gültigkeitsüberprüfung wird auf dieser Ebene nicht beendet, da die CObList
-Klasse auch von AssertValid
überschrieben wird. Diese Überschreibung bewirkt weitere Gültigkeitsüberprüfungen des internen Listenzustands. Auf diese Weise führt eine Gültigkeitsüberprüfung für ein CMyData
-Objekt zu weiteren Gültigkeitsüberprüfungen, mit denen der interne Zustand des gespeicherten CObList
-Listenobjekts getestet wird.
Mit geringem Mehraufwand könnten die Gültigkeitsüberprüfungen auch auf die in der Liste gespeicherten CPerson
-Objekte ausgeweitet werden. Sie könnten eine CPersonList
-Klasse von CObList
ableiten und AssertValid
überschreiben. Hierbei würden Sie CObject::AssertValid
aufrufen und dann die Liste durchlaufen, wobei für jedes in der Liste gespeicherte AssertValid
-Objekt CPerson
aufgerufen würde. Durch die am Anfang dieses Themas dargestellte CPerson
-Klasse wird AssertValid
bereits überschrieben.
Dieses Verfahren ist beim Erstellen von Debugbuilds äußerst hilfreich. Beim späteren Erstellen eines Releasebuilds wird das Verfahren automatisch deaktiviert.
Einschränkungen von "AssertValid"
Eine ausgelöste Assertion weist darauf hin, dass das Objekt mit Sicherheit fehlerhaft ist, und die Ausführung wird angehalten. Wird keine Assertion ausgelöst, so bedeutet dies jedoch nur, dass kein Problem festgestellt wurde, die Fehlerfreiheit des Objekts kann aber trotzdem nicht garantiert werden.
Verwenden von Assertionen
Erfassen von logischen Fehlern
So kann eine Assertion für eine Bedingung festgelegt werden, die entsprechend der Programmlogik true ergeben muss. Die Assertion bleibt so lange wirkungslos, bis ein logischer Fehler auftritt.
Angenommen, Sie simulieren die Bewegungen von Gasmolekülen in einem Behälter, und die numMols
-Variable gibt die Gesamtanzahl der Moleküle an. Diese Zahl darf nie kleiner als 0 (null) sein, sodass Sie die folgende MFC-Assertionsanweisung einfügen können:
ASSERT(numMols >= 0);
Sie können auch eine CRT-Assertion wie die folgende einschließen:
_ASSERT(numMols >= 0);
Diese Anweisungen bleiben wirkungslos, solange das Programm einwandfrei funktioniert. Wenn jedoch aufgrund eines logischen Fehlers der Wert von numMols
negativ wird, wird die Programmausführung durch die Assertion unterbrochen, und das Dialogfeld Assertionsfehler wird angezeigt.
Überprüfen der Ergebnisse
Besonders eignen sich Assertionen zum Testen von Operationen, deren Ergebnisse durch einen schnellen Blick auf den Code nicht ersichtlich sind.
Dies ist beispielsweise beim folgenden Code der Fall, in dem die iMols
-Variable abhängig vom Inhalt der verketteten Liste, auf die mols
zeigt, aktualisiert wird.
/* This code assumes that type has overloaded the != operator
with const char *
It also assumes that H2O is somewhere in that linked list.
Otherwise we'll get an access violation... */
while (mols->type != "H2O")
{
iMols += mols->num;
mols = mols->next;
}
ASSERT(iMols<=numMols); // MFC version
_ASSERT(iMols<=numMols); // CRT version
Die Anzahl der von iMols
gezählten Moleküle muss immer kleiner oder gleich numMols
, d. h. der Gesamtanzahl der Moleküle, sein. Durch die visuelle Prüfung dieser Schleife kann nicht ermittelt werden, dass dies immer zutrifft, sodass diese Bedingung nach der Schleife mit einer Assertionsanweisung überprüft wird.
Suchen von unbehandelten Fehlern
Assertionen ermöglichen es Ihnen, den Code an den Stellen, an denen alle Fehler bereits behandelt worden sein sollten, auf Fehlerzustände zu testen. Im folgenden Beispiel gibt eine Grafikroutine einen Fehlercode oder, bei erfolgreicher Ausführung, 0 (null) zurück.
myErr = myGraphRoutine(a, b);
/* Code to handle errors and
reset myErr if successful */
ASSERT(!myErr); -- MFC version
_ASSERT(!myErr); -- CRT version
Wenn der Fehlerbehandlungscode einwandfrei funktioniert, sollte der Fehler behandelt und myErr
vor Erreichen der Assertion auf 0 (null) zurückgesetzt werden. Wenn myErr
einen anderen Wert aufweist, schlägt die Assertion fehl, das Programm wird angehalten, und das Dialogfeld Assertionsfehler wird angezeigt.
Assertionsanweisungen sind allerdings kein Ersatz für Fehlerbehandlungscode. So kann die Assertionsanweisung im folgenden Beispiel im Releasecode u. U. Probleme bereiten:
myErr = myGraphRoutine(a, b);
/* No Code to handle errors */
ASSERT(!myErr); // Don't do this!
_ASSERT(!myErr); // Don't do this, either!
Bei diesem Code wird davon ausgegangen, dass der Fehlerzustand durch die Assertionsanweisung behandelt wird. Dies würde im Releaseversionscode dazu führen, dass alle von myGraphRoutine
zurückgegebenen Fehlercodes nicht behandelt würden.