C 語言,從 1970 年代設計並實現之初,它就注定了帶有強烈工程師文化的語言,而缺乏一些學術氣息。它的許多細節設計,都帶有強烈的實用化痕跡。C 語言因 UNIX 操作系統而生,是 UNIX 系統的母語。這導致在這個廣泛應用的操作系統上開發,必須通過 C 語言的形式和系統進行交互。這不僅影響了 UNIX 一個平台上的軟件,既而也影響了後來世界上最大的桌面系統 Windows ,以及越來越多的嵌入式平台。
由於大部分應用軟件最終都需要和操作系統打交道,所以用來開發應用軟件的語言,絕大部分也需要利用 C 語言完成和操作系統的通訊。這個世界上絕大部分流行的編程語言,都選擇了用 C 語言來實現其編譯器或解釋器,以及基礎部分的運行時庫。無論 C 語言設計本身有何種缺憾,在今天,它已無可取代。
到了今天,大部分程序員不再需要逐個時間周期的去摳程序的性能。不需要刻意追求速度最快,最節省系統資源的軟件。不需要寫那些和系統內核緊密聯系的程序。但 C 語言在此之外,依然有其重要的應用領域。我們可以把它作為對最終機器模型的高層次的統一抽象工具,而不必考慮機器環境的差異。經過 30 多年的發展,證明了 C 語言的確是對經典機器模型的最佳表述。僅僅通過增加了一個非常薄的膠合層就得到了一個清晰簡潔的設計。正是這一點,使得 C 語言在計算機硬件高速發展的幾十年中,一直生機勃勃。
我們在討論 C 語言時,其實不僅僅涉及了 C 語言本身那用三十幾個保留字構成的精簡的控制結構和簡約的語言特征。還包括了一套對 # 號打頭的預處理部分(尤其是基於文本替換的宏處理),以及某些慣用的源代碼組織方式(例如:所有的接口定義被定義在後綴為 h 的文件中,並通過預處理方式替換進源代碼),和基本的程序庫。
這幾部分語言核心之外的部分相對獨立。以至於使用 C 語言開發並不一定使用標准化的那些東西。C 語言對運行時環境的依賴是非常小的。
而編譯預處理器又使得語言富有彈性,甚至可以寫出違背 C 語言哲學的代碼。著名的 IOCCC 大賽展示了許多常人無法理解的 C 代碼。但實際上,C 語言主張代碼清晰,表裡如一。開發者和維護者都能很容易的預測每一行代碼背後的行為。避免存在一些陰暗的角落藏著一些罕見的用法導致程序運行時出現詭異的行為。C 語言在發展過程中一直堅持著最小意外原則。而這一點,正是 C 語言的一個著名發展分支 C++ 所偏離的東西。
C 語言並不是絕對意義上最快的語言。但是它的效率非常好,在切合大部分機器模型並給出統一抽象的基礎上,幾乎沒有其它語言做的更好了。這也是 C 語言哲學的一部分:在統一硬件抽象模型的基礎上,盡可能的利用所在硬件環境的一切資源。有時候,C 語言程序員會走向某種極端。追求語言細節的優化,覺得某種代碼的組織方式會比另一種方式更高效。但幾乎總是錯的。優化取決於對具體硬件的理解,以及對編譯器如何翻譯這些代碼的了解。但這正是設計 C 語言想避免的東西。我們不必去爭論在語句級上每行代碼精確開銷的優劣。
同時,C 語言的另一設計哲學就是讓每行 C 代碼盡量准確的對應相當數量的目標機器碼。這使得程序員可以更為容易的理解程序的運行過程。讓程序員腦海裡可以實時地做一個源代碼到最終控制流程的映射。基於這個思想,C 語言一直沒有增加對結構進行運算的操作符(而 C++ 中把類或結構模擬成原生類型的做法相當普遍)。甚至於 inline 關鍵字也遲遲沒有被標准化(inline 出現在 C99 標准中,而這個最新的 C 語言標准並沒有被廣泛接受),正是因為它某種程度破壞了這一點。
C 語言在堅持以上幾點理念時,並非突出某個方面(比如追求性能),而是同時兼顧的。
C 語言並不是這個世界上唯一的編程語言,可惜的是,不是所有程序員都認識到了這點。對於把 C 語言作為自己唯一開發語言的程序員來說,很有必要開拓自己的眼界,這樣反過來才能更為清晰的理解 C 語言的內在精神。並不是說,某某語言本身是用 C 語言來實現,那麼 C 語言就可以以同樣的方式,解決那種語言解決的問題(甚至更為高效)。一些 C 語言中的概念,到了另一種語言中,很可能用完全不同的方式展現出來。正如自然語言會影響人的思維方式一樣,編程語言一樣會影響人對某種算法的編碼形式。在 C 裡,我們總以為某些寫法是自然而然的,但換了種語言卻很可能並不盡然。
無論如何 C 語言的語法和設計影響了許多其它語言。最為徹底的是 C++ 。以及大多數程序員都能叫的出名字的一些流行語言:Java , PHP ,Javascript,Perl ,C#,D,Objective-C 等等。 這些給人造成一種錯覺,新的語言取代了舊的,對老的語言做了改良和完善。最廣泛傳播的觀點是,C++ 是 C 的一個超集,它能做所有 C 能做的所有事情,且能做的更好。持有這種觀點的 C++ 程序員們甚至向把已有的各種 C 代碼用 C++ 重新實現。但實際上,C 和 C++ 更應該被看成是相互平等的存在。C++ 更像是一種借用了幾乎全部 C 語法(但還是有細微差異)的全新語言。它們在很多方面都有設計理念上的差異。C++ 企圖完全兼容 C 的語法卻不想完全繼承 C 語言的理念,這使它背負了巨大的包袱。而 C 的另一個繼任者:Objective-C ,拋棄了一些東西,則顯得清爽一些。
回顧 C++ 出現的時代背景在於把面向對象當成解決復雜問題的“銀彈”的年代。這使得 C++ 在發明之初,迅速的占領了大量原本是 C 語言的市場,甚至被看成是 C 語言的替代品。但 C++ 的擁趸們並沒有等到這一天。歷史證明,面向對象也不是“銀彈”、最近十年,C++ 的粉絲們從 C++ 語言的犄角旮旯裡挖掘出來的各種武器,讓 C++ 語言變成了包含多種編程范式的巨無霸。卻並沒有讓解決問題變得更容易。這並不完全是語言的問題,可能有很大程度上是面向對象等開發方法本身的問題。這也證明了 C 語言保持自身的簡潔正是其生機昂然的源泉。
和浩如煙海的 C++ 書籍相比較。如果你已經是程序員,但還不了解 C 語言的話。學習 C 語言,只需要讀一本書,而這本書沒有第二選擇,就是經典的《The C Programming Language》(K&R)。薄薄的一本就講透了語言的方方面面。可惜的是,C 語言過於注重對機器模型的抽象,並不適合用來程序員入門。尤其是在國內的教材市場,充斥著大量糟糕的 C 語言教材。在這些拙劣的教材中,甚至把開發工具(比如特定的 C 語言開發集成環境)和特定的硬件環境(甚至是過時的 8086 內存模型)與語言教學混為一談。
對於 C 語言不是母語的程序員來說,有充分的理由去學習一下 C 語言。那是低投入,高產出的。它會使你學會在硬件層次上思考問題(這或許對你是一個新的思維角度)。而且 C 語言已經非常穩定,不會再有(它本身也不希望有)大的變化,不用擔心學到的知識會過時。C 語言在 1990 年制訂出一個現在通行的標准( C90 )以來,在 C 的主流開發社區中幾乎沒有變過了。雖然,從 1999 年開始,C 語言委員會幾經修訂 C 語言的新標准( C99 ),但似乎並不被廣泛接受。雖然有很大程度上,這是源於世界上最大的 C/C++ 商業編譯器提供商微軟對其不感興趣。可在開源界,即使有 GNU C 對 C 語言新標准的不斷推動,那些實際用 C 語言做開發的大佬們還是紛紛表示,新的標准還不是很成熟。新的特性也不是特別有必要。
筆者用 C99 開發有一些年頭,但也只使用了其中一個子集,不太敢在正式項目中完全推廣。至於 C 語言近年來的發展,我個人比較欣賞蘋果公司對 C 語言添加的 blocks 擴展以用來實現 closure 。但並不看好這些新特性會迅速融入 C 語言社區。
C 語言從語言角度上講,最大缺陷在於要求程序員自己去做內存管理。用 C 語言去處理復雜的數據結構,程序員大部分的時間都花在了這上面,並且滋生了無數 bug 。調試 C 程序變成了一項獨立於編寫 C 程序的技能。防止緩沖區溢出、防止數據讀寫越界、正確的動態回收內存、避免懸空指針,這些在大部分語言看起來不可思議的關注點,在 C 語言程序員眼裡變得稀松平常。甚至是衡量 C 程序員技能經驗水平的重要標志。可要知道,這些和具體問題的解決過程無關。
也有人試圖在 C 語言層面解決這個問題,例如以庫形式提供垃圾回收的機制(筆者也曾做過類似嘗試)。但 C 語言本身的設計使它無法成為一個完美的解決方案。同樣的問題也存在於 C++ 。現在看來,不對語言做大的改造,很難回避。可改造本身又違背了 C 語言一貫的哲學。C 語言的發明人之一的 Ken Thompson 近年來參與了新的 Go 語言的設計和實現,可以看成從另一角度對新的程序開發語言的嘗試,可那已經不是 C 。
這個問題在一定程度上也促使了 java 的誕生。Java 采用了虛擬機和字節碼的方式改造了底層的機器模型。並在底層模型的基礎上加入了垃圾回收機制。並在語言層面取消了指針。在 C 語言的原生地,也有更多的動態(腳本)語言出現。先是有 awk 這樣的簡易語言,後有 perl ,再是 python 等的流行。在 Unix 風格下,程序員傾向於為特定領域設計特定的語言。C 和 Unix 的設計哲學是一體的。它們都鼓勵清晰的模塊化設計。讓模塊之間獨立,再用薄的膠合層聯系起來。腳本語言在現代類 Unix 系統上大量出現,並充當這種粘合工作就是一種發展必然。而原本的充當粘合部分的腳本語言,也逐步發展起來,遠遠超出腳本的用途范疇。做為程序員,尤其是 C 程序員,必須對它們有所了解並掌握其中的一些,才能適應現代的挑戰。
我們不應該指望一門語言解決所有的問題。可至於 C 語言本身,它將在很長的一段時間,帶著它的優雅和缺陷,繼續扮演它在計算機世界中重要的角色。