如何優雅的編程——C語言界面的一點小建議

    咱們鼓勵在編程時應有清晰的哲學思惟,而不是給予硬性規則。我並不但願大家能承認全部的東西,由於它們只是觀點,觀點會隨着時間的變化而變化。但是,若是不是直到如今把它們寫在紙上,長久以來這些基於許多經驗的觀點一直積累在個人頭腦中。所以但願這些觀點能幫助大家,瞭解如何規劃一個程序的細節。(我尚未看到過一篇講關於如何規劃整個事情的好文章,不過這部分能夠是課程的一部分)要是能發現它們的特質,那很好;要是不認同的話,那也很好。但若是能啓發大家思考爲何不認同,那樣就更好了。在任何狀況下,都不該該照搬我所說的方式進行編程;要用你認爲最好的編程方式來嘗試完成程序。請一以貫之並且絕不留情的這麼作。node

    歡迎在評論區留言討論~程序員

1 排版問題

    程序是一種出版物。意味着程序員們會先閱讀(也許是幾天、幾周或幾年後的你本身閱讀),最後才輪到機器。機器的快樂就是程序能編譯,機器纔不在意程序寫的有多麼漂亮,但是人們應該保持程序的美觀。有時人們會過分關心:用漂亮的打印機呆板地打印出漂亮的輸出,而這些輸出只是將全部介詞用英文文本以粗體字體凸顯出來,都是些與程序無關的細節。雖然有不少人認爲程序就應該像 Algol.68 所描述的同樣(有些系統甚至要求照搬該風格編寫程序),可清晰的程序不會由於這樣的呈現而變得更清晰,只會使糟糕的程序變得更好笑。算法

    對於清晰的程序來講,排版規範一貫都是相當重要的。固然,衆所周知最有用的是縮進,可是當墨水遮蓋了意圖時,就會控制住排版。所以即使堅持使用簡單的舊打字機輸出,也該意識到愚蠢的排版。避免過分修飾,好比保持註釋的簡潔和靈活。經過程序整齊一致地說出想表達的。接着往下看。編程

2 變量命名

    對於變量名稱,長度並非名稱的價值所在,清晰的表達纔是。不經常使用的全局變量可能會有一個很長的名稱,像 maxphysaddr。在循環中每一行所使用的數組索引,並不須要取一個比 i 更詳盡的名字。取 index 或者 elementnumber 會輸入更多的字母(或調用文本編輯器),而且會遮蓋住計算的細節。當變量名稱很長時,很難明白髮生了什麼。在必定程度上,這是排版問題,看看下面數組

for(i=0 to 100)
                array[i]=0

vs.網絡

for(elementnumber=0 to 100)
                array[elementnumber]=0;

    現實例子中的問題會變得更糟。因此僅需把索引當成符號來對待。數據結構

    指針也須要合理的符號。np 僅僅只是做爲指針 nodepointer 的助記符。若是一向都聽從命名規範,那麼很容易就能推斷出 np 表示「節點指針」。在下一篇文章中會提到更多。編程語言

    同時在編程可讀性的其它方面,一致性也是極其重要的。假使變量名爲 maxphysaddr,則不要給同級關係的變量取名 lowestaddress。編輯器

    最後,我傾向於「最小長度」但「最大信息量」的命名,並讓上下文補齊其他部分。例如:全局變量在使用時不多有上下文幫助理解,那麼它們的命名相對而言更須要使人易懂。所以我稱 maxphyaddr 做爲一個全局變量名,對於在本地定義和使用的指針來講 np 並不必定是 NodePoint。這是品味的問題,但品味又與清晰度相關。函數

    我避免在命名時嵌入大寫字母;它們的閱讀溫馨性太彆扭了,像糟糕的排版同樣使人心煩。

3 指針的使用

    C 語言不一樣尋常,由於它容許指針指向任何事物。指針是鋒利的工具,像任何這樣的工具同樣,使用得當能夠產生使人愉悅的生產力,但使用不當也能夠形成極大的破壞。指針在學術界的名聲不太好,由於它太危險了,莫名其妙地就變得糟糕的不行。但我認爲它是強大的符號,它能夠幫助咱們清楚地自我表達。

    思考:當有指針指向對象時,對於那個對象,確切地說它只是名稱,其它什麼也不是。聽起來很瑣碎,但看看下面的兩個表達式:

np
node[i]

    第一個指向一個 node(節點),第二個計算爲(能夠說)同一個 node。但第二種形式是不太容易理解的表達式。這裏解釋一下,由於咱們必需要知道 node 是什麼,i 是什麼,還要知道 i 和 node 與周圍程序之間相關的規則是什麼。孤立的表達式並不能說明 i 是 node 的有效索引,更不用提是咱們想要元素的索引。若是 i、j 和 k 都是 node 數組中的索引將很容易出差錯,並且連編譯器都不能幫助找出錯誤。當給子程序傳參數時,尤爲容易出錯:指針只是一個單獨的參數;但在接收的子程序中必須認爲數組和索引是一體的。

    計算爲對象表達式自己,比該對象的地址更不易察覺,並且容易出錯。正確使用指針能夠簡化代碼:

parent->link[i].type

vs.

lp->type.

    若是想取下一個元素的 type 能夠是

parent->link[++i].type

    或

(++lp)->type.

    i 前移,但其他的表達式必須保持不變;用指針的話,只須要作一件事,就是指針前移。

    把排版因素也考慮進來。對於處理連續的結構體來講,使用指針比用表達式可讀性更好:只須要較少的筆墨,並且編譯器和計算機的性能消耗也很小。與此相關的問題是,指針類型會影響指針正確使用,這也就容許在編譯階段使用一些有用的錯誤檢測,來檢查數組序列不能分開。並且若是是結構體,那麼它們的標籤字段就是其類型的提示。所以

np->left

    是足以讓人明白的。若是是索引數組,數組將取一些精心挑選的名字,並且表達式也會變得更長:

node[i].left.

    此外,因爲例子變得愈來愈大,額外的字符更加讓人惱火。

    通常來講,若是發現代碼中包含許多類似並複雜的表達式,並且表達式計算爲數據結構中的元素,那麼明智地使用指針能夠消除這些問題。考慮一下

if(goleft)
             p->left=p->right->left;
        else
             p->right=p->left->right;

    看起來像利用複合表達式表示 p。有時這值得用一個臨時變量(這裏的 p)或者把運算提取成一個宏。

4 過程名稱

    過程名稱應該代表它們是作什麼的,函數名稱應該代表它們返回什麼。函數一般在像 if 這樣的表達式使用,所以可讀性要好。

if(checksize(x))

    是沒有太大幫助的,由於不能推斷出 checksize 錯誤時返回 true,仍是非錯誤時返回。相反

if(validsize(x))

    使這點能清晰表達,而且在常規使用中未來也不大可能出錯。

5 註釋

    這一個微妙的問題,須要本身體會和判斷。因爲一些緣由,我傾向於寧肯清除註釋。第一,假如代碼清晰,而且使用了規範的類型名稱和變量名稱,應該從代碼自己就能夠理解。第二,編譯器不能檢查註釋,所以不能保證準確,特別是代碼修改過之後。誤導性的註釋會很是使人困惑。第三,排版問題:註釋會使代碼變得雜亂。

    但有時我會寫註釋,像下文同樣僅僅只是把它們用於介紹。例如:解釋全局變量的使用和類型(我老是在龐大的程序中寫註釋);做爲一個不尋常或者關鍵過程的介紹;或標記出大規模計算的一節。

    有一個糟糕註釋風格的例子:

i=i+1;           /* Add one to i */

    還有更糟糕的作法:

/**********************************
 *                                *
 *          Add one to i          *
 *                                *
 **********************************/

               i=i+1;

    先不要嘲笑,等到在現實中看到再去吧。

    或許除了諸如重要數據結構的聲明(對數據的註釋一般比對算法的更有幫助),這樣相當重要部分以外,須要避免對註釋的「可愛」排版和大段的註釋;基本上最好就不要寫註釋。若是代碼須要靠註釋來講明,那最好的方法是重寫代碼,以便能更容易地理解。這就把咱們帶到了複雜度。

6 複雜度

    許多程序過於複雜,比須要有效解決的問題更加複雜。這是爲何呢?大部分是因爲設計很差,但我會跳過這個問題,由於這個問題太大了。然而程序每每在微觀層面就很複雜,有關這些能夠在這裏解決。

  • 規則 1:不要判定程序會在什麼地方耗費運行時間。 瓶頸老是出如今使人意想不到的地方,直到證明瓶頸在哪,不要試圖再次猜想並加快運行速度。

  • 規則 2:估量(measure) 在沒有對代碼作出估量以前不要優化速度,除非發現最耗時的那部分代碼,要不也不要去作。

  • 規則 3:當 n 很小時(一般也很小),花哨的算法運行很慢。 花哨算法有很大的常數級別複雜度。在你肯定 n 老是很大以前, 不要使用花哨算法。(即便假如 n 變大,也優先使用規則 2).例如,對於常見問題,二叉樹總比伸展樹高效。

  • 規則 4:花哨的算法比簡單的算法更容易有 bug,並且實現起來也更困難 儘可能使用簡單的算法與簡單的數據結構。

    如下幾乎是全部實際程序中用到的數據結構:

  • 數組
  • 鏈表
  • 哈希表
  • 二叉樹

    固然也必需要有把這些數據結構靈活結合的準備,好比用哈希表實現的符號表,其中哈希表是由字符型數組組成的鏈表。

  • 規則 5:以數據爲核心 若是選擇了適當的數據結構並把一切都組織得頗有條理性,算法老是不言而喻的。編程的核心是數據結構,而不是算法。(參考 Brooks p. 102)

  • 規則 6:就是沒有規則 6。

