1. 程式人生 > >C語言的精髓是指標

C語言的精髓是指標

作者:invalid s
連結:https://www.zhihu.com/question/20125963/answer/104060886
來源:知乎
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。
 

no pains, no gains

 

對C來說,指標、無越界檢查等等是一切痛苦的根源;但這些痛苦並不是白白付出的。

可以和彙編比效率(甚至可以做到“編譯器自動優化的程式碼比80%彙編高手手工優化的彙編程式碼都好”),就是這些付出所應得的收穫。

 

事實上,任何一門設計合理的語言,給你的限制或提供的什麼特性,都不是沒有好處/代價的。
準備在哪方面付出、想要得到什麼,這就是選擇語言的依據,也是為何會有這麼多種語言的原因。

——————————————————————

具體的說指標有什麼好處……這很難。要麼掛一漏萬,要麼……其實別的語言也有類似特性,並非C所獨有,至多是……有些限制或者稍微多繞了幾步而已。

真要說清楚這個並不容易。或許,其實你應該問的是:“C究竟有什麼優勢?指標在其中起了什麼作用”?或者,C這個老掉牙的奇葩究竟有什麼獨門祕技、奇特思想,以至於它現在還能牢牢佔據程式語言排行榜的首位?

 

那麼,這裡就籠統的、從理論層面答非所問的胡扯幾句。

——————————————————————

從哪裡開始呢?先說思想吧。

 

軟體開發/設計行業有這麼一句話:沒有什麼是不能通過增加一個抽象層解決的。

這句話很對……但抽象層並不是免費的。這點就很少有人想過了:一旦你和什麼東西之間被加上了一個抽象層,那你就一定得在每次訪問它時受到某種限制、或者付出某些代價

 

換句話說,一旦和某個實體之間有了抽象層:
1、你必須間接訪問該實體(如果實現的很好,有時候的確能夠無需付出效能代價;但並不能保證任何時候都無代價)
2、你必須以抽象者所期望的方式訪問該實體:即便你知道該實體其實是什麼、在處理某些問題時用不著七拐八繞,你也得七拐八繞著訪問它。即:封裝有時候反而會增加複雜度。

越是底層,抽象就越難做。因為,其一,稀奇古怪的需求實在太多,總有你想不到的地方;其二,如果你抽象了,那麼就必須保證任何情況下,這個抽象都得真的像它所定義的那樣工作;而這個往往意味著很多方面的代價。

 

後一句話可能有些難以理解。我來舉個例子。
比如說,陣列就是對“一系列連續記憶體單元”的抽象;它對外表現為“一個固定大小的容器”。

現在問題來了:如果有人訪問第(陣列大小+1)個元素,那麼你就必須阻止他。否則,這個陣列就不像容器了——用術語說,就是你沒有封裝好它,導致細節暴露出來了。

於是,每次有人訪問陣列,你都得先檢查待訪問元素的下標是否越界——這就導致每次訪問,你都必須付出幾倍的時間代價。

 

而對C來說,陣列就是一個指向一片記憶體區域的指標……它並不去封裝這個概念;恰恰相反,它鼓勵你去了解藏在表象背後的東西。

於是乎,舉例來說,在大量文字中搜索匹配某個模式的字串(即strstr函式),如果C用3秒能搜完,其它語言再快可能也得9秒。因為每和一個字元比較,其它語言都要多兩次索引越界與否的檢查動作。

當然,這個好處並不是白撿到的。C語言使用者因此而付出的代價,就是防不勝防的緩衝區溢位問題……

 

再看一個例子。

假設我們實現底層網路包的識別/分析工作(就好像wireshark那樣),我們需要:
1、分析包的來源、去向、型別、控制資料等資訊
2、分析TCP/UDP包的內部資訊可能是哪個已知協議(http、https、msn、ssh等等等等)、並輸出分析結果(如果無法識別,以16進位制數字顯示)
3、可以很容易的擴充支援的協議(比如加入QQ、WOW之類協議的支援)

如果你用java……尤其是隻知道設計模式的那些人,想象下這種程式設計起來得有多麻煩、處理起來效率得多低吧。

 

