- C/C++代碼調(diào)試的藝術(shù)(第2版)
- 張海洋
- 3703字
- 2023-06-21 18:59:25
2.3 調(diào)試執(zhí)行
上一節(jié)介紹了調(diào)試過程中最重要的技術(shù)——斷點(diǎn),合理設(shè)置斷點(diǎn)有助于調(diào)試并發(fā)現(xiàn)BUG。本節(jié)主要介紹調(diào)試過程中另一個(gè)重要的概念——調(diào)試執(zhí)行,即程序在斷點(diǎn)處中斷后,我們希望通過什么方式來執(zhí)行程序中的代碼,以達(dá)到預(yù)期效果,并能夠快速地定位并解決BUG。
2.3.1 啟動調(diào)試
啟動調(diào)試有多種方法:可以從“調(diào)試”菜單中執(zhí)行“開始調(diào)試”命令;也可以從“調(diào)試”菜單中執(zhí)行“附加到進(jìn)程”命令。兩者的功能略有不同,后面會有一個(gè)專門的小節(jié)來介紹“附加到進(jìn)程”這一命令。
還可以按F5鍵來啟動調(diào)試。這種啟動調(diào)試的方法一般是在程序還沒有開始運(yùn)行時(shí)使用,可以直接從調(diào)試器中啟動程序。這種方式的好處是可以調(diào)試程序任意位置的代碼,比如main函數(shù)的第一行。使用這種方式來運(yùn)行程序是一個(gè)好習(xí)慣,尤其是在開發(fā)階段,因?yàn)槲覀儾恢莱绦蛑惺欠癜珺UG,也不知道程序在執(zhí)行過程中是否會崩潰、是否有內(nèi)存泄漏等。但是,如果我們在調(diào)試器中運(yùn)行程序,那么就很容易探測到上述情況,從而提前發(fā)現(xiàn)并解決BUG。如果在程序運(yùn)行過程中出現(xiàn)了一些錯(cuò)誤的操作,比如內(nèi)存被多次釋放、緩存區(qū)溢出等,就可以立即捕獲這些錯(cuò)誤。
建議平時(shí)以啟動調(diào)試的方式來運(yùn)行程序,而不要選擇“開始執(zhí)行(不調(diào)試)”,即使我們并沒有特定的調(diào)試目的,這樣也可以及早發(fā)現(xiàn)并解決程序中的問題。
啟動調(diào)試后,程序會執(zhí)行到第一個(gè)斷點(diǎn)處暫停,這里的第一個(gè)斷點(diǎn)指的不是位置上的第一個(gè)(即代碼行最靠前的那一個(gè)),而是邏輯上的第一個(gè)(即所有斷點(diǎn)中最先執(zhí)行的那個(gè))。
當(dāng)程序暫停后,“啟動調(diào)試”菜單就會變成“繼續(xù)”,如果此時(shí)繼續(xù)按F5鍵或者單擊調(diào)試菜單中的“繼續(xù)”,程序就會繼續(xù)執(zhí)行,直到遇到下一個(gè)斷點(diǎn)。
與啟動調(diào)試對應(yīng)的功能是停止調(diào)試,可以從“調(diào)試”菜單中執(zhí)行“停止調(diào)試”命令,或者按組合鍵Ctrl+Shift+F5來停止調(diào)試,此時(shí)無論程序是暫停狀態(tài)還是運(yùn)行狀態(tài),都會停止運(yùn)行。另外一種方法是直接退出程序,但是必須是在程序沒有進(jìn)入暫停狀態(tài)時(shí)退出,否則程序便無法退出。
2.3.2 逐語句執(zhí)行
逐語句(Step Into)執(zhí)行(F11鍵)也稱為單步執(zhí)行或者逐行執(zhí)行,即一行一行地執(zhí)行程序中的代碼。如果某行代碼中調(diào)用了一個(gè)函數(shù),那么逐語句執(zhí)行命令就會進(jìn)入函數(shù)中去,而不是跳過函數(shù)。我們來看一個(gè)簡單的例子,如代碼清單2-1所示。
代碼清單2-1 逐語句執(zhí)行示例

