天天看點

MFC 斷言

一、關于斷言

   所謂的斷言就是可以肯定為正确的一個陳述語句。

   假設某個函數需要一個指向文檔對象的指針作為參數,但卻錯誤地使用了一個視圖指針來調用該函數。如果函數繼續使用該錯誤位址,輕則程式運作得不到正确的結果,重則破壞視圖資料。由于這種錯誤往往要到使用視圖資料時才會發現,因而要找出導緻錯誤的根本原因就要付出相當大的代價了。

   隻要在函數開始部分加入斷言檢查,檢驗指針是否真正指向一個文檔對象,就可以完全地避免此類問題的産生。

   二、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 對象

// 其它操作