編寫易于調(diào)試的VC代碼
一 程序的設(shè)計(jì)
要避免錯(cuò)誤,首先要從好的設(shè)計(jì)開始。對(duì)于程序的設(shè)計(jì),需考慮到程序的兩個(gè)特性:
1簡(jiǎn)單性
大多數(shù)常見的錯(cuò)誤來源于程序設(shè)計(jì)中不必要的復(fù)雜成分。一個(gè)好的設(shè)計(jì)應(yīng)該反映問題本身的要求,而不必為了刻意追求“滿足將來的需要”而添加不必要的特性。實(shí)際上,簡(jiǎn)單優(yōu)雅的設(shè)計(jì)比那些復(fù)雜的設(shè)計(jì)更能迎合未來的需求。
2 耦合性
耦合(decoupling)性用來衡量不同對(duì)象之間的依賴程度。松耦合的程序易于理解和實(shí)現(xiàn),易于測(cè)試和維護(hù),且這種程序包含錯(cuò)誤的可能性小,錯(cuò)誤也較容易發(fā)現(xiàn)和清除。
二 編程風(fēng)格
編程風(fēng)格是個(gè)人問題,有很大的隨意性。一個(gè)好的編程風(fēng)格不僅讓代碼易理解,也易于調(diào)試。好的編程風(fēng)格包括:
1 清晰地書寫代碼
如果沒有必要,盡量不要使用語(yǔ)言中的高級(jí)特性,因?yàn)檫@些特性不易于理解和調(diào)試。使用大多數(shù)程序員都能理解的語(yǔ)言成分來書寫代碼不易犯錯(cuò)且易于理解和維護(hù)。
2 編寫結(jié)構(gòu)良好的代碼
當(dāng)程序崩潰時(shí)所能得到的最基本的調(diào)試信息是源代碼文件、問題所在行的行號(hào)和一個(gè)調(diào)用棧(call stack)。調(diào)用棧是調(diào)試程序時(shí)最有幫助的部分,它提供錯(cuò)誤出現(xiàn)的上下文,也就是帶參數(shù)的函數(shù)調(diào)用序列。你書寫的代碼結(jié)構(gòu)越好,調(diào)用棧就能給你越多信息。
3 使用良好的標(biāo)識(shí)符
一個(gè)好名字能使你的代碼更容易被理解和維護(hù)。流行的匈牙利命名法(Hungarian Notation)實(shí)際上是把標(biāo)識(shí)符的意義和表示方法結(jié)合起來?,F(xiàn)在,匈牙利命名法表現(xiàn)出不少的局限性,匈牙利命名法過于看重前綴的作用,對(duì)一個(gè)變量的表達(dá)信息不完整,實(shí)際上并沒有傳遞多少有用信息,它使代碼難于閱讀,難以維護(hù)。一個(gè)好的命名傳統(tǒng)是指示出變量的作用域以便在需要的時(shí)候檢查它的定義,并明確地指出一個(gè)變量是全局的、局部的還是成員數(shù)據(jù)。依賴變量的定義比依賴匈牙利前綴更加有用和可靠。
好的名字能夠用平常的語(yǔ)言概括出該標(biāo)識(shí)符所代表的實(shí)體的含義。在選擇類、函數(shù)、變量的名字時(shí)可以考慮以下幾個(gè)原則:
取簡(jiǎn)單的描述性名字,好的名字能簡(jiǎn)要地概括出這個(gè)標(biāo)識(shí)符代表的含義。
避免簡(jiǎn)寫,簡(jiǎn)寫使標(biāo)識(shí)符難于閱讀和記憶,盡量使用混合大小寫的完整的單詞。
避免相似性的文字,避免混淆。
避免采用一般的或隨機(jī)產(chǎn)生的名字,而應(yīng)采用有實(shí)際意義的名字。如欲從按鈕類派生位圖按鈕,取一個(gè)CBitmapButton,而不是CMyButton。
4 用簡(jiǎn)單的語(yǔ)句行
在VC中,一行可寫多個(gè)語(yǔ)句。但調(diào)試是面向行的,過于復(fù)雜的行難于調(diào)試。因此,從調(diào)試的角度出發(fā),每一個(gè)語(yǔ)句都應(yīng)獨(dú)自成行。
5 使用統(tǒng)一的排列
統(tǒng)一的排列方式使類、變量的定義和語(yǔ)句更加明顯。
6 用括號(hào)使書寫清晰
你不一定能都記住各種運(yùn)算符的優(yōu)先級(jí)和結(jié)合律,而使用多余的括號(hào)并不影響編譯后的代碼。因此,如果你不能確定是否需要括號(hào)時(shí),請(qǐng)加上它。
7 使用好的注釋
用好的注釋能使你的代碼不易出錯(cuò),而且便于其他程序員閱讀,便于理解和維護(hù)。
三 編寫程序時(shí)應(yīng)注意的問題
1 充分利用VC++的特性
可用下列技術(shù)來充分利用VC++的編譯器的特性:
(1)用const代替#define來創(chuàng)建常量;
(2)用enum代替#define來創(chuàng)建常量集合;
(3)用內(nèi)聯(lián)(inline)函數(shù)代替#define;
這三種技術(shù)用C++而不是C預(yù)處理。使用預(yù)處理的問題在于編譯器對(duì)于預(yù)處理器所作的事情一無所知,因此無法用數(shù)據(jù)類型檢查錯(cuò)誤和不一致的地方。預(yù)處理的名字不在符號(hào)表里,因此也不能用調(diào)試工具來檢查預(yù)處理常量。相似地,預(yù)處理宏被編譯進(jìn)去,不能用調(diào)試工具跟蹤。編譯器能充分了解const、enum和inline語(yǔ)句,從而能在編譯的時(shí)候?qū)Τ霈F(xiàn)的問題發(fā)出警告。
但預(yù)處理在很多調(diào)試代碼中起重要作用。調(diào)試代碼經(jīng)常需要從非調(diào)試代碼里面得到不同的行為,而最有效的辦法就是讓預(yù)處理為調(diào)試創(chuàng)建不同的代碼。
(4)用new和delete代替malloc和free;
在創(chuàng)建對(duì)象、類型的安全性和靈活性方面。使用new/delete比malloc/free要好。另外,new可被重載,提供了更大的靈活性。
(5)用輸入輸出流(iostreams)代替stdio。
使用C++輸入輸出流(<<和>>)而不使用C標(biāo)準(zhǔn)輸入輸出庫(kù)(printf/sprintf和scanf/sscanf),有利于安全性和擴(kuò)展性。從調(diào)試的角度來看,標(biāo)準(zhǔn)輸入輸出函數(shù)的最大問題在于編譯器不能對(duì)控制流參數(shù)進(jìn)行任何類型檢測(cè),而輸入輸出流的任何問題都能在編譯時(shí)檢測(cè)出來。
2 使用頭文件
要在頭文件中聲明所有共享的外部符號(hào),而且保留函數(shù)原型中的參數(shù)名。把所有的共享定義放在頭文件中,不要在.cpp文件里面看到extern關(guān)鍵字。
3 初始化變量
在使用變量之前一定要把它們初始化。在初始化之前就使用變量肯定會(huì)產(chǎn)生錯(cuò)誤。通常不需對(duì)對(duì)象進(jìn)行初始化,對(duì)對(duì)數(shù)據(jù)成員應(yīng)在構(gòu)造函數(shù)中初始化。必須明確地為在棧中和堆中分配的數(shù)組和數(shù)據(jù)結(jié)構(gòu)進(jìn)行初始化。對(duì)于對(duì)象,應(yīng)該初始化每個(gè)需要初始化的數(shù)據(jù)成員。因?yàn)樽兞康氖褂檬怯蓛?yōu)化器來檢查的,所以檢測(cè)未初始化的本地變量,發(fā)布版本要比調(diào)試版本要做得好。
4 使用布爾表達(dá)式
C++的布爾類型:bool,值為true和false,大小為一個(gè)字節(jié)。
Windows程序通常用BOOL類型。定義如下:
Typedef int BOOL;
#define FALSE 0
#define TRUE 1
在C++中,一個(gè)布爾表達(dá)式如果為0則為假,其他則為真。因此,對(duì)布爾表達(dá)式應(yīng)該檢查是否問假而不是檢查是否為真。
5 使用句柄和指針
初始化一個(gè)指針時(shí),要么讓其指向一個(gè)有效的內(nèi)存地址,要么設(shè)為0(空指針),避免指針指向無效地址?;厥罩羔?biāo)笇?duì)象時(shí)要重新初始化這個(gè)指針,并且在指針被釋放前為空時(shí)就對(duì)其進(jìn)行處理。對(duì)句柄的處理跟指針一樣。
6 用引用而不是指針做參數(shù)
用指針做函數(shù)的參數(shù)可傳遞一個(gè)空指針,很靈活,但也很容易忘了對(duì)指針進(jìn)行初始化。而引用是對(duì)象的別名,它必須和有效的對(duì)象相關(guān)聯(lián),不存在空的和沒有初始化的引用。當(dāng)在函數(shù)中收到一個(gè)引用參數(shù)時(shí),可以肯定這是一個(gè)有效的對(duì)象。程序用引用做參數(shù)比用指針做參數(shù)更為健壯。
7 強(qiáng)制類型轉(zhuǎn)換(cast)
進(jìn)行數(shù)據(jù)類型的強(qiáng)制類型轉(zhuǎn)換時(shí),將會(huì)調(diào)用相應(yīng)的構(gòu)造函數(shù)或轉(zhuǎn)換函數(shù)來創(chuàng)建一個(gè)新類型的臨時(shí)對(duì)象。對(duì)指針的正確類型轉(zhuǎn)換可消除一個(gè)編譯錯(cuò)誤,但并沒改變指針。強(qiáng)制類型轉(zhuǎn)換破壞了編譯器進(jìn)行類型檢查的功能,而這正是編譯器查找錯(cuò)誤的最有效的機(jī)制。為了保證安全性,每一個(gè)強(qiáng)制類型轉(zhuǎn)換都需要手工進(jìn)行類型檢查。為盡量避免強(qiáng)制類型轉(zhuǎn)換,你可以:避免使用多態(tài)數(shù)據(jù)類型;使用更加廣泛的基類;提供特殊的存取函數(shù);讓編譯器隱式處理類型轉(zhuǎn)換等措施。
8 使用構(gòu)造函數(shù)和析構(gòu)函數(shù)
構(gòu)造函數(shù)需要分配內(nèi)存,創(chuàng)建資源或者打開文件,這些運(yùn)算并不總是成功。構(gòu)造函數(shù)沒有返回值,沒有直接顯示錯(cuò)誤的方法。一個(gè)常見的方法(在很多MFC類中使用)是把對(duì)象創(chuàng)建分為兩步:第一步,讓構(gòu)造函數(shù)以一種不會(huì)出錯(cuò)的方式初始化對(duì)象;第二步,讓某些初始化函數(shù)(如Init或Open)完成工作,這一步可能出錯(cuò)。另一種方法是在構(gòu)造函數(shù)中使用異常:第一步,以不會(huì)出錯(cuò)的方式初始化對(duì)象;第二步,用可能在try段內(nèi)出錯(cuò)的代碼初始化對(duì)象;第三步,在catch代碼里面處理異常。如果出現(xiàn)異常,就會(huì)在構(gòu)造函數(shù)里清除分配的資源,并且再次拋出異常。
異常處理的一個(gè)關(guān)鍵細(xì)節(jié)就是在棧展開的過程中拋出的異常會(huì)終止整個(gè)應(yīng)用程序。在處理異常時(shí)經(jīng)常要調(diào)用析構(gòu)函數(shù),因此析構(gòu)函數(shù)很容易出錯(cuò),一定要保證析構(gòu)函數(shù)的異常在析構(gòu)函數(shù)中得到處理。要保證基類的析構(gòu)函數(shù)是虛函數(shù)。這樣,就算對(duì)象是一個(gè)指向基類的指針,也會(huì)調(diào)用派生類的析構(gòu)函數(shù)。否則,就會(huì)引起資源泄漏(resource leak)。
在VC程序中使用調(diào)試語(yǔ)句
為了更好地對(duì)程序調(diào)試,可以使用如下方法:使用斷言、使用跟蹤語(yǔ)句、使用異常和返回值。
一、斷言
1、基本概念
斷言是一種讓錯(cuò)誤在運(yùn)行時(shí)候自我暴露的簡(jiǎn)單有效實(shí)用的技術(shù)。它們幫助你較早較輕易地發(fā)現(xiàn)錯(cuò)誤,使得整個(gè)調(diào)試過程效率更高。
斷言是布爾調(diào)試語(yǔ)句,用來檢測(cè)在程序正常運(yùn)行的時(shí)候某一個(gè)條件的值是否總為真,它能讓錯(cuò)誤在運(yùn)行時(shí)刻暴露在程序員面前。使用斷言的最大好處在于,能在更解決錯(cuò)誤的發(fā)源地的地方發(fā)現(xiàn)錯(cuò)誤。斷言具有以下特征:
.斷言是用來發(fā)現(xiàn)運(yùn)行時(shí)刻錯(cuò)誤的,發(fā)現(xiàn)的錯(cuò)誤是關(guān)于程序?qū)崿F(xiàn)方面的。
.斷言中的布爾表達(dá)式顯示的是某個(gè)對(duì)象或者狀態(tài)的有效性而不是正確性。
.斷言在條件編譯后只存在于調(diào)試版本中,而不是發(fā)布版本里。
.斷言不能包含程序代碼。
.斷言是為了給程序員而不是用戶提供信息。
使用斷言最根本的好處是自動(dòng)發(fā)現(xiàn)許多運(yùn)行時(shí)產(chǎn)生的錯(cuò)誤,但斷言不能發(fā)現(xiàn)所有錯(cuò)誤。斷言檢查的是程序的有效性而不是正確性,可通過斷言把錯(cuò)誤限制在一個(gè)有限的范圍內(nèi)。當(dāng)斷言為假,激活調(diào)試器顯示出錯(cuò)代碼時(shí),可用Call Stack命令,通過檢查棧里的調(diào)用上下文、少量相關(guān)參數(shù)的值以及輸出窗口中Debug表的內(nèi)容,通常能檢查出導(dǎo)致斷言失敗的原因。_ASSERTE宏(屬于C運(yùn)行時(shí)間庫(kù))還能在斷言失敗時(shí)顯示出失效斷言。下面我們討論一下MFC庫(kù)中的斷言。
2、MFC庫(kù)中的斷言
(1) ASSERT(布爾表達(dá)式)
用MFC時(shí)最好選擇ASSERT宏,它的優(yōu)點(diǎn)是即使出現(xiàn)了WM_QUIT消息也能顯示斷言失效消息框。
(2) VERIFY(布爾表達(dá)式)
VERIFY宏中的布爾表達(dá)式在發(fā)布版本中被保留下來。VERIFY宏簡(jiǎn)化了對(duì)函數(shù)返回值的檢查,一般用來檢查Windows API的返回值。由于VERIFY宏里的布爾表達(dá)式在發(fā)布版本里保留了下來,因此最好盡量不要使用這個(gè)宏以實(shí)現(xiàn)程序代碼和調(diào)試代碼的完全分離。
(3 )ASSERT_VALID(指向CObject派生類對(duì)象的指針)
ASSERT_VALID宏通過調(diào)用重載的AssertValid函數(shù)來確定指向CObject派生類對(duì)象的指針是否有效。無論你什么時(shí)候從CObject派生類中得到一個(gè)對(duì)象,在對(duì)這個(gè)對(duì)象做任何操作之前都應(yīng)該調(diào)用ASSERT_VALID宏。
(4) ASSERT_KINDOF(類名, 指向CObject派生類對(duì)象的指針)
這個(gè)宏用來驗(yàn)證指向CObject派生類對(duì)象的指針是否從某個(gè)特殊類中派生,在調(diào)用它之前先調(diào)用ASSERT_VALID宏。只有在很特殊的場(chǎng)合下才用得到,如檢測(cè)編譯器可能錯(cuò)過的對(duì)象類型問題。
此外,還有兩個(gè)沒有正式文件的ASSERT宏的變種:ASSERT_POINTER(指針,指針類型),ASSERT_NULL_OR_POINTER(指針,指針類型)。
3、什么時(shí)候使用斷言
把斷言看作一種簡(jiǎn)單的制造柵欄的方法,這種柵欄能使錯(cuò)誤在穿過自己時(shí)暴露。
.檢查函數(shù)的輸入
.檢查函數(shù)的輸出
.檢查對(duì)象的當(dāng)前狀態(tài)
.堅(jiān)持邏輯變量的合理性和一致性
.檢查類中的不變量
公有成員函數(shù)比私有和保護(hù)的成員函數(shù)需要更全面的斷言。
不正確地使用斷言會(huì)導(dǎo)致錯(cuò)誤。斷言應(yīng)該檢測(cè)那些在程序正常運(yùn)行的時(shí)候永遠(yuǎn)都不可能出現(xiàn)的狀態(tài)。斷言是用來揭示錯(cuò)誤的,而不是用來糾正運(yùn)行時(shí)刻錯(cuò)誤的。
4、斷言與防御性編程(Defensive Programming)
斷言在調(diào)試的時(shí)候向程序員揭示運(yùn)行時(shí)刻錯(cuò)誤(調(diào)試版本里),而防御性編程使用戶在運(yùn)行程序(發(fā)布版本里)時(shí),當(dāng)出現(xiàn)意外情況時(shí)程序仍能繼續(xù)工作。實(shí)際上,防御性的編程要求程序在檢測(cè)到意外時(shí)返回一個(gè)“安全”的值(比如布爾函數(shù)返回false,指針和句柄返回空值),一個(gè)錯(cuò)誤代碼或者拋出一個(gè)異常來解決問題。特定的防御性編程技術(shù)包括:處理無效函數(shù)參數(shù)和數(shù)據(jù)、出現(xiàn)問題的時(shí)候程序失敗、檢查臨界函數(shù)返回的錯(cuò)誤代碼以及處理異常。需要防御性編程的標(biāo)準(zhǔn)問題包括:錯(cuò)誤的輸入數(shù)據(jù)、內(nèi)存或者硬盤空間不夠、不能打開一個(gè)文件、外部設(shè)備不能訪問、網(wǎng)絡(luò)連接不上或者甚至在程序中還有錯(cuò)誤,目的是保持程序的運(yùn)行狀態(tài)。如果你的程序是防御性的,別忘了使用斷言。如果你使用斷言,也別忘了防御性編程。這兩種技術(shù)最好結(jié)合在一起使用。
二、跟蹤語(yǔ)句
1、基本概念
跟蹤語(yǔ)句(trace statements)可使程序執(zhí)行,并使程序員可對(duì)可變值進(jìn)行查看。它們提供了一個(gè)用于觀察的程序,并且獨(dú)立于一個(gè)交互式的調(diào)試器,但是最具有特色的是它們常用于對(duì)調(diào)試器提供的信息進(jìn)行補(bǔ)充。在VC中,跟蹤消息通常輸出到輸出窗口中的Debug標(biāo)簽,也可以重新輸出到一個(gè)文件中。跟蹤語(yǔ)句的特性如下:
.跟蹤語(yǔ)句用于報(bào)告代碼中重要的運(yùn)行事件。
.跟蹤語(yǔ)句的編譯通常是有條件的,并只存在于調(diào)試版本中,而在發(fā)布版本中不被編譯。
.跟蹤語(yǔ)句不能包含程序代碼或?qū)Τ绦虼a有間接的影響作用。
.跟蹤語(yǔ)句的目的是向程序員提供信息,而不是向用戶。
跟蹤語(yǔ)句也是調(diào)試語(yǔ)句,它可以執(zhí)行程序,并且在運(yùn)行中程序員可以查看變量。跟蹤語(yǔ)句對(duì)于那些使用交互式調(diào)試器很難調(diào)試的程序是很有效的。
跟蹤語(yǔ)句和斷言的區(qū)別如下:
.跟蹤語(yǔ)句是無條件的,斷言是有條件的布爾語(yǔ)句。
.跟蹤語(yǔ)句用于顯示程序執(zhí)行和變量值,不直接顯示bug,斷言用于顯示出bug。
.跟蹤語(yǔ)句將信息輸出到調(diào)試窗口或文件中,可被隨意地忽略,斷言打斷程序的執(zhí)行。
2、MFC中的跟蹤語(yǔ)句
在MFC中,你可以使用TRACE和AfxOutputDebugString宏、CObject::Dump虛擬函數(shù)和AfxDumpStack函數(shù)。TRACE宏由AfxDump實(shí)現(xiàn),AfxDump由AfxOutputDebugString實(shí)現(xiàn)。AfxOutputDebugString宏和AfxDumpStack函數(shù)可以在所有版本中編譯,其他只能在調(diào)試版本中編譯。
(1)TRACE宏有以下形式:
_TRACE(reportType,format);
_TRACE0(reportType,format,arg1);
_TRACE1(reportType,format,arg1,arg2);
_TRACE2(reportType,format,arg1,arg2,arg3);
_TRACE3(reportType,format,arg1,arg2,arg3,arg4);
在MFC中,推薦使用TRACEn宏,當(dāng)使用TRACE宏時(shí)需要使用_T宏來格式化參數(shù)以正確解決Unicode的校正,而TRACEn不需要。
MFC TRACE宏中的一個(gè)缺點(diǎn)是AfxTrace函數(shù)使用一個(gè)512字符固定大小的緩沖區(qū),這使得它在跟蹤長(zhǎng)字符串時(shí)是無用的。
(2)CObject::Dump
CObject類有一個(gè)轉(zhuǎn)儲(chǔ)(dump)虛擬函數(shù),所有繼承CObject的類都可以通過重載這個(gè)函數(shù),輸出它們的值。
3、Visual C++消息Pragma
消息Pragma實(shí)際上是一個(gè)編譯時(shí)的跟蹤語(yǔ)句,你可以使用它來警告在預(yù)處理過程中發(fā)現(xiàn)的潛在的編連(build)問題。典型的例子:
#if (WINVER>=0x0500)
#pragma message (“NOTE:WINVER has been defined as 0x0500 or greater.”)
#endif
消息Pragma是非常有用的,尤其是在復(fù)雜編連中。然而,如果你要檢測(cè)一種特定的問題,而不是潛在的問題,使用#error預(yù)處理來代替打斷編譯會(huì)更直接一些。
每當(dāng)你的程序中有錯(cuò)誤而你想得到更多信息的時(shí)候,你應(yīng)該去查看一下跟蹤消息。由于VC輸出窗口的緩沖區(qū)是有大小限制的,因此如果跟蹤消息數(shù)據(jù)產(chǎn)生的速度超過輸出窗口處理的速度,那么消息會(huì)塞滿緩沖區(qū),導(dǎo)致數(shù)據(jù)丟失。避免這個(gè)問題的簡(jiǎn)單方法是在輸出大量數(shù)據(jù)的代碼段如轉(zhuǎn)儲(chǔ)對(duì)象時(shí),調(diào)用Sleep API函數(shù)。
三、異常
1、基本概念
錯(cuò)誤是一種條件,在這種條件下,如果不執(zhí)行額外的處理,線程就不能正常地執(zhí)行下去。異常是用于處理錯(cuò)誤的。使用異常的一個(gè)很明顯的好處就是它們通過發(fā)出錯(cuò)誤信號(hào),可以讓程序代碼和錯(cuò)誤處理代碼分開,而且不會(huì)讓程序忽略錯(cuò)誤,你不用不斷地檢查函數(shù)的返回值,因此它們將程序代碼簡(jiǎn)單化。另一個(gè)好處是它們不需要嚴(yán)格的編程作風(fēng)。
異常的基本特性:
.異常是基于每個(gè)進(jìn)程而提出并處理的。
.異常不能被線程忽略,必須被處理。
.未處理的異常會(huì)使進(jìn)程結(jié)束,而不僅僅是結(jié)束線程。
.異常出來在釋放棧時(shí)會(huì)釋放所有的棧對(duì)象,避免了資源的漏洞。
.異常處理需要大量的額外操作,使得它不適于經(jīng)常運(yùn)行的代碼。
.可以拋出任何類型的異常對(duì)象,除了整數(shù)。
如果正確執(zhí)行,異常處理有下面的特性:
.異常是不是正常的運(yùn)行結(jié)果,是特殊情況。
.異常在返回值無效的情況下使用。
.異常是可靠的,不可能被忽略。
.異常簡(jiǎn)化了錯(cuò)誤處理,簡(jiǎn)化了程序代碼,使錯(cuò)誤處理更加方便。
Visual C++的默認(rèn)情況下,在調(diào)試版本中處理異常,而在發(fā)布版本中并不進(jìn)行處理。由于異常也是錯(cuò)誤,Windows異常碼采用了同Windows錯(cuò)誤碼一樣的位映射模式,為一個(gè)32位的值,這些碼由Microsoft定義,任何異常碼的最高四位總是1100(二進(jìn)制),即十六進(jìn)制里的0xC。
2、Windows結(jié)構(gòu)異常和C++異常
Windows結(jié)構(gòu)異常作為硬件異常(如訪問非法或被零除)或操作系統(tǒng)異常的結(jié)果被拋出,C++異常只能由throw語(yǔ)句拋出。Windows結(jié)構(gòu)異常處理不能處理對(duì)象的解析,因此你應(yīng)該在C++程序中一直使用C++異常。然而,C++異常不能處理硬件和操作系統(tǒng)異常,你的程序需要將結(jié)構(gòu)異常轉(zhuǎn)化為C++異常。C++異常并不直接從你的程序代碼中拋出而是從C++運(yùn)行庫(kù)中拋出,因此你需要調(diào)用棧窗口來返回你的代碼。為了正確處理硬件和操作系統(tǒng)異常,你可以創(chuàng)建自己的異常類并使用_set_se_translator函數(shù)安裝一個(gè)結(jié)構(gòu)異常向C++異常的轉(zhuǎn)化器,但不要捕獲那些不能恢復(fù)所產(chǎn)生問題的轉(zhuǎn)化后的結(jié)構(gòu)異常。
3、MFC中的異常
在MFC中,所有的異常對(duì)象都是從CException基類(它有使用起來非常方便的GetErrorMessage和ReportError成員函數(shù))中派生來的。大多數(shù)的MFC異常對(duì)象都是動(dòng)態(tài)分配的,而且當(dāng)它們被捕獲時(shí),必須被刪除,而沒有被捕獲的MFC異常由MFC本身在AfxCallWndProc函數(shù)中捕獲并刪除。
4、異常的開銷
當(dāng)拋出C++異常時(shí),函數(shù)調(diào)用鏈將從此回溯搜索,尋找可以處理拋出這類異常的處理器。若沒找到,進(jìn)程結(jié)束。如果找到,調(diào)用棧將被釋放,所有的自動(dòng)(局部)變量也將釋放,然后棧將被整理為異常處理器的上下文相關(guān)設(shè)備。因此異常開銷由一個(gè)異常處理器目錄和一個(gè)活動(dòng)的自動(dòng)變量表(它需要額外的代碼、內(nèi)存,而且不論異常是否拋出,都會(huì)運(yùn)行),還得加上函數(shù)調(diào)用鏈的搜索、自動(dòng)變量的解析和棧的調(diào)整(它只在拋出異常的時(shí)候需要執(zhí)行)組成。
5、異常策略
(1)拋出時(shí)機(jī)
拋出異常的時(shí)機(jī)應(yīng)該是一個(gè)函數(shù)發(fā)現(xiàn)一個(gè)錯(cuò)誤,如果沒有一些特殊的操作,該錯(cuò)誤能阻止程序正常的運(yùn)行,而這種操作它自己不能完成,或是在函數(shù)不可能有返回值的時(shí)候。
使用異常處理更簡(jiǎn)單,更可靠,更有效,可以創(chuàng)建更健壯的代碼。然而,應(yīng)該只在意外的情況下使用異常處理。如果你認(rèn)為一個(gè)指針應(yīng)該是空值,這種條件下就直接在代碼中檢查這個(gè)值,而不要使用異常。
(2)何時(shí)捕獲
對(duì)于這個(gè)問題,有一些可能的標(biāo)準(zhǔn):
.當(dāng)函數(shù)知道如何處理這個(gè)異常時(shí)。
.當(dāng)這個(gè)函數(shù)可以合理地處理這個(gè)異常而高級(jí)的函數(shù)不知道如何處理時(shí)。
.當(dāng)拋出異常可能使進(jìn)程崩潰時(shí)。
.當(dāng)函數(shù)可以繼續(xù)執(zhí)行它的任務(wù)時(shí)。
.當(dāng)需要整理分配好的資源時(shí)。
異常處理的一個(gè)缺點(diǎn)是它可能導(dǎo)致資源的泄露。因此,防止資源泄露更應(yīng)該是保持程序異常安全的一部分。棧釋放時(shí)會(huì)自動(dòng)整理局部變量,但不包括動(dòng)態(tài)分配的變量??梢允褂弥悄?smart)指針來保護(hù)你的代碼在存在異常的情況下不會(huì)產(chǎn)生資源泄漏。
(3)怎樣捕獲
.非MFC的C++異常應(yīng)該通過引用來捕獲。使用引用捕獲異常不需要?jiǎng)h除異常對(duì)象(因?yàn)槭褂靡貌东@的異常會(huì)在棧中傳送),而且它保留了多態(tài)性(因此你捕獲的異常對(duì)象正是你拋出的異常對(duì)象)。
.MFC異常應(yīng)該通過指針來捕獲。使用指針捕獲異常需要你刪除對(duì)象。因?yàn)樗鼈兺ǔ亩阎蟹峙?,?dāng)你處理完異常之后,需要調(diào)用Delete成員函數(shù)來刪除。你不可以使用省略捕獲處理器捕獲MFC異常,這會(huì)導(dǎo)致一個(gè)內(nèi)存泄露。必須使用Delete成員函數(shù)刪除MFC異常,而不用delete,因?yàn)橐恍㎝FC異常為靜態(tài)對(duì)象創(chuàng)建。
在釋放棧的過程中拋出異常會(huì)導(dǎo)致進(jìn)程的終止。釋放棧涉及到調(diào)用析構(gòu)函數(shù),異常可以阻止調(diào)用delete操作符,這樣會(huì)有資源泄漏,因此異常最好不要從析構(gòu)函數(shù)中拋出。如果非要在析構(gòu)函數(shù)里拋出異常,必須妥善處理,避免資源泄漏。
6、異常與防御性編程
在異常發(fā)生時(shí)繼續(xù)執(zhí)行程序,遠(yuǎn)比執(zhí)行一個(gè)正常的關(guān)閉動(dòng)作要重要。如果可能,應(yīng)該將精力集中在繼續(xù)執(zhí)行程序,并在必須的情況下才正常地關(guān)閉程序。可能最根本的正常關(guān)閉是一個(gè)在崩潰時(shí)可以重新啟動(dòng)自己的進(jìn)程,這是Windows資源管理器使用的一種技術(shù)。
如果一個(gè)與錯(cuò)誤相關(guān)的C++異常是可預(yù)料的,如果它發(fā)生在非關(guān)鍵性的代碼中,如果它不是發(fā)生在程序啟動(dòng)或結(jié)束過程中或一個(gè)不可恢復(fù)的結(jié)構(gòu)異常的結(jié)果中,這個(gè)程序就可以從其中恢復(fù)。
一旦你的程序可以從與錯(cuò)誤相關(guān)的異常中恢復(fù),應(yīng)該先檢查程序的狀態(tài)和它的文檔。如果程序和文檔已經(jīng)被破壞了,進(jìn)程也應(yīng)該終止運(yùn)行。否則,程序需要通知客戶機(jī)確定動(dòng)作的過程。如果客戶機(jī)同意執(zhí)行下去,程序應(yīng)該恢復(fù)錯(cuò)誤并繼續(xù)執(zhí)行。
四、返回值
并不是在所以場(chǎng)合下都能使用異常,如在使用Windows API編程或帶有COM編程時(shí)并不使用異常。在異常不適合的時(shí)候,使用返回值是一個(gè)好的辦法。
返回值的基本特性:
.返回值可以指示正常和不正常的函數(shù)運(yùn)行,但不能阻止線程的繼續(xù)運(yùn)行。
.返回值很容易被忽略。
.返回值在典型情況下是一個(gè)整數(shù),通常映射符合于一個(gè)預(yù)定義的值。
.返回值能高效地傳遞和接收。
因此,返回值最適合用于以下的情形:
.用于非錯(cuò)誤的狀態(tài)信息
.用于大多數(shù)情況下可以隨意忽略而不會(huì)出問題的錯(cuò)誤。
.用于更易于出現(xiàn)在循環(huán)中的錯(cuò)誤。
.用于中間語(yǔ)言模塊如COM組件中的錯(cuò)誤。
使用Visual C++調(diào)試器調(diào)試
一、調(diào)試版本與發(fā)布版本
有時(shí)程序能在調(diào)試版本運(yùn)行但不能運(yùn)行于發(fā)布版本,反之也有可能。一般說來,一個(gè)發(fā)布版本意味著某些類型的優(yōu)化,而一個(gè)調(diào)試版本則沒有優(yōu)化。下面我們來看看它們的區(qū)別:
1、特別針對(duì)調(diào)試版本的編譯選項(xiàng)
(1)/MDd,/MLd或者/MTd
調(diào)試版本的運(yùn)行時(shí)刻庫(kù)有調(diào)試符號(hào),使用了調(diào)試堆,調(diào)試堆的目的是發(fā)現(xiàn)內(nèi)存破壞和內(nèi)存泄漏,并且向用戶報(bào)告源代碼的哪個(gè)地方出了問題。特性:
.調(diào)試版本的運(yùn)行時(shí)刻庫(kù)對(duì)內(nèi)存的分配作了跟蹤,允許用戶檢查內(nèi)存泄漏。
.在剛分配的內(nèi)存里寫上0xCD的字節(jié)模式,用0xCD來填充剛分配的內(nèi)存,有助于發(fā)現(xiàn)數(shù)據(jù)未被初始化的錯(cuò)誤。
.在被釋放的內(nèi)存寫上0xDD的字節(jié)模式,有助于發(fā)現(xiàn)已被釋放的內(nèi)存。
.在緩沖區(qū)的兩邊分配了四字節(jié)的保護(hù)數(shù)據(jù),并用0xFD的字節(jié)模式作初始化,來檢查寫內(nèi)存的上溢出和下溢出。
.在每個(gè)內(nèi)存分配的地方對(duì)源代碼文件名和行號(hào)作了記錄,有助于用戶在源代碼中對(duì)內(nèi)存分配進(jìn)行定位。
(2)/Od
這個(gè)選項(xiàng)用來關(guān)閉優(yōu)化開關(guān)。因?yàn)槲幢粌?yōu)化的代碼直接對(duì)應(yīng)于源代碼,所以比優(yōu)化后的代碼更容易讀懂。未被優(yōu)化的代碼編譯和鏈接會(huì)更快,會(huì)有更短的調(diào)試周期。而由于優(yōu)化,發(fā)布版本不見得會(huì)比調(diào)試版本運(yùn)行得好,優(yōu)化代碼要求編譯器做一些假設(shè),去除冗余,但有時(shí)這個(gè)假設(shè)是錯(cuò)誤的,并且去掉的冗余也有可能隱藏錯(cuò)誤。如發(fā)布版本的幀指針(EBP寄存器)省略(FPO)隱藏了函數(shù)原型不匹配的錯(cuò)誤;在同步異常模式(只能由throw語(yǔ)句拋出,編譯器默認(rèn),由/GX編譯選項(xiàng)設(shè)置)下,異常處理程序可能被優(yōu)化掉,會(huì)阻止程序中的C++異常處理代碼安全地捕獲結(jié)構(gòu)異常,在這種情況下,你必須使用異步異常模式(采取任何指令都會(huì)產(chǎn)生異常的機(jī)制,由/Eha編譯選項(xiàng)設(shè)置)。
(3)/D “_DEBUG”
打開條件編譯調(diào)試代碼開關(guān)。只有這個(gè)符號(hào)被定義,調(diào)試代碼才會(huì)被編譯,MFC使用_DEBUG符號(hào)來確定到底鏈接的是哪個(gè)版本的MFC類庫(kù)。在調(diào)試版本中,內(nèi)聯(lián)默認(rèn)情況下是被關(guān)閉的。
(4)/ZI
創(chuàng)建編輯繼續(xù)(Edit and Continue)的程序數(shù)據(jù)庫(kù)。這個(gè)選項(xiàng)會(huì)打開/GF編譯選項(xiàng),/GF編譯選項(xiàng)會(huì)消除重復(fù)字符串,并將字符串放到只讀內(nèi)存。編輯繼續(xù)功能需要獲取存儲(chǔ)在PDB文件里的特殊信息來使得代碼的修改對(duì)調(diào)試器有效。如果被修改文件對(duì)應(yīng)的信息不在PDB文件里,編輯繼續(xù)功能就不能進(jìn)行,而且在調(diào)試過程中對(duì)代碼的任何修改都會(huì)出現(xiàn)下面的提示信息“One or more files are out of date or do not exist.”。
(5)/GZ
在調(diào)試版本中用來發(fā)現(xiàn)那些在發(fā)布版本里才發(fā)現(xiàn)的錯(cuò)誤。其作用如下:
.用0xCC模式初始化自動(dòng)(本地)變量。
.在通過函數(shù)指針調(diào)用函數(shù)時(shí),檢查棧指針,確認(rèn)是否有調(diào)用規(guī)則不匹配。
.在函數(shù)最后檢查棧指針是否被改變。
(6)/Gm
打開最小化重新鏈接開關(guān),減少鏈接時(shí)間。
2、特別針對(duì)發(fā)布版本的編譯選項(xiàng)
(1)/MD,/ML或者/MT
使用發(fā)布版本的運(yùn)行時(shí)刻庫(kù)。
(2)/O1或者/O2
打開優(yōu)化開關(guān),使得程序會(huì)最小或說速度會(huì)最快,優(yōu)化器還可能發(fā)現(xiàn)代碼中潛在的錯(cuò)誤,而這些錯(cuò)誤可能會(huì)被調(diào)試版本掩蓋。
(3)/D “NDEBUG”
關(guān)閉條件編譯調(diào)試代碼開關(guān)。
(4)/GF
消除重復(fù)字符串并將它們放到只讀內(nèi)存中以避免被錯(cuò)誤地修改。
(5)/Zi
創(chuàng)建包含調(diào)試符號(hào)的程序數(shù)據(jù)庫(kù)。
如果一個(gè)錯(cuò)誤只發(fā)生在發(fā)布版本里,除非你是個(gè)匯編高手,否則你需要調(diào)試符號(hào)來提示你到底程序出現(xiàn)了什么問題,調(diào)試符號(hào)保存在程序的數(shù)據(jù)庫(kù)文件(PDB)中。Visual C++的AppWizard默認(rèn)情況下沒有為發(fā)布版本創(chuàng)建調(diào)試符號(hào)。為創(chuàng)建調(diào)試符號(hào),打開工程設(shè)置對(duì)話框,選擇Win32 Release,在C/C++標(biāo)簽里選擇Common類,在調(diào)試信息里,如果是發(fā)布版本選擇Program Database,如果是調(diào)試版本選擇Program Database for Edit and Continue(編輯繼續(xù)選項(xiàng)與優(yōu)化鏈接不相容,不適于發(fā)布版本)。在Link標(biāo)簽里選擇Debug類,然后選擇Debug Info和Microsoft format選項(xiàng),最好不要選擇Separate types選項(xiàng),這樣所有的調(diào)試信息才會(huì)被合并到單獨(dú)的一個(gè)PDB文件中。對(duì)于發(fā)布版本,選擇Link標(biāo)簽,在Project options對(duì)話框的最后加上“/OPT:REF”,這個(gè)選項(xiàng)使得不被引用的函數(shù)和數(shù)據(jù)不會(huì)出現(xiàn)在可執(zhí)行文件中,避免了文件的無謂增大。對(duì)于調(diào)試版本不要使用這個(gè)選項(xiàng),它會(huì)關(guān)閉增量鏈接(incremental linking)。
二、Visual C++編輯器的“設(shè)置”菜單
當(dāng)你打開或新建一個(gè)包含至少一個(gè)工程的Workspace后,Visual C++的Project菜單中的“Settings…”命令就變?yōu)橛行?,選擇它或者按下熱鍵Alt+F7后,便可調(diào)出工程設(shè)置對(duì)話框,這里面的選項(xiàng)將影響整個(gè)工程的建立和調(diào)試過程,因此很重要。
在這個(gè)對(duì)話框中,左上方的下拉列表框用于選擇一種工程配置,包括有Win32 Debug、Win32 Release和All Configurations(指前兩種配置一起),某些選項(xiàng)在不同的工程配置中有不同的缺省值。左邊的樹形視圖給出了當(dāng)前工程所有的文件及分類情況。下面我們就以Win32 Debug為例來看看與工程有關(guān)的的四個(gè)主要選項(xiàng)卡的各自功能與含義(一共有十個(gè)選項(xiàng)卡):
1、 General選項(xiàng)卡
這個(gè)選項(xiàng)卡比較簡(jiǎn)單,從上向下的第一個(gè)選項(xiàng)用于更改使用MFC類庫(kù)的方式: DLL的方式或是靜態(tài)連接。我們可以在兩種方式之間進(jìn)行切換。第二個(gè)選項(xiàng)用于指定在編譯連接過程中生成的中間文件和輸出文件的存放目錄,對(duì)于調(diào)試版本來說,缺省的目錄是工程下面的“Debug”子目錄。第三個(gè)選項(xiàng)用于指定是否允許每種工程配置都有自己的文件依賴關(guān)系(主要指頭文件),由于絕大多數(shù)工程的調(diào)試版本和發(fā)布版本都具有相同的文件依賴關(guān)系,所以通常不需要更改該選項(xiàng)。
2、 Debug選項(xiàng)卡
Debug選項(xiàng)卡中是一些與調(diào)試有關(guān)的選項(xiàng),由于選項(xiàng)比較多,它們被分成了幾個(gè)類,我們可以從Category中選擇不同的類別,選項(xiàng)卡就會(huì)切換顯示出相應(yīng)的選項(xiàng)。
在General類別中,可以指定要調(diào)試的可執(zhí)行文件名。另外三個(gè)選項(xiàng)可以指定用于調(diào)試的工作目錄,開始調(diào)試時(shí)給程序傳送的命令行參數(shù),以及進(jìn)行遠(yuǎn)程調(diào)試時(shí)可執(zhí)行文件的路徑。
3、C/C++選項(xiàng)卡
C/C++選項(xiàng)卡控制著Visual C++的編譯器,其中的選項(xiàng)比較多。下面有一個(gè)Project Options編輯框,里面列出的各種命令開關(guān)將會(huì)在開始編譯時(shí)作為命令行參數(shù)傳送給Visual C++的編譯器。這些命令開關(guān)會(huì)跟隨其它選項(xiàng)改變而改變。
在General類別中,Warning level用于指定編譯器顯示警告的級(jí)別,如果選中了Warnings as errors,那么顯示的每一個(gè)警告都將會(huì)引起一個(gè)錯(cuò)誤,這樣在編譯完畢后就無法啟動(dòng)連接器來進(jìn)行連接。Optimizations用于設(shè)置代碼優(yōu)化方式,優(yōu)化的目的主要有提高運(yùn)行速度和減小程序體積兩種,但有時(shí)候這兩種目的是相互矛盾的。另外,在極少數(shù)情況下,不進(jìn)行優(yōu)化,程序能正常運(yùn)行,打開了優(yōu)化措施之后,程序卻會(huì)出現(xiàn)一些莫名其妙的問題。其實(shí)這多半是程序中有潛在的錯(cuò)誤,關(guān)閉優(yōu)化措施往往只是暫時(shí)解決問題。Debug info用于指定編譯器產(chǎn)生的調(diào)試信息的類型,為了使用Visual C++的即編即調(diào)功能,必須在這里選擇生成“Program Database for Edit and Continue”類型的調(diào)試信息。Preprocessor definitions是一些預(yù)先定義的宏名。
C++ Language類別中的選項(xiàng)涉及到了C++語(yǔ)言的一些高級(jí)特性,包括有成員指針的表示方式、異常處理、運(yùn)行時(shí)類型信息,一般情況下都不用改變它們。Code Generation類別中的選項(xiàng)涉及如何生成目標(biāo)代碼,一般情況下保持缺省值即可。在Customize類別中,從上到下六個(gè)選項(xiàng)的含義分別為:是否禁止使用Microsoft對(duì)C++的擴(kuò)展;是否允許函數(shù)級(jí)別的連接;是否消除重復(fù)的字符串;是否允許進(jìn)行最小化的重建;是否允許遞增編譯方式;是否允許編譯器在開始運(yùn)行時(shí)向Output窗口中輸出自己的版本信息。
在Listing Files類別中,我們可以指定編譯器生成瀏覽信息和列表文件(Listing file),前者可由瀏覽信息維護(hù)工具BSCMAKE生成瀏覽信息文件,后者則包含了C/C++源文件經(jīng)過編譯后對(duì)應(yīng)的匯編指令。Optimizations類別允許我們對(duì)優(yōu)化措施進(jìn)行更細(xì)微的控制,選擇了Customize后,便可以選擇進(jìn)行哪幾項(xiàng)優(yōu)化,在Inline function expansion中我們可以指定對(duì)內(nèi)聯(lián)函數(shù)的擴(kuò)展方式。Precompiled Headers類別中是關(guān)于預(yù)編譯頭文件的一些選項(xiàng),一般情況下都不用更改。Preprocessor類別中是關(guān)于預(yù)處理的一些選擇。
4、Link 選項(xiàng)卡
Link選項(xiàng)卡控制著Visual C++的連接器。在General類別中,可以指定輸出的文件名,以及一些在連接過程中需要使用的額外的庫(kù)文件或目標(biāo)文件,下邊五個(gè)選項(xiàng)的含義分別為:生成調(diào)試信息;忽略所有缺省的庫(kù)文件;允許遞增連接方式(這種方式可以加快連接的速度);生成MAP文件;允許進(jìn)行性能分析。在Customize中選中Use program database允許使用程序數(shù)據(jù)庫(kù)。在Debug類別中,我們可以指定調(diào)試信息的類別是Microsoft的格式,還是COFF格式,或者兩種都有,選中Separate types后連接器會(huì)把調(diào)試信息分開放在PDB文件中,這樣連接起來會(huì)更快一些,但調(diào)試時(shí)速度卻會(huì)慢一些。Input類別中是一些與輸入庫(kù)文件有關(guān)的選項(xiàng),我們可以在這里指定使用或不使用某些庫(kù)文件或目標(biāo)文件。Output類別中則是一些與最終輸出的可執(zhí)行文件有關(guān)的選項(xiàng),一般情況下都不用改變。
三、Visual C++調(diào)試工具
1、調(diào)試窗口
(1)觀察窗口(Watch)
調(diào)試程序時(shí),可使用觀察窗口監(jiān)視變量和表達(dá)式。
(2)快速查看窗口(Quick watch)
功能和觀察窗口差不多。
(3)變量窗口(Variables)
變量窗口有三個(gè)標(biāo)簽:Auto標(biāo)簽顯示了當(dāng)前語(yǔ)句和前一條語(yǔ)句用到的變量,Locals標(biāo)簽顯示當(dāng)前函數(shù)的局部變量,this標(biāo)簽顯示了this指針執(zhí)行的對(duì)象。
(4)寄存器窗口(Register)
可以監(jiān)視CPU的寄存器、標(biāo)志值以及浮點(diǎn)堆棧
(5)內(nèi)存窗口(Memory)
可顯示從一特定地址開始的虛擬內(nèi)存。Address框允許你指定從哪個(gè)虛擬內(nèi)存地址開始顯示。
(6)調(diào)用棧窗口(Call stack)
可顯示引起當(dāng)前源代碼語(yǔ)句執(zhí)行的一系列函數(shù)調(diào)用,當(dāng)前函數(shù)在堆棧的頂端。
(7)反匯編窗口(Disassembly)
可查看編譯器生成的對(duì)應(yīng)于源代碼的匯編指令。
2、調(diào)試符號(hào)
程序數(shù)據(jù)庫(kù)文件(.pdb)包含了Visual C++調(diào)試器所需的調(diào)試信息和程序信息。調(diào)試信息包含了變量的名字和類型、函數(shù)原型、源代碼行號(hào)、類和結(jié)構(gòu)的布局、FPO調(diào)試信息(重建堆棧幀)以及進(jìn)行增量鏈接所需的信息。對(duì)于設(shè)置了Program Database for Edit and Continue選項(xiàng)的程序,PDB還要包含執(zhí)行編輯繼續(xù)功能所需的信息。
3、使用斷點(diǎn)
斷點(diǎn)(BreakPoint)是運(yùn)行你向調(diào)試器描述環(huán)境,并讓調(diào)試器設(shè)置好程序狀態(tài)的一種機(jī)制。如果沒有斷點(diǎn),只有在程序里一步一步跟蹤使用調(diào)試器。在Visual C++中,你可以設(shè)置三種類型的斷點(diǎn):代碼定位斷點(diǎn)、數(shù)據(jù)斷點(diǎn)和消息斷點(diǎn)。
四、提高調(diào)試器的查錯(cuò)能力
盡量采用編譯時(shí)刻檢查而不是運(yùn)行時(shí)刻檢查。
1、使用最高的編譯警告級(jí)別/W4
象if(x=2)這樣的語(yǔ)句,默認(rèn)的警告級(jí)別為/W3時(shí)不顯示任何信息,但改成最高警告級(jí)別/W4時(shí)則會(huì)出現(xiàn)“waning C4706:assignment within conditional expression”的警告。/W4能給出一些/W3所不能給的警告。
2、在調(diào)試版本中使用/GZ編譯選項(xiàng)
/GZ選項(xiàng)用來發(fā)現(xiàn)那些在發(fā)布版本里才發(fā)現(xiàn)的錯(cuò)誤,包括未被初始化的自動(dòng)(局部)變量、堆棧錯(cuò)誤、不正確的函數(shù)原型等。
3、使用#pragma warning編譯器指示
你可以使用#pragma warning編譯器指示來禁止整個(gè)程序、特定的頭文件、特定的代碼文件或是特定的某一行代碼的特定警告,這看你把#pragma放在哪里。
4、使用沒有警告的編譯法則/WX
這個(gè)編譯選項(xiàng)把所有的警告當(dāng)成錯(cuò)誤來對(duì)待,只有在假警告被消除之后才能應(yīng)用。有時(shí)編譯警告可能是合理的,處理編譯警告的核心是要發(fā)現(xiàn)錯(cuò)誤,而不是抑制警告本身。這個(gè)法則對(duì)于大的程序開發(fā)小組來說很有幫助。最終目標(biāo)是消除錯(cuò)誤,而不是消除警告。
五、內(nèi)存空間與分配
1、內(nèi)存分配錯(cuò)誤
動(dòng)態(tài)內(nèi)存分配錯(cuò)誤有兩種基本類型:內(nèi)存錯(cuò)誤和內(nèi)存泄漏。
(1)內(nèi)存錯(cuò)誤
當(dāng)一個(gè)指針或者該指針?biāo)赶虻膬?nèi)存單元成為無效單元,或者內(nèi)存中分配的數(shù)據(jù)結(jié)構(gòu)被破壞時(shí),就會(huì)造成內(nèi)存錯(cuò)誤。指針未被初始化,指針被初始化為一個(gè)無效地址,指針被不小心錯(cuò)誤地修改,在與指針相關(guān)聯(lián)的內(nèi)存區(qū)域被釋放后使用該指針(這種指針被稱為虛懸(dangling)指針),這些都會(huì)使指針變?yōu)闊o效指針。當(dāng)通過一個(gè)錯(cuò)誤指針或者虛懸指針對(duì)內(nèi)存進(jìn)行寫入,或者將指針強(qiáng)制轉(zhuǎn)換為不匹配的數(shù)據(jù)結(jié)構(gòu),又或者是寫數(shù)據(jù)越界,內(nèi)存自身也會(huì)遭到破壞。刪除未被初始化的指針、刪除非堆指針、多次刪除同一指針或者覆蓋一個(gè)指針的內(nèi)部數(shù)據(jù)結(jié)構(gòu),都會(huì)造成內(nèi)存分配系統(tǒng)錯(cuò)誤。
(2)內(nèi)存泄漏
內(nèi)存泄漏在被動(dòng)態(tài)分配的內(nèi)存沒有被釋放時(shí)產(chǎn)生。有許多情況會(huì)導(dǎo)致內(nèi)存泄漏,如沒有在程序的全部執(zhí)行路徑中釋放內(nèi)存,沒有在析構(gòu)函數(shù)中釋放所有的內(nèi)存等。一個(gè)程序在崩潰之前可運(yùn)行的時(shí)間越長(zhǎng),則導(dǎo)致崩潰的原因與內(nèi)存泄漏的關(guān)系越大。
Windows會(huì)在程序結(jié)束的時(shí)候?qū)⑿孤┑膬?nèi)存收回,因此內(nèi)存泄漏是個(gè)暫時(shí)性的問題。但為什么必須消除內(nèi)存泄露呢?首先,內(nèi)存泄漏往往會(huì)導(dǎo)致系統(tǒng)資源的泄漏。動(dòng)態(tài)分配內(nèi)存往往不僅僅代表一塊存儲(chǔ)區(qū)域,還代表了某些類型的系統(tǒng)資源,如文件、窗口、設(shè)備上下文、GDI對(duì)象等。其次,高質(zhì)量的程序和特定的服務(wù)器程序必須能夠無限地運(yùn)行下去。最后,內(nèi)存泄漏往往是其他程序錯(cuò)誤或不良編程習(xí)慣的征兆。
導(dǎo)致內(nèi)參泄漏的原因:忘記釋放內(nèi)存;構(gòu)造函數(shù)失??;存在內(nèi)存泄漏的析構(gòu)函數(shù);存在內(nèi)存泄漏的異常處理程序;多個(gè)返回語(yǔ)句;使用錯(cuò)誤形式的delete。
2、關(guān)于內(nèi)存的初始化
在調(diào)試版本里,堆里未被初始化的內(nèi)存被0xCD字節(jié)模式填充,堆里釋放的內(nèi)存被0xDD字節(jié)模式填充。堆棧里被初始化的內(nèi)存被0xCC字節(jié)模式填充。調(diào)試版本和發(fā)布版本里,未被初始化的全局內(nèi)存都被初始化為0。
3、內(nèi)存虛擬地址空間
Windows使用一組固定的范圍來分割進(jìn)程的4GB虛擬地址空間,因此有時(shí)可通過查看指針的返回值來判斷指針是否有效。
(1)Windows2000虛擬地址空間劃分
0~0XFFFF(64KB):不能用來檢測(cè)空指針賦值(訪問沖突)
0x10000(64KB)~0x7FFEFFFF(2GB-64KB):Win32進(jìn)程私有的(非保留的),用于程序代碼和數(shù)據(jù)
0x7FFF0000(2GB-64KB)~0x7FFFFFFF(2GB):不能用來防止覆蓋OS分區(qū)(訪問沖突)
0x800000000(2GB)~0xFFFFFFFF(4GB):為操作系統(tǒng)保留,不可訪問(訪問沖突)
(2)Windows2000虛擬地址空間使用
0x00030000~0x0012FFFF:線程棧
0x00130000~0x003FFFFF:堆(有時(shí)堆位于此處)
0x00400000~0x005FFFFF:可執(zhí)行代碼
0x00600000~0x0FFFFFFF:堆(有時(shí)堆位于此處)
0x10000000~0x5FFFFFFF:App DLLs、Msvcrt.dll、Mfc42.dll
0x77000000~0xFFFFFFFF:Advapi32.dll、Comctl32.dll、Gdi32.dll、Kernel32.dll、Ntdll.dll、Rpcrt4.dll、Shell32.dll、User32.dll
其中,0x00400000是所有版本的Windows能使用的最低基地址。
六、一些調(diào)試技術(shù)
1、調(diào)試死循環(huán)
使用Debug菜單下的Break命令。在Windows2000中,如果程序有輸入請(qǐng)求,可以使用F12鍵中斷程序,然后檢查窗口的調(diào)用棧,或單步跟蹤代碼找到死循環(huán)的發(fā)生原因。
2、用Spy++調(diào)試與消息有關(guān)的問題
調(diào)試消息的最好方案是使用Visual C++提供的Spy++工具。Spy++允許程序員查看窗口、消息、進(jìn)程和線程。Spy++默認(rèn)的消息輸出:第一欄顯示行號(hào)。第二欄顯示接受消息的句柄。第三欄中的“S”表示消息是用SendMessage發(fā)出的,“P”代表消息是由PostMessage發(fā)出的,“R”是消息句柄的返回值。第四欄給出解碼后的消息名,消息參數(shù)或返回值。
3、非常規(guī)方法
(1)重新編連你的應(yīng)用程序
當(dāng)你的程序表現(xiàn)出異常的或意外的行為,或者Visual C++編譯器因?yàn)橐粋€(gè)內(nèi)部編譯器錯(cuò)誤而失敗時(shí),最好刪除工程中的Debug或Release文件夾,從頭開始重新進(jìn)行編連。
(2)重新啟動(dòng)Visual C++
Visual C++有超強(qiáng)的能力,但編譯器的某些特性也會(huì)引起奇怪的錯(cuò)誤。如果你的程序表現(xiàn)得很奇怪,你可是試著清除所有的斷點(diǎn),關(guān)閉或隱藏觀察窗口,檢查工程設(shè)置對(duì)話框看最近做了什么修改,直至重新啟動(dòng)Visual C++以便消除由于Visual C++環(huán)境引起的異常行為。
(3)重新啟動(dòng)Windows
當(dāng)你發(fā)現(xiàn)Windows或者其他程序表現(xiàn)出異常的或出人意料的行為時(shí),就應(yīng)該重新啟動(dòng)Windows,以消除操作系統(tǒng)給調(diào)試帶來的干擾。
(完)
[參考文獻(xiàn)]
Everett N.McKay,Mike Woodring著,何健輝,許俊鵑,董偉譯.Debugging Windows Programs(Windows程序調(diào)試).北京:中國(guó)電力出版社.2002
后記:
《Windows 程序調(diào)試》這本書中介紹了許多調(diào)試的基本知識(shí)和技巧。這當(dāng)中的有些問題,在我看這本書前曾使我百思不得其解,如運(yùn)行應(yīng)用程序時(shí),忽然跳出一個(gè)對(duì)話框,說某段內(nèi)存為"Write",看了這本書之后你就會(huì)明白產(chǎn)生的機(jī)理所在。本文對(duì)書中相同主題的內(nèi)容做了一定的抽象概括,對(duì)書中前后相關(guān)的內(nèi)容做了整理,并適當(dāng)補(bǔ)充了一些書中沒有的內(nèi)容。由于自己所關(guān)注的方面并未涉及到多線程和COM,因此對(duì)這方面的調(diào)試技術(shù)可參考源書。