一、關于斷言
所謂的斷言就是可以肯定為正确的一個陳述語句。
假設某個函數需要一個指向文檔對象的指針作為參數,但卻錯誤地使用了一個視圖指針來調用該函數。如果函數繼續使用該錯誤位址,輕則程式運作得不到正确的結果,重則破壞視圖資料。由于這種錯誤往往要到使用視圖資料時才會發現,因而要找出導緻錯誤的根本原因就要付出相當大的代價了。
隻要在函數開始部分加入斷言檢查,檢驗指針是否真正指向一個文檔對象,就可以完全地避免此類問題的産生。
二、assert宏
assert宏能夠計算作為參數傳遞的表達式值。如果表達式為真,則執行繼續。否則,程式顯示一個消息并中斷。此時可以選擇忽略錯誤、終止程式或進入調試器。下面是如何在函數中應用assert宏驗證參數的一個例子:
void foo(char* p,int size)
{
assert(p != 0); // 驗證緩沖區指針
assert((size >= 100); // 确認緩沖區大小至少為100位元組
// foo 函數的其它計算過程
}
如果沒有定義_debug預處理符,則該語句不會真正生成代碼。visual c++會在調試模式編譯時自動定義_debug,而在發行模式下,該預處理符是不存在的。如果定義了_debug,則上述兩個斷言生成的代碼類如:
//assert(p != 0);
do
if(!(p != 0) && afxassertfailedline(__file__, __line__))
afxdebugbreak();
} while(0);
//assert((size >= 100);
if(!(size >= 100) && afxassertfailedline(__file__,__line__))
}while(0);
do-while結構在一個單獨的語句塊之内封裝了整個斷言操作。if語句計算斷言表達式,如果值為0則調用afxassertfailedline()。afxassertfailedline()顯示消息框并提供 “abort,retry,or ignore”選擇,如果選擇retry則調用afxdebugbreak(),并由此激活調試器。
和afxassertfailedline()的簡單目的(即顯示對話框以供選擇)相比,它的實作顯得非常複雜。該函數的源代碼在afxasert.cpp内,可以發現它使用了一些特殊的函數以確定消息框正确顯示。舉個例子,如果斷言失敗是在程式發送wm_quit消息之後的某個位置,則afxassertfailedline()為了顯示消息框必須臨時地從隊列中删除這個消息。
傳遞給afxassertfailedline()的參數__file__ 和 __line__分别為代表程式檔案名和目前行号的預處理符号。它們由ansi标準定義,由編譯器自動生成具體數值。
三、verify宏
由于assert僅在程式的調試版起作用,測試表達式總是被動的。也就是說,它們不能包含指派、增量、減量等真正改變資料的操作。但有時候我們需要驗證一個主動表達式,比如指派語句。這時可以使用verify代替assert。下面是一個例子:
char* q; // 指針的副本
verify(q = p); // 拷貝指針并執行驗證
assert((size >= 100); // 確定緩沖區大小至少為100位元組
// 執行 foo 的其它操作
在調試模式下assert和verify是相同的。但在發行模式下,verify能夠繼續對表達式求值(但不再進行斷言檢驗),而assert語句在效果上就如同已經删除了一樣。
盡管在mfc源代碼中可以找到一些應用verify的例子,但assert用得更為普遍。一些程式員總是完全避免使用verify,因為他們已經習慣于使用被動斷言。請記住,如果在assert語句中使用了主動表達式,編譯器不會發出任何警告。在發行模式下編譯時該表達式會被直接删除,進而導緻程式運作的錯誤。由于發行版程式不含調試資訊,這種類型的錯誤是很難找到原因的。
四、debug_only宏
可以認為debug_only宏是assert宏的一種特殊格式,它用于計算表達式而不執行斷言檢查。這在隻為調試目的而加入某語句時很有用,如:
void foo(char* p,int size,char fill)
char* q; // 指針副本
assert(isalpha(fill)); // 確定fill為字母
debug_only(if(!isalpha(fill)) fill = 'x'); // 如果fill值非法,則指定為'x'
在上例中,即使第三個參數非法,調試過程仍可繼續。事實上,很少有程式員喜歡用這個宏。他們更習慣于使用傳統的方法,即使用_debug預處理符使得調試代碼隻在調試時被編譯:
assert(isalpha(fill)); // 确認fill為字母
#ifdef _debug
if(!isalpha(fill)) fill = 'x'); // 如果fill值非法則使其為'x'
#endif
// 執行foo的其它操作
五、assert_valid宏
assert在執行簡單驗證時很有用,但對于c++對象,特别是由cobject派生的對象,則有更好的方法來實作類似操作。作為一般規則,我們應在開始使用每一個對象之前檢查資料訛誤。assert_valid宏使得對cobject的派生類實作該操作非常簡單,其過程如下所示:
void cmyview::foo(cyourview* pview) // cmyview 和 cyourview 從cobject 繼承
assert_valid(this); // 驗證本身
assert_valid(pview); // 驗證pview
如下所示,這些宏直接調用afxassertvalidobject():
void cmyview::foo(cyourview* pview)
//assert_valid(this); // 驗證本身
afxassertvalidobject(this,__file__,__line__);
//assert_valid(pview); // 驗證pview
afxassertvalidobject(pview,__file__,__line__);
afxassertvalidobject()的定義可以在objcore.cpp檔案找到,它是一個沒有正式說明文檔的mfc函數。afxassertvalidobject()首先檢查指針非空(null)且指向一個合法的記憶體位址,其大小符合由相關的cruntimeclass對象指定的數值。如果這些測試失敗,則afxassertvalidobject()的行為就像一個普通斷言錯誤- -即調用afxassertfailedline(),有可能還要調用afxdebugbreak();否則,afxassertvalidobject()調用虛函數assertvalid(),可以覆寫後者以執行其它的完整性檢查。
如果使用應用向導或類向導生成基于mfc的類,通常會得到assertvalid()的骨架,最好改寫這些骨架代碼以增加最基本的完整性檢查。下面是一個典型的例子,類sample從cobject繼承,假定它含有職員名字及其薪水:
class sample : public cobject
protected:
cstring m_name; // 職員名字
double m_salary; // 薪水
public:
sample(lpctstr name,double salary) : m_name(name), m_salary(salary) {}
virtual void assertvalid() const;
void sample::assertvalid() const
cobject::assertvalid(); // 驗證基類
assert(!m_name.isempty()); // 驗證職員名字
assert(m_salary > 0); // 驗證薪水
顯然,依賴于派生類的資料成員,這些assertvalid()函數可以更為複雜。由于該函數隻在調試版起作用,因而無需擔心由此産生的時間開銷。然而,在某些熱點如cview::ondraw()函數,mfc會頻繁地驗證其cdocument指針,此時在文檔對象内應該避免冗長的測試。當然,如果正确地使用了文檔/視圖結構,許多應用資料将會儲存在cdocument的派生類對象中,此時妥協和折衷還是必要的。
六、assert_kindof宏
許多時候隻需要驗證指針确實引用了所希望的對象類型,以確定可以安全地通路該對象的成員。assert_kindof能夠完成該檢查,而且時間開銷比assert_valid要少。其調用形式如下:
void sample::dosomething(cmydocument* pdoc) const{assert_kindof(cmydocument,pdoc);}
assert_kindof要求所檢查的指針指向從cobject派生的對象,而且該對象實作mfc運作時類資訊聲明。這種聲明通常是通過在類聲明中包含declare_dynamic宏,在實作檔案包含implement_dynamic宏來實作的。應用向導(或類向導)能夠為大多數文檔和視圖類自動生成該聲明。
下面是由assert_kindof生成的真正代碼:
void sample::dosomething(cmydocument* pdoc) const
//assert_kindof(cmydocument,pdoc) 生成以下代碼
assert(pdoc->iskindof(runtime_class(cmydocument)));
// 現在可以确認 pdoc 指向 cmydocument 對象
// 其它操作