C++代碼和調試

    公司開發項目調試的時候遇到一個頭疼的問題,一個函數給很多地方調用了,但我只要某個參數值時執行該斷點,這種高級的調試從來沒接觸過,上網搜了一下,果然有這方面的調試技術,下面是我找的資料,挺有用的,分享一下。

    原文鏈接是:http://hhfighting.blog.163.com/blog/static/55700323200922093543827/

 

C++代碼和調試

本部分教程主要介紹了良好的C++代碼風格、如何書寫安全的代碼以及在Visual C++環境下的程序調試技術,這些內容對於新員工從學生成長爲真正的程序員,逐步參與實際項目的開發工作,以及閱讀第三方代碼非常重要。

1 規範易懂的代碼

現階段軟件開發,都要依靠團隊的合作。程序員不再是個人英雄主義的代名詞,程序員一方面要依賴大量其他程序員完成的代碼,一方面又提供大量代碼給其 他人使用,代碼實際上具備了兩個要素:首先是可靠的提供某種功能,其次是清楚地表達作者的思想。任何交流都必須有一定的規範才能進行,體現在代碼中就是規 範易懂。另外,規範易懂的代碼纔是可重複使用的,規範的代碼具有更長的壽命,具有更好的可維護性,也更方便後期的擴展。

1.1 好代碼的幾個特徵

怎麼樣的代碼纔算規範易懂,體現在細節上會有無數的爭論,實際上無論風格和習慣如何,好的代碼具有幾個共同的特徵:
1. 良好的命名:好的變量名和函數名,讓閱讀代碼的人馬上就知道該變量或者函數的作用,很容易就能理解程序的大概結構和功能。程序員有必要理解匈牙利命名法。
2. 一致性:一致性帶來更好的程序,一致的代碼縮進風格能夠顯示出代碼的結構,採用何種縮進風格並不重要,實際上,特定的代碼風格遠沒有一致的使用它們重要。
3. 註釋:註釋是幫助程序讀者的一種手段,程序作者也是未來的程序讀者之一。最好的註釋是簡潔地點明程序的突出特徵,或是提供一種概觀,幫助別人理解程序;但如果註釋只是說明代碼已經講明的事情,或者與代碼矛盾,或者以精心編排的形式迷惑干擾讀者,那就是幫了倒忙。

1.2 養成好習慣

前面已經提過,特定的代碼風格遠沒有一致的使用他們重要,所以,把過多的精力放到A or B的選擇上是浪費時間,你要做的是堅持。如何書寫規範易懂的代碼,如何養成良好的習慣,下面是一些提示。

1. 按照匈牙利命名法給變量和函數命名。
2. 遵循國際流行的代碼風格。
3. 寫代碼的同時就遵循你的命名規範和書寫風格,千萬不能事後補救。
4. 利用工具(Parasoft C++ Test)檢查你的代碼,評估一下自己形成良好的習慣沒有。
5. 堅持不懈直到養成習慣。

2 編寫安全可靠的代碼

在大型應用軟件系統中,各個代碼片段共同構成完整的系統,代碼間的交互非常頻繁,程序崩潰往往並不在錯誤發生的時候就發生,而是延遲了一段時間,經 過數個函數之間的中轉後才發生,此時定位和查找錯誤非常費時費力,如何才能及時反映程序中的錯誤,如何在代碼中避免一些幼稚的語義錯誤呢?一個函數往往會 被其他程序員拿來使用,但是他怎麼能夠正確的使用其他人編寫的函數呢?這部分內容能夠(部分)幫助解決這些問題。

