南開(kāi)大學(xué)21級(jí)C++作業(yè)解析||特別篇

C++程序設(shè)計(jì)基礎(chǔ)課程已經(jīng)進(jìn)入最后的章節(jié),而隨著問(wèn)題越來(lái)越具體,大家的問(wèn)題也越來(lái)越雜,這次就不再針對(duì)具體的題目進(jìn)行專(zhuān)門(mén)解析了。針對(duì)這段時(shí)間大家出現(xiàn)問(wèn)題的共性,這次想和大家討論兩個(gè)主題:1 不同版本的C++環(huán)境在何處不同;2 怎么優(yōu)雅高效地解決作業(yè)問(wèn)題。

1 關(guān)于不同版本C++環(huán)境的一些討論
本課程使用的是趙宏老師在2019年編寫(xiě)的教材《程序設(shè)計(jì)基礎(chǔ)》,但教材中使用的C++環(huán)境仍然是 Visual Studio 2010. 按照老師的要求,我們的作業(yè)批改是以程序在2010版本的運(yùn)行結(jié)果為準(zhǔn)的。
但是,現(xiàn)在許多同學(xué)都用上了VS2017, 2019,也有和我一樣使用VisualStudioCode+MinGW配置的環(huán)境。這些不同版本的環(huán)境在運(yùn)行C++程序時(shí)是存在微妙的不同的。這里總結(jié)如下:

