斷言敘述用來指定您預期在程式某個點應為 true 的條件。 如果該條件不成立,則判斷提示會失敗,程式的執行會中斷,並會出現 [判斷提示失敗] 對話方塊 。
Visual Studio 支援以下列建構為基礎的 C++ 判斷提示陳述式:
MFC 程式的 MFC 判斷提示。
ATLASSERT 適用於使用 ATL 的程式。
使用 C 執行時期程式庫的程式之 CRT 斷言。
其他 C/C++ 程式的 ANSI 斷言函式。
您可以使用斷言來擷取邏輯錯誤、檢查作業結果,以及測試應該處理的錯誤狀況。
本主題內容
斷言的運作方式
當偵錯工具因為 MFC 或 C 執行階段程式庫判斷提示而停止時,如果來源可用,偵錯工具會流覽至來源檔案中發生判斷提示的點。 宣告訊息會同時出現在 「輸出」視窗 和 「宣告失敗 」對話方塊中。 如果您想要儲存斷言訊息以供日後參考,您可以將判斷提示訊息從 「輸出」 視窗複製到文字視窗。 「 輸出」 視窗也可能包含其他錯誤訊息。 請仔細檢查這些訊息,因為它們提供判斷提示失敗原因的線索。
使用斷言來偵測開發期間的錯誤。 通常,對每個假設使用一個斷言。 例如,如果您假設引數不是 NULL,請使用斷言來測試該假設。
偵錯和發行組建中的判斷提示
主張陳述式只有在 _DEBUG 定義時才會編譯。 否則,編譯器會將斷言視為 Null 陳述式。 因此,斷言陳述式不會在最終 Release 程式中施加額外負擔或效能成本,並可讓您避免使用 #ifdef 指引。
使用斷言的副作用
當您將斷言新增至程式碼時,請確定斷言沒有副作用。 例如,請考慮下列修改 nM 值的斷言:
ASSERT(nM++ > 0); // Don't do this!
因為ASSERT運算式不會在程式的發行版本中執行,所以nM在偵錯和發行版本中會有不同的值。 若要避免 MFC 中的此問題,您可以使用 VERIFY 巨集,而不是 ASSERT。
VERIFY 評估所有版本中的運算式,但不會檢查發行版本中的結果。
在斷言語句中使用函數呼叫時要特別小心,因為評估函數可能會產生意想不到的副作用。
ASSERT ( myFnctn(0)==1 ) // unsafe if myFnctn has side effects
VERIFY ( myFnctn(0)==1 ) // safe
VERIFY 在除錯和發布版本中調用myFnctn ,因此可以使用。 不過,使用 VERIFY 會在發行版本中產生不必要的函式呼叫負擔。
CRT 斷言
CRTDBG.H標頭檔會定義用於斷言檢查的_ASSERT和_ASSERTE巨集。
| Macro | Result |
|---|---|
_ASSERT |
如果指定的運算式評估為 FALSE,則會顯示檔案名稱和行號 _ASSERT。 |
_ASSERTE |
與 _ASSERT相同,另外還會顯示已斷言表達式的字串表示形式。 |
_ASSERTE 更加強大,因為它能報告被判定為 FALSE 的斷言表達式。 這可能足以在不參考原始碼的情況下識別問題。 不過,應用程式的偵錯版本會包含使用 _ASSERTE宣告之每個運算式的字串常數。 如果您使用許多 _ASSERTE 巨集,這些字串運算式會佔用大量記憶體。 如果這成為一個問題,請使用_ASSERT來節省內存。
當定義_DEBUG時,_ASSERTE巨集定義如下:
#define _ASSERTE(expr) \
do { \
if (!(expr) && (1 == _CrtDbgReport( \
_CRT_ASSERT, __FILE__, __LINE__, #expr))) \
_CrtDbgBreak(); \
} while (0)
如果斷言運算式的評估結果為 FALSE,則會呼叫 _CrtDbgReport 來報告斷言失敗(預設使用訊息對話方塊)。 如果您在訊息對話方塊中選擇 [ 重試 ], _CrtDbgReport 則會傳回 1,並 _CrtDbgBreak 透過 DebugBreak呼叫偵錯工具。
如果您需要暫時停用所有斷言,請使用 _CtrSetReportMode。
檢查堆積損壞
下列範例會使用 _CrtCheckMemory 來檢查堆積是否損毀:
_ASSERTE(_CrtCheckMemory());
檢查指標有效性
下列範例會使用 _CrtIsValidPointer 來驗證指定的記憶體範圍是否對讀取或寫入有效。
_ASSERTE(_CrtIsValidPointer( address, size, TRUE );
下列範例會使用 _CrtIsValidHeapPointer 來驗證某指標是否指向本機堆積中的記憶體(這是由此 C 執行階段程式庫的實例創建和管理的堆積,一個 DLL 可以擁有自己的程式庫實例,因而具備獨立於應用程式堆積的專屬堆積)。 此斷言不僅捕獲 null 或越界位址,還捕獲指向靜態變數、堆疊變數和任何其他非本地記憶體的指向。
_ASSERTE(_CrtIsValidHeapPointer( myData );
檢查記憶體區塊
下列範例會使用 _CrtIsMemoryBlock 來驗證記憶體區塊是否位於本機堆積中,且具有有效的區塊類型。
_ASSERTE(_CrtIsMemoryBlock (myData, size, &requestNumber, &filename, &linenumber));
MFC 斷言
MFC 定義了用於斷言檢查的 ASSERT 巨集。 它也會定義MFC ASSERT_VALID和CObject::AssertValid方法,以檢查CObject衍生物件的內部狀態。
如果 MFC ASSERT 巨集的引數評估為零或 false,巨集會停止程式執行並警示使用者;否則,執行會繼續。
當判斷提示失敗時,訊息對話方塊會顯示來源檔案的名稱和判斷提示的行號。 如果您在對話方塊中選擇 [重試],呼叫 AfxDebugBreak 會導致偵錯工具的執行中斷。 此時,您可以檢查呼叫堆疊,並使用其他偵錯工具來判斷判斷提示失敗的原因。 如果您已啟用 即時偵錯,且偵錯工具尚未運行,則對話方塊可以啟動偵錯工具。
下列範例說明如何使用ASSERT來檢查函式的傳回值:
int x = SomeFunc(y);
ASSERT(x >= 0); // Assertion fails if x is negative
您可以將 ASSERT 與 IsKindOf 函式搭配使用,以提供函式引數的類型檢查:
ASSERT( pObject1->IsKindOf( RUNTIME_CLASS( CPerson ) ) );
巨集在 ASSERT 發行版本中不會產生任何程式碼。 如果您需要評估發行版本中的運算式,請使用 VERIFY 巨集,而不是 ASSERT。
MFC ASSERT_VALID 和 CObject::AssertValid
CObject::AssertValid 方法提供物件內部狀態的執行階段檢查。 雖然當您從AssertValid派生類別時不需要覆寫CObject,但這樣做可以讓您的類別更可靠。
AssertValid 應該對物件的所有成員變數執行判斷提示,以確認它們包含有效的值。 例如,它應該檢查指標成員變數是否不是 NULL。
下列範例示範如何宣告 AssertValid 函式:
class CPerson : public CObject
{
protected:
CString m_strName;
float m_salary;
public:
#ifdef _DEBUG
// Override
virtual void AssertValid() const;
#endif
// ...
};
當您覆寫 AssertValid 時,在執行自己的檢查之前,請先呼叫 AssertValid 的基底類別版本。 然後使用 ASSERT 巨集來檢查您衍生類別中特有的成員,如下所示:
#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
如果您的任何成員變數儲存物件,這個 ASSERT_VALID 巨集可以用於測試其內部有效性(如果其類別覆寫了 AssertValid)。
例如,假設類別 CMyData,它會將 CObList 儲存在其其中一個成員變數中。
CObList變數 m_DataList,儲存物件的CPerson集合。 簡略的宣告如下所示:CMyData
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 ...
};
CMyData中的AssertValid覆寫如下所示:
#ifdef _DEBUG
void CMyData::AssertValid( ) const
{
// Call inherited AssertValid.
CObject::AssertValid( );
// Check validity of CMyData members.
ASSERT_VALID( m_pDataList );
// ...
}
#endif
CMyData 使用機制 AssertValid 來測試儲存在其資料成員中的物件有效性。 覆寫AssertValid 的 CMyData 會針對其自己的 m_pDataList 成員變數調用 ASSERT_VALID 巨集。
有效性測試不會在此層級停止,因為類別 CObList 也會覆寫 AssertValid。 此覆寫會對清單的內部狀態執行額外的有效性測試。 因此,對 CMyData 物件進行有效性測試,會進一步引發對儲存的 CObList 清單物件內部狀態進行其他有效性測試。
透過更多工作,您也可以為清單中儲存的 CPerson 物件新增有效性測試。 您可以從 CObList 衍生類別 CPersonList 並覆寫 AssertValid。 在覆寫中,您會呼叫 CObject::AssertValid,然後迭代遍歷清單,對儲存在清單中的每個 CPerson 物件呼叫 AssertValid。 本主題開頭顯示的 CPerson 類別已置換 AssertValid。
當您建置偵錯時,這是強大的機制。 當您後續建置以進行發行時,該機制會自動關閉。
AssertValid 的限制
觸發的斷言表示物件肯定是錯誤的,執行將停止。 然而,缺少斷言僅表示沒有發現問題,但不保證物件是好的。
使用斷言
捕捉邏輯錯誤
您可以根據程式的邏輯,在必須為 true 的條件上設定斷言。 除非發生邏輯錯誤,否則斷言不會生效。
例如,假設您正在模擬容器中的氣體分子,而變數 numMols 代表分子的總數。 此數字不能小於零,因此您可以包含 MFC 判斷提示陳述式,如下所示:
ASSERT(numMols >= 0);
或者您可以包含如下所示的 CRT 斷言:
_ASSERT(numMols >= 0);
如果您的程式正常運作,這些陳述式不會執行任何動作。 不過,如果邏輯錯誤導致 numMols 小於零,判斷提示會停止程式的執行,並顯示 判斷提示失敗對話方塊。
檢查結果
斷言在測試其結果不易從快速視覺檢查中辨識的操作中具有重要價值。
例如,考慮以下程式碼,它根據鏈結串列mols的內容更新變數iMols:
/* 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
計iMols數的分子數必須始終小於或等於分子總數。 numMols 迴圈的目視檢查並未顯示一定是這種情況,因此在迴圈之後使用斷言語句來測試該條件。
尋找未處理的錯誤
您可以在程式中使用斷言來測試應該已經處理錯誤的情況。 在下列範例中,圖形常式會傳回錯誤碼或零以表示成功。
myErr = myGraphRoutine(a, b);
/* Code to handle errors and
reset myErr if successful */
ASSERT(!myErr); -- MFC version
_ASSERT(!myErr); -- CRT version
如果錯誤處理程式碼正常運作,則應該在達到斷言之前處理錯誤並 myErr 重設為零。 如果有另一個值,則 myErr 判斷提示會失敗,程式會停止,並會出現 [ 判斷提示失敗] 對話方塊 。
不過,宣告陳述式並不能取代錯誤處理程式碼。 下列範例顯示判斷提示陳述式,該陳述式可能會導致最終版本程式碼發生問題:
myErr = myGraphRoutine(a, b);
/* No Code to handle errors */
ASSERT(!myErr); // Don't do this!
_ASSERT(!myErr); // Don't do this, either!
此程式碼依賴斷言語句來處理錯誤情況。 因此,傳 myGraphRoutine 回的任何錯誤碼都將在最終發行程式碼中未處理。