在代碼清單2-1中,order_bus函數(shù)的第一行(也就是第241行)代碼設(shè)置了一個(gè)斷點(diǎn),當(dāng)代碼執(zhí)行到這里時(shí)會暫停。我們可以按F11鍵來逐語句執(zhí)行,每按一次就執(zhí)行一行代碼,當(dāng)執(zhí)行到第245行代碼時(shí),由于這條語句中調(diào)用了total_bus函數(shù),因此這時(shí)按F11鍵,就會進(jìn)入total_bus函數(shù)中繼續(xù)執(zhí)行,即調(diào)試已經(jīng)從order_bus函數(shù)進(jìn)入total_bus函數(shù)中。如果在total_bus函數(shù)中繼續(xù)逐語句執(zhí)行,那么執(zhí)行方式一致。如果遇到了函數(shù)調(diào)用,還會繼續(xù)進(jìn)入函數(shù)內(nèi)部執(zhí)行。
如果調(diào)用的函數(shù)是系統(tǒng)函數(shù)或者C/C++庫函數(shù)會怎樣呢?逐句執(zhí)行會得到什么結(jié)果?如果該函數(shù)有源代碼,也會直接進(jìn)入該函數(shù)中;如果該函數(shù)沒有對應(yīng)的源代碼,則會像普通語句一樣逐語句執(zhí)行,不會進(jìn)入函數(shù)中。
如果一行語句中有多個(gè)函數(shù)調(diào)用,逐語句執(zhí)行會依次進(jìn)入多個(gè)函數(shù)中執(zhí)行,但是會按照什么順序執(zhí)行呢?我們來看一個(gè)逐語句執(zhí)行多個(gè)函數(shù)調(diào)用的例子,如代碼清單2-2所示。
代碼清單2-2 逐語句執(zhí)行多個(gè)函數(shù)調(diào)用

在代碼清單2-2中,第163行代碼設(shè)置了一個(gè)斷點(diǎn)。該斷點(diǎn)所在處的代碼是一個(gè)if條件語句,if表達(dá)式里面調(diào)用了兩個(gè)函數(shù):is_ordered和get_seat_num。如果此時(shí)進(jìn)行逐語句執(zhí)行,會首先進(jìn)入is_ordered函數(shù)中,從函數(shù)返回后,再次執(zhí)行逐語句執(zhí)行,則會進(jìn)入到get_seat_num函數(shù)中。但是,我們都知道C/C++編譯器會對代碼進(jìn)行一些優(yōu)化,有時(shí)并不會完全按照預(yù)期執(zhí)行,特別是條件語句。仍然以第163行代碼為例,由于兩個(gè)函數(shù)中間的操作符是&&,表示兩個(gè)條件都為真時(shí),整個(gè)if條件才為真。如果第一個(gè)函數(shù)返回false,第二個(gè)函數(shù)也不會執(zhí)行。因?yàn)槔^續(xù)執(zhí)行已經(jīng)沒有任何意義,無論get_seat_num函數(shù)獲取到的值是否大于100,都不會改變整個(gè)if條件的取值,所以如果逐語句執(zhí)行從is_ordered函數(shù)返回,并且其返回值為false的話,那么再次執(zhí)行逐語句執(zhí)行也不會進(jìn)入get_seat_num函數(shù)中,而是會直接執(zhí)行下一行代碼。
類似的一種情況是,如果兩個(gè)條件之間的操作符是||,如
if (is_ordered(bus) || get_seat_num(bus) < 100)
如果is_ordered函數(shù)的返回值為true,第二個(gè)函數(shù)get_seat_num就不會被執(zhí)行,因?yàn)闊o論是否執(zhí)行第二個(gè)函數(shù)get_seat_num,都不會影響整個(gè)if條件的判斷。
注意
這里的逐語句執(zhí)行好像是逐行執(zhí)行,大多數(shù)情況下的確如此。但是C/C++代碼規(guī)范比較靈活,有別于其他書寫要求嚴(yán)格的編程語言,C/C++可以在一行中書寫很多代碼。所以逐語句(逐行)執(zhí)行指的并不是物理意義上的一行代碼,而是邏輯上的代碼行,如果一行代碼中書寫了很多命令,逐語句執(zhí)行的時(shí)候會逐個(gè)執(zhí)行,就像逐個(gè)執(zhí)行函數(shù)調(diào)用一樣。
2.3.3 逐過程執(zhí)行
逐過程(Step Over)執(zhí)行與逐語句執(zhí)行有一些相似之處。如果代碼行中沒有函數(shù)調(diào)用,那么執(zhí)行結(jié)果是相同的,都是執(zhí)行完當(dāng)前代碼行,在下一行代碼處暫停。不同之處在于當(dāng)前代碼行中是否包含函數(shù)。如果當(dāng)前代碼行中有函數(shù)調(diào)用,逐語句執(zhí)行會進(jìn)入函數(shù)中然后暫停,而且如果有多個(gè)函數(shù)的話,逐語句執(zhí)行會依次進(jìn)入到每個(gè)函數(shù)中。逐過程執(zhí)行則剛好相反,無論當(dāng)前代碼行有多少個(gè)函數(shù)調(diào)用,都不會進(jìn)入到函數(shù)中,而是直接進(jìn)入到下一行代碼并暫停。
所以大多數(shù)情況下逐過程執(zhí)行可以節(jié)省調(diào)試時(shí)間,對于不重要的函數(shù)調(diào)用或者函數(shù)代碼,就可以使用逐過程執(zhí)行或者按F10鍵,直接跳過該函數(shù),執(zhí)行下一行代碼。
2.3.4 跳出執(zhí)行
跳出執(zhí)行(Step Out)是指跳出當(dāng)前執(zhí)行的函數(shù)。跳出執(zhí)行的組合鍵是Shift+F11,該功能只有在程序暫停的狀態(tài)下才可以使用,即正在逐語句或者逐過程執(zhí)行代碼時(shí),跳出執(zhí)行才有效。
跳出執(zhí)行非常有用,比如我們正在一個(gè)函數(shù)中進(jìn)行逐語句或者逐過程調(diào)試時(shí),而且已經(jīng)對關(guān)鍵代碼進(jìn)行了檢查,相關(guān)的信息也進(jìn)行了查看,如果并不關(guān)心函數(shù)后面部分的代碼,這個(gè)時(shí)候就沒有必要再逐步進(jìn)行調(diào)試,就可以跳出執(zhí)行。執(zhí)行跳出命令或者按Shift+F11組合鍵,就會跳出當(dāng)前函數(shù)的調(diào)試,進(jìn)入調(diào)用該函數(shù)的代碼的下一行代碼處并暫停。
2.3.5 運(yùn)行到光標(biāo)處
運(yùn)行到光標(biāo)處(Run To Cursor)是一個(gè)非常有趣的功能。“調(diào)試”菜單中不包含該命令,只能在上下文菜單中找到或者使用Ctrl+F10組合鍵來調(diào)用。如圖2-10所示,在第389行代碼處單擊鼠標(biāo)右鍵后,就會在彈出的菜單中看到“運(yùn)行到光標(biāo)處”命令。
“運(yùn)行到光標(biāo)處”相當(dāng)于先在光標(biāo)處設(shè)置一個(gè)斷點(diǎn),然后繼續(xù)執(zhí)行“啟動調(diào)試/繼續(xù)”命令。不過這只是一個(gè)虛擬的斷點(diǎn),不會出現(xiàn)在“斷點(diǎn)”窗口中,而且只會作用一次,即執(zhí)行過后就不再起作用。
以圖2-10為例,假設(shè)我們在第389行代碼處執(zhí)行“運(yùn)行到光標(biāo)處”命令或者按Ctrl+F10組合鍵,如果此時(shí)程序沒有啟動,那么程序會啟動并進(jìn)入調(diào)試狀態(tài)。如果第389行代碼之前還有其他斷點(diǎn),那么會先在其他斷點(diǎn)處暫停,繼續(xù)調(diào)試執(zhí)行才會執(zhí)行到第389行代碼處并暫停,因此,“運(yùn)行到光標(biāo)處”設(shè)置的斷點(diǎn)本身并不具備比其他斷點(diǎn)更高的優(yōu)先級,其作用只是一個(gè)普通的一次性斷點(diǎn)。