2.1 契約編程
契約編程(Design by Contract)的思想在C++聖經級的著作,C++之父Bjarne Stroustrup的《C++程序設計語言》中略微提到過,OO領域的聖經級著作《面向對象軟件構造》以大篇幅闡釋了契約編程,現在越來越多的軟件開發 人員認識到契約編程的重要性,並逐步地在實際工作中採用契約編程。
對契約編程簡單的解釋是:對實現的代碼塊(函數、類)通過規定調用條件(約束)和輸出結果,在功能的實現者和調用者之間定義契約。
具體到我們的工作,開發人員應該對完成的每個函數和類,定義契約。契約編程看似平淡無奇,對程序開發沒有什麼具體的幫助,實際上,契約編程在開發階段就能夠最大程度的保證軟件的可靠性和安全性。
在 實際工作中,每當你需要使用其他程序員提供的模塊,你並不知道如何調用,也不知道你傳入的參數是否合法,有時候對於功能模塊的處理結果也不敢相信。這些本 來應該很明顯的信息因爲模塊提供者沒有顯式的提供,造成了調用者只能忐忑不安的摸着石頭過河,浪費了大量時間,而且爲了讓自己的代碼更安全可靠,在代碼中 做了大量的判斷和假設,造成代碼結構的破壞和執行效率的損失,最後,調用者依舊不能確保自己的調用是正確的。
而契約編程通過嚴格規定函數(或類)的行爲,在功能提供者和調用者之間明確了相互的權利和義務,避免了上述情況的發生,保證了代碼質量和軟件質量。

2.2 主動調試
主動調試指在寫代碼的時候,通過加入適量的調試代碼,幫助我們在軟件錯誤發生的時候迅速彈出消息框,告知開發人員錯誤發生地點,並中止程序。這些調試代碼只在Debug版中有效,當經過充分測試,發佈Release版程序的時候,這些調試代碼自動失效。
主動調試和契約編程相輔相成,共同保證軟件開發的質量。契約編程相當於經濟生活中籤訂的各種合同,而主動調試相當於某方不遵守合同時採取的法律懲罰措施。
各種開發語言和開發工具都提供這些調試語句,標準C++提供了assert函數,MFC提供了ASSERT調試宏幫助我們進行主動調試,在實際工作中,建議統一使用MFC的ASSERT調試宏。

2.2.1 參數檢查
對於編寫的函數,除了明確的指定契約外,在函數開始處應該對傳入的參數進行檢查,確保非法參數傳入時立即報告錯誤信息。例如:
BOOL GetPathItem ( int i , LPTSTR szItem , int iLen )
{
ASSERT ( i > 0 ) ;
ASSERT ( NULL != szItem ) ;
ASSERT ( ( iLen > 0 ) && ( iLen < MAX_PATH ) ) ;
ASSERT ( FALSE == IsBadWriteStringPtr ( szItem , iLen ) ) ;
}
對指針的檢查尤其要注意,通常程序員會這樣進行檢查:
// An example of checking only a part of the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{
ASSERT ( NULL != pfnCallback ) ;

}
這樣的檢查只能夠排除指針爲空的情況,但是如果指針指向的是非法地址,或者指針指向的對象並不是我們需要的類型,上面的例子就沒有辦法檢查出來,而是統統認爲是正確的。完整的檢查應該如下:
// An example of completely checking the error condition
BOOL EnumerateListItems ( PFNELCALLBACK pfnCallback )
{
ASSERT ( FALSE == IsBadCodePtr ( pfnCallback ) ) ;
}

2.2.2 內部檢查
恰當地在代碼中使用ASSERT,對bug檢測和提高調試效率有極大的幫助,下面舉個簡單的例子加以說明。
switch( nType )
{
case GK_ENTITY_POINT:
// do something
break;
case GK_ENTITY_PLINE:
// do something
break;
default:
ASSERT( 0 );
}

在上面的例子中,switch語句僅僅處理了GK_ENTITY_POINT和GK_ENTITY_PLINE兩種情況,應該是系統中當時只需要處 理這兩種情況,但是如果後期系統需要處理更多的情況,而此時上面這部分代碼又沒有及時更新,或者是因爲開發人員一時疏忽遺漏了。一個可能導致系統錯誤或者 崩潰的bug就出現了,而使用ASSERT可以及時地提醒開發人員他的疏忽,儘可能快的消滅這個bug。

還有一些情況,在開發人員編寫代碼時,如果能夠確信在某一點出現情況A就是錯誤的,那麼就可以在該處加上ASSERT,排除情況A。

綜上所述,恰當、靈活的使用ASSERT進行主動調試,能夠極大提高程序的穩定性和安全性,減少調試時間,提高工作效率。

2.3 有用的代碼風格
一些好的代碼風格也能夠幫助你避免一些幼稚的、低級的錯誤,而這種錯誤又是很難檢測到的。由於C++語言簡潔靈活的 特性,有時候敲錯一個字符,或者漏敲一個字符,都有可能造成極大的災難,而這種錯誤並不是隨着你的編程水平和經驗的提高就能逐步避免的,誰都會敲錯字符, 對吧。
比如程序員經常將等於邏輯判斷符==誤敲成賦值運算符=,對於我來說就不太可能程序運行出錯後才發現,因爲我的習慣是,對於邏輯判斷,將常量置於==的左邊,如果我誤輸入了=,那麼編譯的時候編譯器就會報錯。
if( INT_MAX == i )

3 Visual C++調試技術
檢查代碼直到頭暈眼花也沒有發現錯誤,一運行程序就死機,只好祭出最後的法寶:調試器。Visual C++調試器可以稱得上Windows平臺下最好的C/C++調試器了,而且Visual C++調試器還可以調試用其他語言如Delphi、Java編寫的程序,可謂功能強大。
儘管Visual C++調試器具有如此大的威力,它也只能幫助你發現一些隱藏的邏輯錯誤,對於程序設計和結構的缺陷無能爲力。
程序員最常用到的Visual C++調試技術有設置斷點、跟蹤調用堆棧和反彙編調試,其他編譯器功能均爲調試中的輔助工具,因爲反彙編調試需要程序員具備彙編語言知識和語言底層結構,這裏不再介紹。

3.1 調試的先決條件
專業調試者有一個共同的特點,即他們同時也是優秀的開發者。顯然,如果你不是一個優秀的開發者,那麼你也不可能成爲調試專家,反之亦然。以下是要成爲一名高水平的,至少是合格的調試者或者開發者所需要精通的領域。

1. 瞭解項目:對項目的瞭解是防範用戶界面、邏輯及性能方面的錯誤的第一要素。瞭解各種功能如何在各種源文件裏實現,以及在哪兒實現,你就能夠縮小查找範圍,很快找出問題所在。
2. 掌握語言:掌握項目所使用的語言,調試者(開發者)既要知道如何使用這些語言進行編程,還要知道這些語言在後臺作些什麼。
3. 掌握技術:要解決棘手的問題,第一個重要步驟就是抓住所用技術的要領,這並不意味着你必須對所用技術的一切細節都一清二楚,而是說你應該對所使用的技術有一個大概的瞭解,而且更重要的是,當需要更詳細的信息時,你應該確切的知道在哪兒查找。
4. 操作系統和CPU:任何項目都實際運行在特定的操作系統和特定的CPU,對操作系統瞭解越多,對查找錯誤幫助越大;從理論上來說,掌握彙編語言,你就可以調試解決任何bug。

無論從事什麼工作,只要是經常從事技術工作的人,都必須不斷地學習以跟上技術的發展,更不用說想幹得更好或是想走在技術發展的前沿。經常閱讀優秀的 技術書籍和雜誌,多動手編寫一些實用程序,閱讀其他優秀開發者的代碼,作一些反彙編工作,都會有效幫助你提高開發和調試水平(尤其當你將這四者有機結合起 來)。

3.2 調試過程

確定一個適用於解決所有錯誤的調試過程有一定的難度,但John Robbins提出的調試過程應該說是最實用的:
1. 複製錯誤
2. 描述錯誤
3. 始終假定錯誤是自己的問題
4. 分解並解決錯誤
5. 進行有創見的思考
6. 使用調試輔助工具
7. 開始調試工作
8. 校驗錯誤已被更正
9. 學習和交流

對錯誤進行描述有助於改正錯誤,同時也能夠得到同事們的幫助。逐步縮小問題範圍、排除不存在錯誤的代碼段,直到找到問題所在,是解決所有問題的普遍 適用方法。有些奇怪的錯誤需要你把視線從代碼堆轉移到諸如操作系統、硬件環境等其他方面去。善用各種調試輔助工具能夠節省你大量的時間,而且某些工具本身 就不會給你犯有些錯誤的機會。當你解決了一個bug,停下來思考一下,什麼導致你(或他)犯了這樣的錯誤,以後如何避免?

要記住調試器僅僅是個工具,就好比一隻螺絲起子,你讓它做什麼它就只做什麼,真正的調試器是你自己腦子中的調試思想。

3.3 斷點及其用法

在Microsoft Visual C++調試器中在源代碼行中設置一個斷點很簡單。只需要打開源文件,將光標放在想要設置斷點的代碼行上,按下F9快捷鍵就可以了,再次按下F9快捷鍵就會 取消斷點。當運行該代碼行的代碼時,調試器將在所設置的位置處停止。這種簡單的位置斷點的功能極其強大,經過統計,只需要單獨的使用這種斷點,就可以解決 99.46%的調試問題。

如果程序並不是每次運行到斷點處都會發生錯誤,那麼不停地在調試器和應用程序之間穿梭很快就會讓人厭倦,這時高級斷點就派上了用場。從本質上來講, 高級斷點允許你將某些智慧寫入到斷點中,讓調試器在執行到斷點處時,只當程序內部狀態符合你指定的條件時纔在斷點處中斷程序運行,並切換到調試器中。

按下Alt+F9快捷鍵彈出Breakpoints對話框,瀏覽一下對話框發現該對話框分爲Location、Data和Messages三頁,分別對應三種斷點:
1. 位置斷點:我們通常使用的簡單斷點均爲位置斷點,我們還可以設置斷點在某個二進制地址或任何函數上,並通過指定各種限定條件來增強位置斷點的功能。
2. 表達式和變量斷點:調試器會讓程序一直運行,直到滿足所設的條件或者指定數據更改爲止。在Intel CPU上,這兩種斷點都儘可能通過CPU的特定調試寄存器使用一個硬件斷點,如果能夠使用調試寄存器,那麼程序將能夠全速運行,否則調試器將單步執行每個 彙編指令,並每步都檢查條件,程序的運行速度將極其緩慢甚至無法運行。
3. Windows消息斷點:使用消息斷點,可以讓調試器在窗口過程接收到一個特定的Windows消息時中斷。消息斷點適用於C SDK類型的程序,對於使用MFC等C++類庫的程序(應該是絕大多數)來說,消息斷點並不實用,可以變通地使用位置斷點來達到同樣效果。
各種高級斷點的設置在MSDN中有詳細的介紹,請在Visual C++子集下搜索主題Using Breakpoints: Additional Information並閱讀相關內容。

3.4 調用堆棧

有時候我們並不清楚應該在哪裏設置斷點,只知道程序正在運行就突然崩潰了,這時候如何定位到出錯地點呢?這時的選擇就是查看調用堆棧,調用堆棧可以幫助我們確定某一特定時刻,程序中各個函數之間的相互調用關係。
方 法是當程序執行到某斷點處或者程序崩潰,控制權轉到調試器後,按下Alt+7快捷鍵,彈出Call Stack窗口,你可以看到當前函數調用情況,當前函數在最上面,下面的函數依次調用其上面的函數。在Call Stack窗口的彈出菜單上選擇Parameter Values和Parameter Types可以顯示各個函數的參數類型和傳入值。

3.5 使用跟蹤工具

有些時候,我們希望瞭解程序中不同函數之間的協作關係,或者由於文檔的缺失,希望能夠確認函數在不同情況下被調用時的傳入參數值。這時使用斷點功能就過分麻煩,而調用堆棧只能查看當前函數的被調用情況,一種較好的方法就是使用TRACE宏以及相對應的工具。
程 序(Debug版)運行中,一旦運行到TRACE宏,就會向當前Windows系統的調試器輸出TRACE宏內指定的字符串並顯示出來,當在Visual C++環境中調試運行(按F5鍵)程序時,可以在Output窗口的Debug頁看到TRACE宏的輸出內容。實際上,TRACE宏是封裝了 Windows API函數OutputDebugString的功能,有些輔助工具可以在不驚動Visual C++調試器的前提下,攔截程序中TRACE宏的輸出內容,比如《深入淺出MFC》的附錄中提到的Microsoft System Journal(MSJ)1996年1月的C/C++專欄介紹的TraceWin工具(在較老版本的MSDN中可以找到源代碼和文檔)以及功能強大的免費 工具DebugView。

使用TRACE宏,我們可以輕鬆瞭解程序中各個函數之間的相互協作關係和被調用的先後順序和時間,進一步說,你能夠完全掌握程序的執行流程。

最後請注意,TRACE宏會對程序效率有所影響,所以,當前不用的TRACE宏最好刪除或者註釋掉。

4 閱讀程序的技巧

對於程序員來說,無論是學習還是工作,經常要閱讀其他程序員的源代碼,如何快速領悟程序的思想,洞悉程序的結構和各個組成部分的功能,進而全面掌握程序所涉及的方方面面,是程序員很重要的一項基本技能。下面介紹一些常用的技巧。

4.1 從功能、界面入手

一個完整的應用程序或者系統是由若干相對獨立的功能構成,這些功能反應在與用戶交互的圖形界面上,就是各種菜單命令、工具欄按鈕命令等等。所以如果 當前只對程序的某幾個功能感興趣,可以在程序中找到這些菜單命令、按鈕命令等的ID響應函數,以此爲起點,逐步深入到程序內部,直到完全理解該功能的實現 爲止。此過程所花費的時間,很大程度上取決於程序員對調試技術的掌握程度。

需要強調的是,在不熟悉程序核心結構和實現技術的情況下,直接採用該方法探究程序,當逐步深入到程序核心時,涉及的程序模塊數量會急劇增長,理解難度也會驟然增大;一旦你對程序核心結構和實現技術瞭然於胸,採用該方法探究程序,會有勢如破竹之感覺。

4.2 砍去枝葉,只留主幹

前面已經提到,無論如何,最終你都要掌握程序核心結構和實現技術。如何掌握呢?方法是首先將拿到的程序進行完整的備份,然後將次要功能都從程序中去 掉,只留下的必須的部分。去除次要功能是一個反覆多次的過程,花費的時間取決於程序員對行業知識的理解程度、編程技術的高低和經驗的多少。

經常遇到無法在短時間內判斷某個模塊是否次要的情況(隨着對程序的理解逐漸加深,以及經驗和技術的積累,這種情況會越來越少),這時候建議直接將該模塊去除,然後重新編譯連接程序,運行程序,看程序運行是否正常。

以上介紹的兩種方法是使用比較頻繁的,兩種方法可以相互結合,交替使用。但無論採用什麼方法探究閱讀程序,都不要指望能夠不費任何氣力,花費一兩個鐘頭就能夠將上萬行的程序探究個明白。

5 參考資料
《程序設計實踐》,機械工業出版社
《高質量C++編程指南》,林銳
《應用程序調試技術》,John Robbins,清華大學出版社
《面向對象軟件構造》,Bertrand Meyer,清華大學出版社

發佈了30 篇原創文章 · 獲贊 66 · 訪問量 65萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章