但如果用C,這個工作是意想不到的簡單清爽……
1、按照IP報頭規範,讀取正確偏移位置的幾個位元組,識別出包的來源地址和目標地址
2、根據報頭某個位置的標記,識別這是一個TCP包還是一個UDP包
3、以便於閱讀的方式,輸出TCP/UDP/IP頭攜帶的資訊
4、拆分出載荷,把載荷首地址、長度等資訊丟給一邊蹲著的協議分析器鏈
6、第一個協議分析器按照已知的網路封包協議定義,識別載荷是不是自己能對付的那種協議
7、如果是,分析之,並輸出分析結果;否則,丟給下一個協議分析器處理
8、如果所有協議分析器都無法識別,則該包可能是私有格式,按預設格式顯示

整個流程甚至可以直接在指標指向的那片記憶體上進行,無需任何複製動作——直接就是真正的0 copy。

其中,協議分析器是一個函式指標,該函式接受三個引數:指向待分析資料頭部的指標、待分析資料長度、返回分析結果的資料結構指標;返回值為一個bool值:true表示包已識別,不需要繼續在協議分析器鏈上傳遞了;false表示無法識別,繼續傳遞給下一個協議分析器。

至於在協議分析器內部,你只需:檢查長度是否足夠;把傳來的指標強制型別轉換成自己支援的資料結構(如 struct msnHead之類);檢查資料結構中各項的值是否正確;如果正確,按標準格式輸出到分析結果。

而新增一個協議分析器,只需如此定義一個函式,然後呼叫register介面,把它掛接在分析器鏈末尾即可——無需考慮構造/析構時機、無需考慮記憶體分配與回收、無需什麼類工廠、反省等等等等。

 

有的時候,資料就是資料、函式就是函式。你封裝成類,反而棘手多了。

尤其是這類偏底層、偏資料和演算法方面的應用,和高層的UI開發不同,類經常是個累贅。當別人還在為類體系如何設計、如何利用好反省機制煩惱時,你憊懶的一個msnHead * pHead = (msnHead *) pData,事情已經完美解決了——用C,就是這麼任性。

 

C並不僅僅把資料當作資料,記憶體當作記憶體;它甚至允許你把硬體看成硬體——赤裸裸的插在總線上的、未加封裝的硬體。

你完全可以取區域性變數的地址、然後順藤摸瓜,把整個棧空間打印出來。
當你玩無鎖程式設計、遭遇ABBA問題時,它允許你把指標看成資料,然後給它附加一個TAG,通過檢查TAG來規避ABBA問題;完了需要索引資料時,重新去掉TAG,剛剛還在隨意揉捏的資料就又變成了指向合法位置的指標。
在古老的DOS時代,你可以直接根據中斷向量表的起始地址、中斷向量號直接計算指向該中斷程式的指標所在的地址——然後,或者跟過去把作業系統的祕密全部打印出來、或者用自己寫的函式替換掉系統中斷服務。
或者,你可以直接向硬碟所在的IO口寫命令字,控制整合在硬碟電路板上的微控制器系統執行任務。
你也可以直接訪問視訊記憶體,然後手動指揮顯示卡顯示哪個頁面。和訪問普通記憶體沒什麼兩樣。

只要是硬體允許你做的,你都可以做。

 

所以,C的好處就是:沒有多餘的抽象/封裝;一切以硬體介面為準,什麼東西是什麼,它就是什麼。你可以在其上無限的發揮想象力——哪怕搞個自己的類體系也不是是小菜一碟——沒有任何限制,沒有任何思維負擔。

 

所有這些,都是圍繞著指標實現的。

當然,這樣也不是沒有代價的:對java來說,一個物件是什麼,它就是什麼;一個類說我保證什麼、你不能碰什麼,你就只能照做。這是語言提供的保證,所以你很難做錯事。這就是封裝的好處。

但對C來說,你的所有要求都可能被人“憊懶”的忽略掉,除非你壓根不給他碰你的資料;別人給你一堆資料,這些資料也很可能是通過某種方式“憊懶”來的,你最好不要隨意動它;作業系統原始碼裡,看起來平平常常幾行程式碼,很可能訪問的是了不得的區域;有時候,除非閱讀原始碼、遵循各種編碼規範並且祈禱別人也遵循它們,你得不到任何保證——得到“無比犀利、無比直接的解決某些問題”的能力同時,你可能也得到了無比犀利、無比奇葩的BUG……