1.1 庫(kù)函數(shù)的調(diào)用
C++的cstring庫(kù),cmath庫(kù)是兩個(gè)很典型的庫(kù),有很多好用的函數(shù),也常常在作業(yè)中被使用。
例如,第3章中牛頓迭代法一題中,需要判斷的值,可以使用cmath庫(kù)的 fabs() 函數(shù);第4章中,判斷字符串是否為回文一題,合理運(yùn)用cstring庫(kù)的 strcpy(), strcmp() 函數(shù)可以極大簡(jiǎn)化代碼;第5章中則有一題求?
的值,使用cmath庫(kù)的pow()函數(shù)也可以簡(jiǎn)化程序代碼。
理論上,調(diào)用這些函數(shù)總是需要包含相應(yīng)的文件:
但是,許多同學(xué)作業(yè)中沒(méi)有上述語(yǔ)句,并表示自己電腦上這么寫(xiě)就是能運(yùn)行。似乎是因?yàn)檩^新的版本的 IDE 已經(jīng)有自動(dòng)包含這些相關(guān)的文件。
后來(lái)我專(zhuān)門(mén)用機(jī)房電腦試了一下,經(jīng)過(guò)研究,我發(fā)現(xiàn):
在 VSCode+MinGW 環(huán)境(比如我的),上面兩個(gè)include缺一不可;
在 VS2019 及更高版本里面,cmath 與 cstring 庫(kù)都是無(wú)需專(zhuān)門(mén)聲明的,確實(shí)可以直接用這兩個(gè)庫(kù)的函數(shù);
在 VS2010 里面,cstring庫(kù)無(wú)需專(zhuān)門(mén)聲明,可以直接用;但cmath庫(kù)仍然是需要聲明的。
1.2 數(shù)組的聲明
在第四章教結(jié)構(gòu)化數(shù)據(jù)的時(shí)候,許多同學(xué)都找我問(wèn)過(guò)用變量n指定數(shù)組長(zhǎng)度的可行性。例如,希望先通過(guò)用戶(hù)輸入指定變量n的值,再以此作為創(chuàng)建的新數(shù)組的長(zhǎng)度。他們嘗試使用以下代碼:
并且會(huì)發(fā)現(xiàn)這么做會(huì)報(bào)錯(cuò)。這是因?yàn)?,C++程序編譯的時(shí)候,N的值還未知,不知道為數(shù)組a分配多少內(nèi)存空間,因此會(huì)直接報(bào)錯(cuò)。正確的做法應(yīng)該要用到第六章中動(dòng)態(tài)分配內(nèi)存的方法:
通過(guò)這種方式,在程序中為數(shù)組a分配動(dòng)態(tài)的內(nèi)存空間,解決了以上問(wèn)題。
但是,經(jīng)過(guò)另一位助教學(xué)長(zhǎng)的提醒,我在自己的電腦上(VSCode+MinGW)試了一下,發(fā)現(xiàn)前面那個(gè)簡(jiǎn)單直接的方法是可以運(yùn)行的。當(dāng)然這種操作終歸是不規(guī)范的,大家學(xué)了第六章之后,還是應(yīng)該用動(dòng)態(tài)分配內(nèi)存的方法。
1.3 數(shù)組的越界訪(fǎng)問(wèn)
第四章中講了創(chuàng)建數(shù)組的方法,并明確了訪(fǎng)問(wèn)數(shù)組內(nèi)容時(shí)下標(biāo)不能越界。事實(shí)上,在 VS2010 中越界訪(fǎng)問(wèn)數(shù)組就會(huì)直接報(bào)錯(cuò):
嚴(yán)格來(lái)說(shuō)數(shù)組越界訪(fǎng)問(wèn)本身就是個(gè)違規(guī)操作,沒(méi)必要單獨(dú)拿出來(lái)講。不過(guò),這個(gè)錯(cuò)誤操作有一個(gè)神奇的特性,想在這里分享給大家。
首先,上面那兩行代碼雖然在VS2010會(huì)直接報(bào)錯(cuò),但是用 MinGW 編譯器可以正常運(yùn)行。雖然 a[3] 對(duì)應(yīng)的內(nèi)存地址沒(méi)有專(zhuān)門(mén)分配給數(shù)組 a,程序仍然會(huì)在這個(gè)地址上寫(xiě)入4.
然后,如果執(zhí)行以下代碼:
理論上這個(gè)代碼應(yīng)該循環(huán)4次并輸出4個(gè)1. 但是實(shí)際測(cè)試發(fā)現(xiàn)這段代碼會(huì)進(jìn)入死循環(huán)!
循環(huán)體內(nèi)添加斷點(diǎn)并進(jìn)行調(diào)試,發(fā)現(xiàn) i 的值在012012... 不斷循環(huán)。
我們兩位助教討論后,終于找到原因:由于一開(kāi)始只為數(shù)組 a 分配了3個(gè)數(shù)的內(nèi)存,for 循環(huán)中創(chuàng)建的局部變量 i 的地址就會(huì)緊接在 a[2] 的后面。接下來(lái),當(dāng)循環(huán)體進(jìn)入第4次循環(huán)時(shí),i=3,所以?a[3]?表達(dá)式強(qiáng)行越界訪(fǎng)問(wèn),其實(shí)訪(fǎng)問(wèn)的是 i 的地址!也就是說(shuō),這里 a[3]=0 其實(shí)等價(jià)于 i=0,這就是為什么上面這段程序陷入死循環(huán)了!以上就是這個(gè)好玩的特性。

雖然這里給大家介紹了不同版本的微妙區(qū)別,但是記住本課程目前以VS2010老古董版本為準(zhǔn),所以前面說(shuō)的VS2019無(wú)需調(diào)用庫(kù)的情況,即使程序在你的環(huán)境能運(yùn)行,如果在10版跑不起來(lái)還是會(huì)被扣分。只要嚴(yán)格遵守規(guī)范的語(yǔ)法,絕大多數(shù)時(shí)候不同版本還是能兼容的。

