1. 程式人生 > >《程式設計實踐》筆記

《程式設計實踐》筆記

第一章 風格

  • 全域性變數使用具有說明性的名字,區域性變數用短名字。

  • 按常規方式使用的區域性變數可以採用極短的名字。

  • 保持一致性。相關的東西應給以相關的名字,以說明它們的關係和差異。

  • 對返回布林型別值 (真或者假)的函式命名,應該清楚地反映其返回值情況。

  • 使用表示式的自然形式。表示式應該寫得你能大聲念出來。

  • 關係運算符 ( < <= == != >= > )比邏輯運算子(& &和| |)的優先順序更高。

  • 特定風格遠沒有一致地使用它們重要。應該取一種風格,當然作者希望是他們所採用的風格,然後一致地使用。

  • 一致地使用習慣用法還有另一個優點,那就是使非標準的迴圈很容易被注意到,這種情況常常預示著有什麼問題。

  • 應該把所有的else垂直對齊,而不是分別讓每個else與對應的if對齊。採用垂直對齊能夠強調所有測試都是順序進行的,而且能防止語句不斷退向頁的右邊緣。

  • “從上面掉下”的方式在一種情況下是可以接受的,那就是幾個case使用共同的程式碼段。

  • 函式巨集最常見的一個嚴重問題是:如果一個引數在定義中出現多次,它就可能被多次求值。

  • 除了0和1之外,程式裡出現的任何數大概都可以算是神祕的數,它們應該有自己的名字。

  • 使用巨集進行程式設計是一種很危險的方式,因為巨集會在背地裡改變程式的詞法結構。我們應該讓語言去做正確的工作。

  • 如果在註釋中只說明程式碼本身已經講明的事情,或者與程式碼矛盾,或是以精心編排的形式干擾讀者,那麼它們就是幫了倒忙。最好的註釋是簡潔地點明程式的突出特徵,或是提供一種概觀,幫助別人理解程式。

  • 註釋應該提供那些不能一下子從程式碼中看到的東西,或者把那些散佈在許多程式碼裡的資訊收集到一起。

  • 有些程式碼原本非常複雜,可能是因為演算法本身很複雜,或者是因為資料結構非常複雜。在這些情況下,用一段註釋指明有關文獻對讀者也很有幫助。此外,說明做出某種決定的理由也很有價值。

  • 許多註釋在寫的時候與程式碼是一致的。但是後來由於修正錯誤,程式改變了,可是註釋常常還保持著原來的樣子,從而導致註釋與程式碼的脫節。無論產生脫節的原因何在,註釋與程式碼矛盾總會使人感到困惑。由於誤把錯誤註釋當真,常常使許多實際查錯工作耽誤了大量時間。

  • 註釋是一種工具,它的作用就是幫助讀者理解程式中的某些部分,而這些部分的意義不容易通過程式碼本身直接看到。

第二章 演算法與資料結構

略。

第三章 設計與實現

本章設計了一個用馬爾科夫鏈隨機生成可以讀的英文文字的程式,演算法如下:

設定w1和w2為文字的前兩個詞
輸出w1和w2
迴圈:
    隨機地選出 w3,它是文字中w1、w2的字尾中的一個。
    列印w3。
    把w1和w2分別換成w2和w3。
重複迴圈

選擇了資料結構:

每個狀態由一個字首和一個字尾連結串列組成。所有這些資訊存在一個散列表裡,以字首作為關鍵碼。每個字首是一個固定大小的詞集合。如果一個字尾在給定字首下的出現多於一次,則每個出現都單獨包含在有關連結串列裡。

並分別用Java、C++、Awk和Perl實現了它。不同語言的效能對比如下:
這裡寫圖片描述
本章最後一節是關於設計的經驗教訓,主要內容有:

  • 很容易把 Perl和Awk程式改造成使用一個詞或三個詞字首的程式,但要想使這個選擇能夠引數化,就會遇到很多麻煩。

  • 使用較高階的語言比更低階的語言寫出的程式速度更慢,但這種說法只是定性的,把它隨意推廣也是不明智的。大型構件,如 C++的STL或指令碼語言裡的關聯陣列、字串處理,能使程式碼更緊湊,開發時間也更短。

  • 當系統內部提供的程式碼太多時,人們將無法知道程式在其表面之下到底做了什麼。我們應該如何評價這種對控制和洞察力的喪失,這是更不清楚的事情。這也就是 STL版本中遇到的情況,它的效能無法預料,也沒有很容易的辦法去解決問題。

  • 當所有東西都正常運轉時,功能豐富的程式設計環境可以是非常有生產效率的,但是如果它們出了毛病,那就沒什麼東西可以依靠了。如果問題牽涉到的是效能或者某些難於捉摸的邏輯錯誤時,我們很可能根本沒有意識到有什麼東西出了毛病。

  • 最好是從資料結構開始,在關於可以使用哪些演算法的知識的指導下進行詳細設計。當資料結構安置好後,程式碼就比較容易組織了。

  • 要想先把一個程式完全設計好,然後再構造它,這是非常困難的。構造現實的程式總需要重複和試驗。構造過程逼迫人們去把前面粗略做出的決定弄清楚。

  • 做產品程式碼要花費的精力比做原型多得多。例如可以把這裡給出的程式看作是產品程式碼( 因為它們已經被仔細打磨過,並經過了徹底的測試 )。產品質量要求我們付出的努力要比個人使用的程式高一兩個數量級。

第四章 介面

在本章中,作者實現了一個可以讀csv檔案,並允許使用者讀取其中某一行或某一列的庫。
首先,作者實現了一個原型,這個原型具有最基本的功能,但在隨後的過程中將被拋棄。如果這個程式是提供給別人用的,在原始設計中的這些倉促選擇引起的麻煩就可能到許多年後才浮現出來。這正是許多不良介面的歷史畫卷。
接下來,作者構造了一個具有普遍使用價值的庫。在此過程中,需要考慮的主要問題有:

  • 資訊隱藏。這個庫應該對輸入行長或域的個數沒有限制。為了達到這個目的,或者是讓呼叫程式為它提供儲存,或者是被呼叫者 ( 庫) 自己需要做分配。

  • 資源管理。必須確定誰負責共享的資訊。函式 c s v g e t l i n e是直接返回原始資料,還是做一個拷貝?

  • 誰來開啟和關閉檔案?做檔案開啟的部分也應負責關閉:互相匹配的操作應該在同一個層次或位置完成。

  • 在錯誤發生的時候,庫函式絕不能簡單地死掉,而是應該把錯誤狀態返回給呼叫程式,以便那裡能採取適當的措施。

  • 如果 csvfield或 csvnfiled在csvgetline遇到EOF之後被呼叫,它們的返回值是什麼?具有錯誤形式的域將如何處理?

C++實現:略。

介面原則:

  • 隱藏實現細節。對於程式的其他部分而言,介面後面的實現應該是隱藏的,這樣才能使它的修改不影響或破壞別的東西。

  • 應該避免全域性變數。如果可能,最好是把所有需要引用的資料都通過函式引數傳遞給函式。

  • 一般地說,窄的介面比寬的介面更受人歡迎,至少是在有了強有力的證據,說明確實需要給介面增加一些新功能之前。

  • 不要在使用者背後做小動作。一個庫函式不應該寫某個祕密檔案、修改某個祕密變數,或者改變某些全域性性資料,在改變其呼叫者的資料時也要特別謹慎。

  • 一個介面在使用時不應該強求另外的東西,如果這樣做僅僅是為了設計者或實現者的某些方便。

  • 外部一致性,與其他東西的行為類似也是非常重要的。

  • 在設計庫( 或者類、包 ) 的介面時,一個最困難的問題就是管理某些資源,這些資源是庫所擁有的,而又在庫和它的呼叫程式之間共享。粗略地說,有關的問題大致涉及初始化、狀態維護、共享和複製以及清除等等。

  • 釋放資源與分配資源應該在同一個層次進行。控制資源分配和回收有一種基本方式,那就是令完成資源分配的同一個庫、程式包或介面也負責完成它的釋放工作。

  • 為了避免出問題,我們必須把程式碼寫成可重入的,也就是說,無論存在多少個同時的執行,它都應該能正常工作。可重入程式碼要求避免使用全域性變數、靜態區域性變數以及其他可能在別的執行緒裡改變的變數。

  • 作為一條具有普遍意義的規則,錯誤應該在儘可能低的層次上檢測和發現,但應該在某個高一些的層次上處理。

  • 如果能把各種各樣的異常值 ( 如檔案結束、可能的錯誤狀態 )進一步區分開,而不是用單個返回值把它們堆在一起,那當然就更好了。

  • 異常機制不應該用於處理可預期的返回值。讀一個檔案最終總要遇到檔案結束,這個情況就應該以返回值的方式處理,而不是通過異常機制。

  • 異常機制常常被人過度使用。由於異常是對控制流的一種旁路,它們可能使結構變得非常複雜,以至成為錯誤的根源。檔案無法開啟很難說是什麼異常,在這種情況下產生一個異常有點過分。異常最好是保留給那些真正無法預期的事件,例如檔案系統滿或者浮點錯誤等等。

  • 在發生錯誤時應該如何恢復有關的資源?如果發生了錯誤,庫函式應該設法做這種恢復嗎?通常它們不做這些事,但也可以在這方面提供一些幫助:提供儘可能清楚的資訊和以儘可能無害的方式退出。

  • 錯誤資訊、提示符或對話方塊中的文字應該對合法輸入給出說明。不要簡單地說一個引數太大,而應說明引數值的合法範圍。如果可能的話,給出的這段文字本身最好就是一段合法的輸入,比如提供一個帶合適引數的完整命令列。

  • 從使用者的觀點看,風格問題,如簡單性、清晰性、規範性、統一性、熟悉性和嚴謹性等,對於保證一個介面容易使用都是非常重要的,不具有這些性質的介面必定是令人討厭的難對付的介面。

第五章 排錯

排錯系統:

  • 在程式設計語言的發展中,一個重要的努力方向就是想通過語言特徵的設計幫助避免錯誤。

  • 每個為預防某些問題而設定的語言特徵都會帶來它自己的代價。如果一個高階語言能自動地去掉一些簡單的錯誤,其代價就是使得它本身很容易產生一個高階的錯誤。

  • 有些程式用排錯系統很難處理,例如多程序的或多執行緒的程式、作業系統和分散式系統,這些程式通常只能通過低
    級的方法排錯。

  • 作為個人的觀點,我們傾向於除了為取得堆疊軌跡和一兩個變數的值之外不去使用排錯系統。

有線索的簡單錯誤:

  • 檢查最近的改動。哪個是你的最後一個改動?如果你在程式發展中一次只改動了一個地方,那麼錯誤很可能就在新的程式碼裡,或者是由於這些改動而暴露出來。

  • 不要兩次犯同樣的錯誤。當你改正了一個錯誤後,應該問問自己是否在程式裡其他地方也犯過同樣錯誤。

  • 一個有效的但卻沒有受到足夠重視的排錯技術,那就是非常仔細地閱讀程式碼,仔細想一段時間,但是不要急於去做修改。

  • 應該稍微休息一下。有時你看到的程式碼實際上是你自己的意願,而不是你實際寫出的東西。離開它一小段時間能夠鬆弛你的誤解,幫助程式碼顯出其本來面目。

  • 另一種有效技術就是把你的程式碼解釋給其他什麼人,這常常會使你把錯誤也給自己解釋清楚了。

無線索,難辦的錯誤:

  • 把錯誤弄成可以重現的。第一步應該是設法保證你能夠使錯誤按自己的要求重現。

  • 如果無法把錯誤弄成每次都出現的,那麼就應該設法弄清為什麼做不到。是否在某些條件下能使它比在其他條件下出現得更頻繁?即使你無法保證錯誤每次都出現,如果你能減少等待它出現的時間,也就能夠更快地找到它。

  • 能否把導致程式失敗的輸入弄得更小一點,或者更集中一點?設法構造出最小的又能保證錯誤現身的輸入,這樣可以減少可能性。什麼樣的變化使錯誤不見了?

  • 採用二分檢索的方式,丟掉一半輸入,看看輸出是否還是錯的。如果不是,回到前面狀態,丟掉輸入的另一半。

  • 研究錯誤的計數特性。有時失敗的例項具有計數特徵方面的模式,這常常是很好的線索,能使我們在尋找中集中注意力。

  • 顯示輸出,使搜尋區域性化。

  • 寫自檢測程式碼。如果需要更多的資訊,你可以寫自己的檢查函式去測試某些條件、打印出相關變數的值或者終止程式。

  • 另一種戰術是寫一個記錄檔案,以某種固定格式寫出一系列的排錯輸出。當程式垮臺的時候,這個檔案裡已經記錄了垮臺前發生的情況。

  • 如果上面的建議都沒有用,那麼又該怎麼辦?這可能是使用一個好的排錯系統,以步進方式遍歷程式的時候了。

  • 如果你在做了大量努力後還是不能找到錯誤,那麼就應該休息一下。清醒一下你的頭腦,做一些別的事情,和一個朋友談談,請求幫助。問題的答案可能會突然從天而降。

  • 偶然也會遇到這種情況,問題確實出在編譯系統,或者庫,或者作業系統,甚至是計算機硬體,特別是如果在錯誤出現的環境裡的什麼東西剛剛換過。

不可重現的錯誤:

  • 應該先檢查所有的變數是否都正確地進行了初始化。

  • 如果在增加排錯程式碼之後錯誤的行為改變了,甚至是消失了,那麼它很可能就是一個儲存分配錯誤—某個時候你的程式碼在被分配的儲存之外寫了什麼東西。

其他人的程式錯誤:

  • grep一類的文字搜尋程式有助於找到所有出現的名字;

  • 交叉引用程式可以幫人看清程式結構的某些思想;

  • 顯示函式呼叫圖 (如果不太大的話)也很有價值;

  • 用一個排錯系統,以步進方式一個一個函式地執行程式,可以幫人看清事件發生的順序;

  • 程式的版本歷史可以給人一些線索,顯示出隨著時間變化人們對程式做了些什麼。

第六章 測試

  • 問題當然是發現得越早越好。如果你在寫程式碼時就係統地考慮了應該寫什麼,那麼也可以在程式構造過程中驗證它的簡單性質。

  • 測試程式碼的邊界情況。

  • 防止問題發生的另一個方法,是驗證在某段程式碼執行前所期望的或必須滿足的性質 ( 前條件)、執行後的性質 ( 後條件) 是否成立。

  • 斷言機制對於檢驗介面性質特別有用,因為它可以使人注意到呼叫和被呼叫之間的不一致性,並可以進一步指出麻煩究竟是出在哪裡。

  • 有一種很有用的技術,那就是在程式裡增加一些程式碼,專門處理所有“不可能”出現的情況,也就是處理那些從邏輯上講不可能發生,但是或許 ( 由於其他地方的某些失誤)可能出現的情況。

  • 檢查錯誤的返回值。一個常被忽略的防禦措施是檢查庫函式或系統呼叫的返回值。

  • 檢查輸出函式( 例如fprintf或fwrite)的返回值也可以發現一些錯誤,例如,要向一個檔案寫入,而磁碟上已經沒有空間了。

  • 在程式設計的過程中測試,其花費是最小的,而回報卻特別優厚。在寫程式過程中考慮測試問題,得到的將是更好的程式碼,因為在這時你對程式碼應該做些什麼瞭解得最清楚。如果不這樣做,而是一直等到某種東西崩潰了,到那時你可能已經忘記了程式碼是怎樣工作的。即使是在強大的工作壓力下,你也還必須重新把它弄清楚,這又要花費許多時間。

系統化測試:

  • 以遞增方式做測試。測試應該與程式的構造同步進行。

  • 測試應該首先集中在程式中最簡單的最經常執行的部分,只有在這些部分能正確工作之後,才應該繼續下去。這樣,在每個步驟中你使更多的東西經過了測試,對程式基本機制能夠正確工作也建立了信心。

  • 如果一個程式有逆計算,那麼就檢查通過該逆計算能否重新得到輸入。

  • 檢驗應保持不變的特徵。

  • 對於那些應該保持不變的特徵,實際上也可以在程式內部進行檢查。

  • 有時一個回答可以由兩條完全不同的途徑得到,或許你可以寫出一個程式的某種簡單版本,作為一個慢的但卻又是獨立的參照物。

  • 度量測試的覆蓋面(完全覆蓋常常很難做到,即使是不考慮那些“不可能發生”的語句)。

測試自動化:

  • 自動化的最基本形式是迴歸測試,也就是說執行一系列測試,對某些東西的新版本與以前的版本做一個比較。在更正了一個錯誤之後,人們往往有一種自然的傾向,那就是隻檢查所做修改是否能行,但卻經常忽略問題的另一面,所做的這個修改也可能破壞了其他東西。

  • 迴歸測試實際上有一個隱含假定,假定程式以前的版本產生的輸出是正確的。這個情況必須在開始時仔細進行審查,使這些不變性質能夠一絲不苟地維持下去。

  • 如果你發現了一個程式錯誤,那麼又該怎麼辦?如果這個錯誤不是通過已有的測試發現的,那麼你就應該建立一個能發現這個問題的新測試,並用那個崩潰的程式碼版本檢驗這個測試。

  • 不要簡單地把測試丟掉,因為它能夠幫你確定一個錯誤報告是否正確,或是說明某些東西已經更正了。

  • 要孤立地測試一個部件,通常必須構造出某種框架或者說是測試臺,它應能提供足夠的支援,並提供系統其他部分的一個介面,被測試部分將在該系統裡執行。

應力測試:

  • 採用大量由機器生成的輸入是另一種有效的測試技術。機器生成的輸入對程式的壓力與人寫的輸入有所不同。量大本身也能夠破壞某些東西,因為大量的輸入可能導致輸入緩衝區、陣列或者計數器的溢位。

  • 通過隨機輸入測試,考查的主要是程式的內部檢查和防禦機制,因為在這種情況下一般無法驗證程式產生的輸出是否正確。這種測試的目標主要是設法引起程式垮臺,或者讓它出現“不可能發生的情況” ,而不是想發現直接的錯誤。

  • 有些測試是針對明顯的惡意輸入進行的。安全性攻擊經常使用極大的或者不合法的輸入,設法引起對已有資料的覆蓋。

測試祕訣:

  • 讓雜湊函式返回某個常數值,使所有元素都跑到同一個雜湊桶裡。

  • 寫一個你自己的儲存分配函式,有意讓它早早地就失敗,利用它測試在出現儲存器耗盡錯誤時設法恢復系統的那些程式碼。

  • 把陣列和變數初始化為某個可辨認的值,而不是總用預設的 0。這樣,如果出現越界訪問,或者取到了一個未初始化的變數值,你將更容易注意到它。

  • 變動你的測試例項,特別是在用手工做小測試時。總使用同樣東西很容易使人陷入某種常規,很可能忽略了其他的崩潰情況。

  • 如果已經發現有錯誤存在,那麼就不要繼續設法去實現新特徵或者再去測試已有的東西,因為那些錯誤有可能影響測試的結果。

  • 測試輸出中應該包括所有的引數設定,這可以使人容易準確地重做同樣的測試。

  • 在不同的機器、編譯系統和作業系統上做測試。

  • 你應該試著不考慮程式碼本身,仔細考慮最困難的測試例項,而不是那些容易的。

  • 互動式程式應該能夠通過指令碼控制,這樣我們就可以用指令碼模擬使用者的行為,使測試可以通過程式完成。這方面的一種技術是捕捉真實使用者的動作,並重新播放它;另一種技術是建立一個能表述事件序列和時間的指令碼語言。

  • 最後,還應該想一想如何測試所用的測試程式碼本身。

第七章 效能

  • 優化的第一要義是不做。程式是不是已經足夠好了?

  • 測量是改進效能過程中最關鍵的一環,推斷和直覺都很容易受騙,所以在這裡必須使用各種工具,如計時命令或輪廓檔案等等。

  • 對於寫得很好的程式碼,很小的改變往往就能解決它的效能問題,而對那些設計拙劣的程式碼,經常會需要大範圍地重寫。

計時:

  • 自動計時測量。 許多系統裡都有這種命令,它們可用於測量一個程式到底用了多少時間。

  • 除了可靠的計時方法外,在效能分析中最重要的工具就是一種能產生輪廓檔案的系統。

  • 畫一個圖。圖形特別適合用來表現效能測量的情況。

加速策略(按照獲益遞減的順序):

  • 在你知道發生了什麼之後,有一些可以採用的策略。我們列出幾個,按照獲益遞減的順序。

  • 開啟編譯系統的優化開關

  • 編譯優化做得越多越深入,把錯誤引進編譯結果 (程式)的可能性也就越大。

  • 如果已經選擇了正確的演算法,程式的速度仍然是問題的話,下一步還能做的就是調整程式碼,整理迴圈和表示式的細節,設法使事情做得更快些。

  • 如果有一個程式要執行一年,那麼就應該從中擠壓出你所能做到的一切。甚至在這個程式已經運行了一個月以後,如果你發現了一種能使它改進百分之十的方法,可能也值得從頭再開始一次。存在競爭物件的程式—遊戲程式、編譯系統、字處理系統、電子表格系統和資料庫系統等,都應該納入這個類別。

程式碼調整:

  • 快取記憶體頻繁使用的值。程式和人都有這種傾向,那就是重複使用最近訪問過的值,或者是近旁的值,而對老的值和遠距離的值則用得比較少。

  • 如果程式中需要的經常是同樣大小的儲存塊,採用一個特定用途的儲存分配器取代一般的分配器,有可能使速度得到實質性提高。

  • 對輸入輸出做緩衝。

  • 特殊情況特殊處理。

  • 預先算出某些值。

  • 使用近似值。

  • 在某個低階語言裡重寫程式碼。

估計

  • 為一個語言或者系統做一次代價模擬卻是比較簡單的,它至少能使你對各種重要操作花費的時間有一個粗略的概念
    例如,我們用一個 C和C++ 代價模擬程式估計了一些獨立語句的代價,採用的方法就是把它們放在迴圈裡,執行成百萬次,然後計算出平均時間。

  • 在這裡也還有很多變數。一個是編譯系統優化的級別。對有關執行情況難以做出預計,另一個重要的原因是計算機的體系結構。