要用好C,你必須能夠看透資料的本質、必須能看透別人程式碼的意圖(並不會有編譯器幫助你、告訴你什麼不能碰)、必須知道自己寫下的每一行程式碼意味著什麼並自己為它所可能造成的任何side effect負責(所以對新手來說,單步執行並觀察每行程式碼造成的所有影響,是入門所必不可少的一步):如果做不到,你就會變成團隊裡的麻煩製造者——在這些要求面前,精通指標只能算剛剛入門罷了。

C是工程師為自己設計的語言。它是為那些對機器瞭如指掌的專家設計的。
C的設計者並不認為需要對工程師做任何約束,因為他們知道自己在幹什麼。
C並不是學院派語言。它並不打算為了貫徹什麼什麼理論、什麼什麼概念而設定什麼條條框框——所以,它不會像pascal、java一樣,為了純粹的結構化/面向物件而排斥其他。相反,結構化是好的,它就拿來用;彙編操縱記憶體/硬體方便,那就不能丟;至於新興的面向物件……記憶體就擺在你面前;C的老基友,unix/linux系,設計之初就貫徹的泛檔案思想,就是面向物件的啟蒙者之一:你居然還敢說C做不到?它只是沒有直接喂到你嘴裡而已!

這就導致,在偏基礎的應用領域,C是當仁不讓的不二之選;甚至,對能夠很好駕馭C的人來說,哪怕是圖形介面之類看似不適合C發揮的領域,使用C也不需要支付什麼額外代價:你說什麼什麼高階機制?不就記憶體裡那麼點事嘛,隨手擼一個GTK給你看看——嗯,除了不太容易找到可靠的人來接手/維護、以及開發商業軟體僱人比較貴且困難外,還真沒多大麻煩(這已經夠麻煩了好嘛)。

它只管提供最犀利的武器,你來負責用對、用好、用精它們。

 

但,這也導致C對使用者的要求居高不下。
它是很強大,但太難駕馭。以致於它已經很難適應目前開發商業軟體的諸多要求了。
尤其是那些對速度並不非常敏感、相對更在乎開發成本(包括人力、時間、金錢等方面的成本)的領域,C的確太麻煩了——從僱人到開發再到維護,一條龍的麻煩。

但C的設計目標並不是佔領一切領域。自始至終,它都不過是一種最為貼近機器、因而操控起來最為方便的高階語言罷了。就好像python的目標只是方便好用的強力粘合劑一樣。
c是unix/linux的一等公民;但unix/linux並不排斥別的語言。恰恰相反,unix/linux系支援的語言種類才是最多最雜的,而且C可以極為輕易的和任何一種語言協作:一句話,不就是記憶體裡那點事嗎。

這一堆堆語言,哪裡合適,你就用;不合適,換別的:就這麼簡單。C的哲學就是看開一切,這點事你都看不開嗎?

 

沒有什麼語言是萬能的——除了C++大概能算半拉“萬能語言”(但是,有了萬能的語言,卻不存在萬能的人,也沒有什麼需要萬能語言的專案。所以近年業界對C++的共識是:它可以當C用、當C with Class用、當java用、當黑魔法般寫泛型庫用……但無論如何,別拿它當C++用。嗯,扯遠了)——因為現實中沒有免費的午餐。想得到什麼,就必然要相應的失去點什麼。

 

編寫一個C程式所需要付出的代價,常常是非常昂貴的——無論對開發者的要求、還是寫出良好程式碼需要付出的努力等等方面,它都是代價高昂的;甚至可能是除了彙編外最貴的(當然,這裡暫不考慮濫用全部特性/正規化的C++)。

付出了這麼多……你買到的其實只有一樣東西,那就是指標,以及藉助指標而對你徹底開放的……其它語言想盡辦法不讓你看到的……“可怕”的底層細節。
和其他熱衷於“封裝細節”的語言不同,它鼓勵你這麼做。
它的哲學和unix一樣:做一件事,把一件事做好——專一而精,不大包大攬,這不正是天然的封裝嗎?

換句話說,在C的觀念裡,指標不是什麼精髓,它只是一扇門,推開門,後面是整個世界

這是C最獨特的優勢,也是這門誕生於上世紀七十年代早期的、老掉牙的語言,至今仍能雄霸最流行語言排行榜之首的……幕後“黑手”。