2 如何優(yōu)雅高效地解決問(wèn)題
關(guān)于這個(gè)問(wèn)題,我想從兩個(gè)方面討論:如何更好地編輯和調(diào)試代碼;如何優(yōu)化解決問(wèn)題的算法。
2.1 更好的編程環(huán)境
雖然本課程學(xué)習(xí)內(nèi)容以 Visual Studio 2010 為準(zhǔn),但如果大家將來(lái)還需要常常用到 C++ 解決問(wèn)題,我們還是建議大家給自己配置一個(gè)更好的代碼編輯器和或 IDE。無(wú)論是新版本的 VS 還是配置了 MinGW 環(huán)境的?VSCode 都能夠給用戶(hù)更好的編程體驗(yàn),其優(yōu)勢(shì)主要體現(xiàn)在更完善的錯(cuò)誤提示、自動(dòng)補(bǔ)全等,極大提高了編程效率。筆者之前曾經(jīng)花了一周時(shí)間研究 VSCode 的 C++ 環(huán)境配置,并在這篇專(zhuān)欄中總結(jié)了相關(guān)經(jīng)驗(yàn):

當(dāng)然如果想嘗試這個(gè),請(qǐng)優(yōu)先保證課上的內(nèi)容已經(jīng)聽(tīng)懂了,作業(yè)做完了而且學(xué)有余力,并保證至少半天空閑時(shí)間。VSCode環(huán)境配置對(duì)新人來(lái)說(shuō)還是挺麻煩的,我自己摸索整整花了一周。但是一旦環(huán)境配好,你的工作效率將得到飛躍。
2.2 斷點(diǎn)調(diào)試
隨著編程作業(yè)越來(lái)越復(fù)雜,有時(shí)算法出現(xiàn)問(wèn)題,輸出錯(cuò)誤,而我們往往面對(duì)一堆代碼無(wú)從下手。這時(shí)斷點(diǎn)調(diào)試絕對(duì)是個(gè)好辦法。具體操作大家可以看教材配套的上機(jī)實(shí)習(xí)第243頁(yè)。(其實(shí)上機(jī)實(shí)習(xí)這本書(shū)附錄很好用的...但凡大家都認(rèn)真看過(guò)我遇到的問(wèn)題得少一半)
添加斷點(diǎn)后,程序會(huì)在斷點(diǎn)處暫停,我們可以實(shí)時(shí)監(jiān)視斷點(diǎn)處各個(gè)變量、常量、指針的狀態(tài)和值,對(duì)我們找到程序中可能的問(wèn)題很有幫助。
比如說(shuō),第5章作業(yè)題:
驗(yàn)證組合數(shù)公式
這道題要求程序分別計(jì)算兩邊的值,判斷是否相等。有的人雖然程序輸出了yes,但仍然被我找到問(wèn)題扣分。找出這些問(wèn)題就是通過(guò)斷點(diǎn)調(diào)試:在完成計(jì)算的地方添加斷點(diǎn)后發(fā)現(xiàn)計(jì)算結(jié)果其實(shí)不對(duì),屬于歪打正著。
再比如第3章的那道牛頓法的題,我看到你的結(jié)果就能推測(cè)你的程序哪里錯(cuò)了,也是通過(guò)斷點(diǎn):通過(guò)循環(huán)體內(nèi)加入斷點(diǎn)就可以確定程序一共經(jīng)歷幾次循環(huán),從而推測(cè)可能的問(wèn)題。
2.3 算法優(yōu)化(這部分是譚助教上機(jī)課講解的內(nèi)容)
還是以上面那道驗(yàn)證組合數(shù)的題為例,我們發(fā)現(xiàn)大家計(jì)算組合數(shù)的方法普遍是
然而,這樣做必須先計(jì)算3個(gè)階乘再相除,太慢了,而且分子太大容易溢出!一個(gè)最簡(jiǎn)單的優(yōu)化就是
更好的辦法是考慮到
然后利用遞歸即可:

考慮進(jìn)一步的優(yōu)化,遞歸需要多次重復(fù)調(diào)用函數(shù),不妨考慮建一個(gè)二維數(shù)組,空間換時(shí)間:

終極優(yōu)化:
