用戶: 密碼:     忘記密碼 | 會員註冊
  

把握C++ builder的除錯藝術

7.3
出處:2345軟件大全 時間:2011-06-27 人氣:955

核心提示:這篇文章,我將從最基本的開始談起。但期望可以涉及更廣的層面,而不只僅是為你的程序除錯(debug)。

  這篇文章,我將從最基本的開始談起。但期望可以涉及更廣的層面,而不只僅是為你的程序除錯(debug)。你將會看到,我認為除錯(debugging)這個字的全部意義,並不只是通過ide的內建機制來運行的。我期望在這篇小小的文章結束時,幾乎每個讀者都可以學到至少是一件新東西,並把它藏到你的兵器庫中。記住,你程序中的錯誤(bug)越少,你的最終用戶對你的程序的感覺就越好;你對錯誤(bug)的處理越好,用戶們發現錯誤(bug)時就越樂於告訴你以便你改正錯誤。好了,現在繫好安全帶,戴上護目鏡,讓我們開始一段瘋狂的路程!

  書寫潔淨的代碼

  首先而且或許是最重要的一點是書寫潔淨、可讀的代碼是極其重要的。能夠在寫完一段代碼後回顧一下並給它加上註釋來說明這段代碼用來做什麼和為什麼這麼做,將會省去你當前跟蹤代碼的無數個痛苦的小時。或許你會多花一點時間來書寫,但當你花過n小時來跟蹤那些難以捉摸的bug時,你就會同意多花點時間來讓程序代碼可讀是多麼值得了。(你本可以很輕易完成除錯的)。假如沒有這麼做過,我建議你停下來,讀讀另一篇scott的出色文章-代碼的風格(大家需要的話,將會盡快翻譯)。

  使用異常及異常處理能力

  現在進入下一步,這仍然是基於代碼的步驟。(除了在極少數的情況下,你不能老是使用系統內建的除錯器,所以知道其他可以找出這些麻煩的蟲子的辦法總是個好主意)。本步驟完全是關於如何做到,更重要的是處理好在你的窗體出現異常時系統扔給你的(產生的)錯誤。在c++標準得到認可前黑暗的舊日子裡,應用程序通常會通過返回值來發出錯誤信號(這種方法在ole和一些winapi函數中仍在使用)。很明顯,你可以很輕易的忽略這些(事實上也是經常的,我的意思是你經常反省一個winapi函數的返回值嗎?)。

  所以他們決定….,okay,我們需要一個新的機制,一個你不能忽略的。但你可以處理,定制(自定義 customize)。異常就此出現了。想要一個非凡的錯誤類型標誌?輕易的很,定義一個新的異常類型(不過是一個類,沒別的),拋出來(產生這個異常)。完了。

  例子:

  class myexception
{
public:
ansistring imessage;
myexception(ansistring message) { imessage=message;}
};

  throw new myexception(“test exception message”);

  就這麼簡單!(當然不是很完全,我會很快加上的)。漂亮而又簡單,並且非常輕易定制來滿足您的需要。okey,你會問到:“我能產生異常了,但如何處理它們?我的意思是,我想在第一時間(位置)從我的代碼中排除異常!”這當然很輕易做到,實際上還很輕易定制呢!標準委員會為我們定義了try {/* code */} catch (...) {/* code */ }機制,跟異常機制一樣,它完全可以定制來滿足您的需要!只需把您的執行代碼段放在try模塊中就行了,您還需要一個catch( ) 或 __finally 模塊來告訴程序(假如)得到一個異常的時候作什麼。現在就是你這麼做的好處,你定義了一個類class類型並且輸入變量來捕捉異常-通過聲明catch( )。(在前面的例子中,該當是這樣-catch(myexception &e) { /*在這裡書寫捕捉到異常後的處理代碼*/})為了讓這個系統更有力,你可以建立完整的子類繼續樹。這樣當你捕捉基類時你可以捕捉所有從這個基類繼續的異常類型(vcl中一個很好的例子就是所有的異常都是從exception類繼續而來的,所以catch(exception& e) 將捕捉所有的vcl異常,當然包也括您所產生的。但esocketerror除外,見xiphias在http://www.bytamin-c.com/ 的howto (若你不喜歡e文的話,我會盡快翻譯)。記住這個想法,我會在當前另一個步驟具體說明)。要讓它再有力一些的話,標準委員會決定包括如下的聲明catch(…) ,沒錯括號中就是三個點。此聲明答應我們捕捉任何異常,我的意思是所有的異常。還想再有力一些?當然可以,你可以用附加的catch( )聲明,跟if..else if…的樣子差不多。這裡要牢牢記住!假如你捕捉到了一個異常類型,那麼當前就它不會被再次捕捉到了!所以先看下面的代碼…

  try
{
// 程序的正常運行代碼
}
catch(edbengineerror &e)
{
// 處理基於數據庫引擎的錯誤
}
catch(eexternalerror &e)
{
//通常處理基於windows的錯誤
}
catch(exception &e)
{
// 處理所有其他的vcl錯誤
}

  你可以看到,這裡按照 "是edbengineerror嗎? 是->處理,不是?->繼續捕捉" "是eexternalerror嗎? 是-> 處理, 不是?-> 繼續下一次捕捉" 等等… 這樣的順序排列。

  接著還有更多的內容。假如你期望對某個異常做些什麼,又不期望異常就此消失,你可以重新拋出(產生)這個異常。它將繼續向後尋找新的catch()過程來處理它。我不能說我經常這麼做。但最好該當知道,就像“拋出”一樣簡單。就是這樣,throw將帶著已經被你處理過的異常繼向後尋找另一個catch來處理它。

  最後而不是最不重要的 (這部分不包括在標準規範中,倒更像是borland專有的增加版)就是 __finally 聲明,使用一個 __finally{ } 模塊,你可以指定不管有否異常產生都將運行的代碼。這裡是清除你通過new方法分配的局部變量及將所有該當設定回正常狀態的標誌復位(例如將一個等待狀態的鼠標指針復位成正常狀態)的最方便的地方。

  呸,太多了!休息一下吧,有空可以看一看c++builder幫助中的exception類, (所有e開頭的,你會注重到它們都是從exception類繼續來的。這也是定制你自己的異常類的好練習!) 當你回來時,我們將進入下一步旅程。

  使用記錄(logging)機制

  您不可能總是使用除錯器來除蟲,有時你沒法依靠內建除錯器的力量,所以有時你將不得不求助於其他的除錯手腕來調試程序。(典型例子如:nt服務、isapi/cgi程序、實時應用程序…等等)此時您將不得不求助於我們這樣經驗豐富的程序員才會談到的老式的除錯/調試技術。例如產生使用某種記錄(logging)機制來看看程序的頭巾下面究竟發生了什麼的念頭。幸運的是,有許許多多的現成的機制可以讓我們的這項工作變得輕易些。這裡我將談到我所偏愛的三種方法,你也可以將您自己的方法email給我,我會考慮加入這一部分。

  okay 先說第一種,(調試/除錯輸出字串)outputdebugstring。幸運的是microsoft已經為我們實現了一個非常廣泛的調試/除錯子系統。包括實現您自己的調試/除錯記錄系統的機制。程序在一個調試/除錯進程內運行的時候,outputdebugstring將它的參數(一個c string)輸出到調試/除錯器的輸出上下文,若調試/除錯器沒有運行,outputdebugstring就被忽略。假如沒有彈出消息的時候,outputdebugstring在終端上也可以很好的運行,當你分發給客戶前別忘了移去它(通過 #ifdef debug…#endif’),程序可以運行的更快一點。“wow,又好又輕易!”你或許會說“但當程序不能在調試/除錯器內運行時,該怎麼辦?”

  請牢記,這只是我的觀點,基於一種觀念的評價,我個人使用gexperts的dbugint.pas界面來調試/除錯。這是個非常優秀的獨立的小程序。假如願意,您可以將它分發給你的客戶們。假如沒有這麼做,像outputdebugstring一樣,假如沒有安裝,它實際上就什麼也不做:)(它將注重終端是否已經安裝在機器上)。要使用dbugint.pas的話很輕易,將它加入你的工程並加上 #include "dbugintf.hpp"(因為是pascal文件,你必須將它加入你的工程以便c++builder編譯器生成hpp頭文件。)然後你只需使用senddebug(“要送到記錄中的字串”);或者你或許想更靈活些,還有senddebugex-增加一個消息類型參數來調用tmsgdlgtype(具體說明參考vcl在線幫助),sendmethodenter, sendmethodexit, and sendseparator 等等(十分自解釋的名字)。只是別忘記加入必須的package包,若你打算將此終端(gdebug.exe)其給你的一些最終用戶的話。gexpert可以從http://www.gexperts.org/ 獲得並且是收費的。

  第三種我要指出的是,這或許是最難的選擇-實現你自己的記錄控制台。可沒有你想的那麼簡單!你或許首先會想到“扔個richedit控件在form上,將它設為只讀的,然後開始記錄,對嗎?” 錯!理論上挺好,但實踐呢,使用richedit控件來記錄將降低程序的運行速度、使內存破碎,丟失、通常會在10分鐘內使整個機器慢下來!!(要說明白為什麼得花上點時間才行,但我向你可以保證)。所以你所需要的是計劃好你的記錄機制的需要,並開始計劃一個定制控件若你想要個彩色的圖標的話。還有一個選擇,需要做點工作,但可是非常有效。就是使用一個listbox控制來記錄,並將style屬性設為lbownerdrawfixed,這樣句柄將會自繪。(這也是gexperts和它的gdebug console所做的)。要做許多工作,但哈哈,假如你想做…

  結合使用記錄機制與類的異常處理機制

  現在進入下一步:)(跟你打賭你從未意識到設置一個優秀的調試/除錯系統需要做如此多的工作!)你不用總是預料各種偶然的異常會發生什麼,而且絕大多數時候當程序經過大量的除蟲測試(盡量攻擊程序,試圖讓它崩潰)後,你根本不用擔心這些。下面這個技術,我建議任何組件開發者第一次在ide中測試一個新組件/新代碼時該當完全遵照。因為在ide中一個異常會帶來很多問題,有時甚至重啟ide也無濟於事(我自己已經這麼做了)。其實也很簡單。在您代碼的每個函數前,或者至少在所有主要的函數前後加上:

  try
{
前端代碼
}
catch(exception &e)
{
senddebugmessage(“exception caught in classname::functionname of type:” +e.classname()
+” with the message:”+e.message);
};

  (並用函數的類名及函數名代替字串中的classname和functionname)。這樣你很快很快就知道異常發生在何處,也不用你強行關閉ide啦。

  okay,是時候回顧一下了。classname()方法是如何幫助我們的?不想每次都只得到一個“exception“串就完了吧?難道是因為將e聲明為一個異常?不對。這是vcl比較酷的部分,任何從tobject繼續來的類能夠主動知道其自身的類型、其基類的類型、等等許多有趣的信息,你可以察看tobject的幫助。所以儘管我們使用的是exception &e,e.classname()將會找出我們得到的異常的實際的類名(譯者註:c++的多態性)。這些好處的代價就是可執行文件的體積更大了,幾乎所有的c++builder/delphi程序員都會發現這一點。(no pain, no gain)沒有痛苦,就沒有收穫.他們說….

  xiphias增加了tstringlist的addingline方法,savetofile方法是另一種記錄(logging)的有效形式。最後該當保證你的應用程序總是寫記錄文件(logfile),或這每次捕捉到異常時重寫記錄文件。

  處理您代碼外產生的異常

  現在的步驟是我們開始學習基於ide的除錯器之前的最後一個基於代碼的步驟。但或許在有嚴峻錯誤發生時,對裝飾應用程序來說這是最重要的步驟。舉例來說,這是顯示一個包括錯誤具體內容的對話框理想的時機。這時彈出在屏幕上的對話框可以方便最終用戶能夠向您報告錯誤。我敢保證您痛恨“oh,有個什麼框子上說在什麼地址發生了個什麼異常錯誤”這樣的報告。其實完全可以很輕易的實現更好的情況,也不會限制你打算如何處理它。第一步是在你的主窗體(例如:工程的主動創建窗體列表中的第一個form)中創建一個象如下這樣的函數:

  void __fastcall applevelexceptionhandler(tobject *sender, exception *e)
{
}


然後加入合適的代碼來顯示錯誤(e->message),錯誤類型(記住e.classname(),只要此時才是它的e.classname()),和聯繫您的具體方法及其他你想加上的任何東西。第二步當然是將它與系統掛鉤,這在c++builder裡很輕易實現:

  application->onexception=applevelexceptionhandler;

  將上一行代碼加到form的 oncreate 事件中。不要吝嗇!你加了這一行後幾乎可以保證不會錯過任何異常,而且無論哪裡異常處理失敗時它都會出現在你的眼前!

  你的回合

  現在你已經得到所有你剛才學習的有用的信息了。是時候開始把它們加到你現在的工程裡去了,否則就忘掉吧,要不然,就把它變成編程習慣的一部分。這是你的自在!

  在這個系列的下個部分,我將討論內建除錯器的使用,來看看你的程序運行時都幹些什麼,如何單步跟蹤代碼、設置斷點、察看變量、和會把新手們嚇的人事不醒的所有其他有趣的工具。直到這裡,您的bugs或許只是小蟲子了吧。

  Okay,(再小小預備一下)現在開始追蹤、搜索經過前次的努力後仍然躲在代碼中的bug的時候了,也就是開始跟蹤前一篇文章代碼裡標記過的bug/異常。首先是預備階段。

  調試可執行程序前的預備

  在我們開始調試可執行程序前,我們需要確保一些設置在大多數情況下的正確性。我將會一條接一條的過一遍,並簡單解釋一下為什麼必須那樣做。(假如您對有些東西感愛好的話,按下幫助按鈕,會有許多更詳盡的內容)。現在就開始吧,先打開Project|Options選項。

  工程選項

  首先我們在"Compiler"(編譯)標籤處停下。您只需簡單的單擊"Full debug"(完全調試模式)按鈕,我們所需的絕大多數的其餘設置就已經搞定了。將"Code optimization"(代碼優化)設為"None"(無)總是件好事,這樣做實際上告訴編譯器:所有的事情都已做好,只需產生氣器碼就行了。而不要為了提高一點點運行速度嘗試進行其他的智能優化。(當然,一切都完成之後,您可以打開此項。)這樣做的好處是大大降低了我們調試的難度。因為程序中的代碼與我們書寫的一樣,沒有被編譯器優化過。在"debugging"(調試)面板中,將"Debug information"(調試信息)選上(點一下),並且必須設置為"Line number information"(行數信息)。我還建議將"Disable inline expansions"(禁用內聯擴展)一項選上。內聯擴展對發佈的代碼來說很好,但調試時最好還是關掉此項,他只會讓您更頭痛。

  然後是"Pascal"標籤,尤其在您的工程裡連接了Pascal單元或使用了基於Pascal的VCL控件時(若您擁有其Pascal源碼時,編譯器會主動使用此節中的設置重新編譯)。這裡您必須將"Optimization"優化選項禁用,然後通常我會將"debugging"(調試)部分的所有選項選上(打鉤)。

  接下來是"Linker"(鏈接)標籤,我們需要選上"Create debug information"(生成調試信息)。"Use dynamic RTL"(使用動態RTL)以及"Don’t generate state files"(不要生成狀態文件)是造成麻煩的選項。我通常都會使用狀態文件(這樣答應增量鏈接,但會在編譯目錄下產生一個4倍於可執行程序或更大的文件),換個角度來說,這樣會增加鏈接大工程時的速度。而使用dynamic RTL本身就是個爭論,尚有很多贊同和反對的討論。

  下一個是"Directories/Conditionals"(路徑/條件)標籤。在這裡我們想要設定"Directories/Conditionals"(調試源路徑)的值。我們永遠都應將此處設定為$(BCB)sourcevcl,但是假如您有任何其他的組件附加的話,通常將它們的路徑也加上是個好主意(路徑與路徑之間用”;”分隔或者您可以用按下…按鈕彈出的對話框來設定它們)。

  最後也是最重要的設置是在"Packages"(程序包)標籤上。根據所有恰如其分的調試經驗您必須禁用"Build with runtime packages"(帶運行時程序包編譯)。這麼做的原因是程序包本身不包括而且不能包括調試信息。這樣做,或許不利於您跟蹤標準的VCL代碼,例如想看清楚VCL函數y中參數x是如何起作用的時候。但是大多數時候,您這麼做將會發現調試器將您的絕大多數“症狀”歸結給VCL,儘管“病因”就在您的源代碼中(或在其他的組件中(這已經在我們所有人身上發生了))。一旦您發佈您的正式版本時,您可以決定是否使用程序包。(譯者註:程序包的本質是一個非凡的DLL,不帶運行程序包(靜態)編譯可以讓您的程序脫離Cbuilder獨立運行。),但在調試時,請禁用掉。按下OK按鈕,我們已經預備好啦。下一個對話框只需打開一次,但最好還是來反省以下我們在這裡的設定是否正確。好了,打開”Tools|Debugger Options…”吧。

  對話框最下方的"Integrated debugging"(集成調試器)選項是要害所在。確信已經打上鉤。按下OK按鈕預備編譯可執行程序吧。我建議重新來一次徹底的編譯(選擇Project|Build All),假如您修改過您的設置的話(尤其是改變”building with packages”方式後)。這將保證我們的所有程序單元按照我們所期望的那樣被編譯。

  設置斷點並闖入可執行程序

  像您所見過的其他任何一款調試器一樣,C++Builder提供強大的斷點設置功能。基本上,斷點是指代碼中的一個點,程序執行至此停下(與退出不同,這只是執行中的暫停)並將控制權交還給調試器。設置一個斷點相當輕易。只需在您想要設置的程序代碼行左側的灰色槽形區域點擊,您會看到一個紅點出現,這一行也會變紅。程序運行到這一點就會暫停,將控制權交還給調試器。

  您或許會問假如我不想每次都停下來呢?當然可以,而且還很輕易做到,這取決於您暫停程序的標準是什麼?(譯者註:條件斷點)。在剛才那個斷點(紅點)上右擊鼠標並從彈出菜單上選擇” Breakpoint Properties”(斷點屬性)。此處可以設定兩種屬性"Condition"(條件)和"Pass Count"(通過次數)。Condition(條件)屬性太方便了。您可以利用if()語句輸入幾乎是任意的條件。但請牢記條件中的所有變量,對此斷點都應是可見的。條件屬性並未被編譯器編譯到執行程序中,而是在運行時,當程序運行至斷點暫停後,反省斷點的條件是否滿足。條件為真,停下,否則讓程序繼續運行。另一個屬性"Pass Count"(通過次數)也很輕易理解。斷點將被通過Pass Count次後停下。結合使用這兩個屬性,在調試您的代碼時,您可以設定非常嚴格的斷點。

  還有一件要牢記的是,當您在調試器中發生異常時,會以產生異常處的那一行代碼上的斷點的形式出現。這種情況很輕易製造。一旦您得到一個異常後應做的步驟我會在當前展示如何在堆棧中回溯並跟蹤找出異常發生的真正原因(如引起異常產生的那一小片代碼)。

  另一個要牢記的提示是當您運行您的程序時,代碼窗口左側有藍點的任意一行都可以設成斷點。所有非法的斷點將會變為紅點中帶一個黃色的小叉,這一行代碼也會變成黃褐色。合法的斷點則變為紅點中帶一個綠色的小鉤。運行時,您可以設置/修改任意一點,斷點立即生效而無須重新編譯。

  察看儲存在變量中的值

  一旦您的程序在您的斷點處停下後,該做什麼?有一件事您想做而且必須做的,那就是察看儲存在您程序中的各種變量真實的值。這部分內容涉及的方面很多,您一定要堅持,忍受這些枯燥的東西。幸運的是當您看完這些,您一定會對調試器這部分最強大的功能有些新的理解。有許多種方法可以察看變量的值,主要要根據您的目的來決定。我會從察看當前函數的Local Variables(局部變量)開始把他們都講完。

  察看局部變量沒有太多可以講的。只需點擊”View|Debug Windows|Local Variables”,或按下ctrl-alt-L將會彈出一個窗口,顯示了當前函數的局部變量。窗口中的變量將會隨您單步向下執行或回溯的函數體的更新而更新。

  使用Watches(觀察)

  下一步您可以通過設定一個variable watch(變量觀察)來察看程序中的變量。就像它的名稱所表達的,觀察一個變量並將其值顯示在變量觀察窗口中(點擊"View|Debug Windows|Watches"或按下 ctrl-alt-W)。您可以通過兩個途徑來添加一個觀察,第一種是在代碼窗口中高亮選擇您要觀察的變量或表達式(是的!它可以理解並對絕大多數簡單表達式求值,比如(i*j)+05 或者 SomeVector[i].Name)並右擊鼠標,選擇"Debug|Add Watch at Cursor"或按下ctrl-f5,就會加入觀察窗口。假如必要,同時會打開觀察窗口

  您還可以通過在觀察窗口的空白處雙擊來添加。這時會彈出添加watch對話框,"Expression"(表達式)域的意思無須多說,但另幾個域我想解釋一下,它們也同樣方便。

  "Repeat count"(重複值)用於您觀察一個已知長度的數組變量(比如一個blah[50]數組)。您要將Expression(表達式)設為數組的名字(本例中是blah)。"Repeat count"設為數組的元素數量(本例中是50)。然後就會顯示數組的每個元素(如:blah[0], blah, blah…)。

  "Digits"(小數位數)用來設定顯示十進制浮點數的小數位數的。下面的點選集合是用來強制設定變量的顯示類型的(將無符號長整數顯示為十六進制格式)。還有一點要非凡說明的是,假如您在watch窗口中用鼠標右擊一個watch後的彈出菜單上會出現"Break When Changed"的選項,這將在變量上設定一個斷點,在此變量發生變化時會暫停程序。

  使用Inspectors(巡視器)

  巡視變量是察看變量中的數據的第三種辦法。也幾乎是觀察完整的類的數據的最佳方法。可以有兩種方法來巡視一個變量。第一種是在local variable window(局部變量窗口)中,雙擊一個變量,將會彈出"Debug Inspector"(調試巡視器)窗口,裡面顯示了這個變量所有的"Data" (variables) (數據(變量))、"Methods" (functions)(方法(函數))和"Properties"(屬性)。假如這是個簡單數據,將會顯示此變量的名稱及其中的值。(譯者註:假如是數組呢?真不錯!)

  您會注重到,Debug Inspector(調試巡視器)很像property editor(屬性編輯器)。當然如此,更加重要的是,事實上您可以在運行時實時改變這些值!!!小心使用啦!改入壞值的後果會讓您有說不出來的悲痛。巡視器的這個能力可用來快速測試(假設的)遊戲關卡(譯者註:似乎FPE,GM),而不用有編譯-運行-修改-編譯-運行的循環。


(舉例巡視Form1)在properties(屬性)頁上,您將會看到某些屬性實際上並沒有顯示其的值,而是顯示了{read=,write=}。假如這些值可以被賦值的話,當您在此區域單擊後,您會注重到一個"?"按鈕出現在屬性的右側。單擊這個按鈕將會系統執行適當的函數來嘗試取回屬性的值。我們可以在這兒舉個例子-就舉Form1的MDIChildCount的屬性吧。在MDIChildCoun的屬性值區域上單擊,在按下"?"按鈕,哇,0(正是非-MDI的程序的指定值)。調試巡視器強大的能力並未到此為止。在巡視器的成員變量的適當區域雙擊可以打開成員變量的巡視窗口,提供與您開始打開窗口一樣的能力。

  巡視器窗口的另一個有用的功能是從對像繼續的能力。這可以在通過在適當區域上右擊選擇"Descend"(繼續)來做到。繼續的後果是產生了一個新的變量。您會注重到頂部的下拉List box中的變量名稱已經換成新的變量名了。您可以直接在ListBox中切換巡視的變量。這使得在對象的不同部分快速切換變得非常簡單,而不會讓大大小小的巡視器窗口煩擾您的工作空間。

  有一點要牢記的是,假如您分開函數,或者分開變量的作用範圍,調試巡視器會失去對變量的跟蹤。若您需要再次察看的話,請重新設置巡視器。但是您在當前函數的代碼中單步運行的話,巡視器會主動刷新。