7 數據編程

    不像許多 if 語句,算法或算法的細節一般以緊湊、高效和明確的數據進行編碼。眼前的工做能夠編碼,歸根究竟是因爲其複雜性都是由不相干的細節組合而成。分析表是典型例子,它經過一種解析固定、簡單代碼段的形式,對編程語言的語法進行編碼。有限狀態機特別適合這種處理形式,可是幾乎任何涉及到對構建數據驅動算法有益的程序,都是將某些抽象數據類型的輸入「解析」成序列,序列會由一些獨立「動做」構成。

    也許這種設計最有趣的地方是表結構有時能夠由另外一個程序生成(經典案例是解析生成器)。有個更接地氣的例子,假如操做系統是由一組表驅動,這組表包含鏈接 I/O 請求到相應設備驅動的操做,那麼能夠經過程序「配置「系統,該程序能夠讀取到某些特殊設備與可疑機器鏈接的描述,並打印相應的表。

    數據驅動程序在初學者中不常見的緣由之一是因爲 Pascal 的專制。 Pascal 像它的創始人同樣,堅信代碼要和數據分開。於是(至少在原始形式上)沒法建立初始化的數據。與圖靈和馮諾依曼的理論背道而馳,這些理論可都是定義存儲計算機的基本原理。代碼和數據是同樣的,或至少能夠算是。還能怎樣解釋編譯器的工做原理呢?(函數式語言對 I/O 也有相似的問題)

8 函數指針

    Pascal 專制的另外一個結果是初學者不使用函數指針。(在 Pascal 中沒有把函數做爲變量) 用函數指針來處理編碼複雜度會有一些使人感興趣的地方。

    指針指向的程序有必定的複雜度。這些程序必須遵照一些標準協議,像要求一組都是相同調用的程序就是其中之一。除此以外,所要實現的只是完成業務,複雜度是分散的。

    有個協議的主張是既然全部使用的功能類似,那麼它們的行爲也必須類似。這對簡單的文檔、測試、程序擴展和甚至使程序經過網絡分佈都有幫助——遠程過程調用能夠經過該協議進行編碼。

    我認爲面相對象編程的核心是清晰使用函數指針。規定好要對數據執行的一系列操做,以及對這些操做響應的整套數據類型。將程序合攏到一塊兒最簡單的方法是爲每種類型使用一組函數指針。簡而言之,就是定義類和方法。固然,面嚮對象語言提供了更多更漂亮的語法、派生類型等等,但在概念上幾乎沒有提出額外的東西。

    數據驅動程序與函數指針的結合,變成了一種表現使人驚訝的工做方法。根據個人經驗,這種方法常常會產生驚喜的結果。即便沒有面向對象語言,無需額外的工做也能夠得到 90% 的好處,而且能更好地管理結果。我沒法再推薦出更高標準的實現方式。我全部的程序都是由這種方式組織管理,並且通過屢次開發後都相安無事——遠遠優於缺乏約束的方法。也許正如所說:從長遠來看,約束會帶來豐厚的回報。

9 包含文件

    簡單規則:包含(include)文件時應該永遠不要嵌套包含。 若是聲明(在註釋或隱式聲明裏)須要的文件沒有優先包含進來,那麼使用者(程序員)要決定包含哪些文件,但要以簡單的方式處理,並採用避免多重包含的結構。多重包含是系統編程的禍根。將文件包含五次或更屢次來編譯一個單獨的 C 源文件的事情家常便飯。Unix 系統中 /usr/include/sys 就用了這麼可怕的方式。

    說到 #ifdef,有一個小插曲,雖然它能防止讀取兩次文件,但實際上常常用錯。#ifdef 是定義在文件自己中,而不是文件包含它。結果是經常致使讓成千上萬沒必要要的代碼經過詞彙分析器,這是(優秀編譯器中)耗費最大的階段。

    只需聽從以上簡單規則,就能讓你的代碼變得優雅而美觀,至少也是賞心悅目,從技術變成藝術~~

    最後仍是要推薦下小編的C/C++學習羣:710520381,邀請碼(柳貓),無論你是小白仍是大牛,小編我都歡迎,不按期分享乾貨,包括小編本身整理的一份2018最新的C/C++和0基礎入門教程,歡迎初學和進階中的小夥伴。

相關文章
相關標籤/搜索