圖2-10 “運(yùn)行到光標(biāo)處”右鍵菜單
如果在執(zhí)行“運(yùn)行到光標(biāo)處”命令時(shí),調(diào)試已經(jīng)開始,那么相當(dāng)于先執(zhí)行一個(gè)“繼續(xù)”命令,然后執(zhí)行至光標(biāo)處暫停。
2.3.6 多次執(zhí)行代碼
多次執(zhí)行代碼指的是在調(diào)試狀態(tài)下,多次執(zhí)行某些代碼。這個(gè)功能非常有用。如果對前面某個(gè)函數(shù)的調(diào)用沒有理解清楚,或者對其返回的值有疑問,這時(shí)就可以對該函數(shù)重復(fù)執(zhí)行一次,而不用等待下一次命中斷點(diǎn)時(shí)再執(zhí)行。因?yàn)槟苓M(jìn)入到一個(gè)斷點(diǎn)是非常不容易的,特別是一些大型的軟件,操作會非常耗時(shí),BUG也不能穩(wěn)定重現(xiàn),因此最好在期望的斷點(diǎn)處暫停下來,絕不能錯(cuò)過反復(fù)調(diào)試的機(jī)會。
假設(shè)我們正在進(jìn)行代碼調(diào)試,準(zhǔn)備查看一輛班車的信息,如圖2-11所示,我們在第216行代碼處設(shè)置了一個(gè)斷點(diǎn),以查看查詢到的班車信息。此時(shí)代碼即將執(zhí)行218行,但是發(fā)現(xiàn)班車信息并不是期望值,于是希望再次執(zhí)行g(shù)et_bus函數(shù),查看問題出現(xiàn)的原因。

圖2-11 代碼多次執(zhí)行示例
VC確實(shí)提供了這樣的功能。從圖2-11中可以看到,第218行代碼的行首有一個(gè)箭頭表示當(dāng)前要執(zhí)行的代碼位置,它相當(dāng)于代碼執(zhí)行的指針,這個(gè)指針指向哪里,代碼就執(zhí)行到哪里。因此,要想執(zhí)行某行代碼,可以將該指針移動到期望執(zhí)行的地方。移動指針的操作沒有菜單命令,也沒有快捷鍵,只能通過拖動鼠標(biāo)來執(zhí)行,這也是最簡單的方式——將箭頭拖動到哪里,就從哪里開始執(zhí)行。
將鼠標(biāo)指針放到小箭頭上面,就會出現(xiàn)如圖2-12所示的代碼執(zhí)行的指針提示。這時(shí)只要按鼠標(biāo)左鍵,拖動箭頭到想要執(zhí)行的位置,比如第216行代碼,即可釋放鼠標(biāo)指針,此時(shí)執(zhí)行指針就會指向新的位置,并準(zhǔn)備好執(zhí)行新位置的代碼。

圖2-12 代碼執(zhí)行的指針提示
從代碼執(zhí)行的指針提示可以發(fā)現(xiàn),如果新的執(zhí)行位置不合理,可能就會導(dǎo)致預(yù)料之外的結(jié)果,甚至?xí)?dǎo)致程序崩潰。因?yàn)樵诔绦驁?zhí)行時(shí),有很多信息需要保存,而且很多信息是互相依賴的,所以一定要保證拖動的位置能夠正常執(zhí)行,否則調(diào)試可能會終止。
利用代碼執(zhí)行指針的功能,除了可以反復(fù)執(zhí)行某些代碼,還可以跳過某些代碼的執(zhí)行。如果不想執(zhí)行某行或者某幾行代碼,就可以通過移動執(zhí)行指針來跳過這幾行代碼。同樣地,如果這幾行代碼很重要,比如是一些賦值或者初始化的操作,就會影響后面代碼的執(zhí)行結(jié)果,需要特別注意。
注意
移動執(zhí)行指針時(shí)需要遵循兩個(gè)基本原則:一是不要移動到函數(shù)外;二是不要跳過重要的初始化操作語句。總之,最基本的原則是要保證程序能夠正常運(yùn)行。至于怎樣做才能保證程序的正常運(yùn)行,不同的程序需要進(jìn)行具體分析,在實(shí)踐中總結(jié)經(jīng)驗(yàn)。
- 現(xiàn)代C++編程:從入門到實(shí)踐
- Delphi程序設(shè)計(jì)基礎(chǔ):教程、實(shí)驗(yàn)、習(xí)題
- MySQL 8從入門到精通(視頻教學(xué)版)
- Python Deep Learning
- Java Web程序設(shè)計(jì)
- Java虛擬機(jī)字節(jié)碼:從入門到實(shí)戰(zhàn)
- The Computer Vision Workshop
- Windows Presentation Foundation Development Cookbook
- AutoCAD VBA參數(shù)化繪圖程序開發(fā)與實(shí)戰(zhàn)編碼
- PyQt編程快速上手
- Learning Jakarta Struts 1.2: a concise and practical tutorial
- Mastering Python
- C語言程序設(shè)計(jì)
- Python程序設(shè)計(jì)教程
- 前端Serverless:面向全棧的無服務(wù)器架構(gòu)實(shí)戰(zhàn)