?
正文
以下為譯文:
雖然 C 語(yǔ)言并不是我所學(xué)的第一門語(yǔ)言,也不是我的最后一門語(yǔ)言,但是我仍然非常喜歡 C,當(dāng)需要寫程序時(shí),我的第一選擇還是 C。同時(shí),我也會(huì)關(guān)注現(xiàn)代編程語(yǔ)言及其發(fā)展趨勢(shì),而且我還使用 Rust 編寫了自己的業(yè)務(wù)愛(ài)好項(xiàng)目。那么,為什么我沒(méi)有拋棄 C 而選擇其他語(yǔ)言呢?我對(duì)于 C++的看法又是如何的呢?
1、為什么說(shuō)C不是最好的語(yǔ)言?
首先,這個(gè)世上沒(méi)有最好的編程語(yǔ)言。每種語(yǔ)言都有獨(dú)特的優(yōu)勢(shì)以及適用情況,所以盡管你可以在 Excel 中編寫光線追蹤程序,但最好還是使用其他語(yǔ)言。因此,我們都需要了解編程語(yǔ)言的限制,不要抱怨 Web 服務(wù)器不是用 Fortran 編寫的,也不要抱怨基本沒(méi)有任何應(yīng)用使用 Perl 或 C++作為內(nèi)部腳本語(yǔ)言。我認(rèn)為 C 語(yǔ)言不太理想的方面包括以下幾點(diǎn)(除了 C 比較老,發(fā)展不快之外,當(dāng)然還與個(gè)人的喜好有關(guān))。
其次,有些時(shí)候,C 的語(yǔ)言不夠明確。比如,*可以是二進(jìn)制乘法運(yùn)算符、一元解引用運(yùn)算符,也可用于聲明指針。
再者,有些情況不夠安全,例如越界訪問(wèn)數(shù)組這種極其常見(jiàn)的錯(cuò)誤都沒(méi)有運(yùn)行時(shí)檢查,這一點(diǎn)連 Borland Pascal 都比不了,更不用說(shuō)更現(xiàn)代的編程語(yǔ)言了(盡管你會(huì)為了提高性能關(guān)閉這個(gè)編譯選項(xiàng))。此外,指針讓我們很難保持一切井然有序。再加上一些其他情況,比如調(diào)用函數(shù)不需要事先聲明原型,這樣很容易將錯(cuò)誤類型的參數(shù)傳遞給函數(shù)。
最后,C 的標(biāo)準(zhǔn)庫(kù)非常有限。有些編程語(yǔ)言甚至擁有開(kāi)箱即用的 Web 服務(wù)器(或者至少有構(gòu)建 Web 服務(wù)器所需的所有模塊),但 C 標(biāo)準(zhǔn)庫(kù)甚至連 Web 服務(wù)器的容器也沒(méi)有。
2、為什么我還是喜歡C?
盡管如此,我還是十分喜歡 C,因?yàn)樗且环N簡(jiǎn)單的語(yǔ)言。從某種意義上說(shuō)很簡(jiǎn)單,很容易表達(dá)自己的想法以及期望。
舉個(gè)例子,假設(shè)兩個(gè)數(shù)組有兩個(gè)偏移量,其中一個(gè)可以為負(fù)數(shù),如果使用C語(yǔ)言編寫,則可以寫成:
?
arr[off1 + off2]
?
如果是Rust,則需要寫成:
?
arr[((off1 as isize) + off2) as usize]
?
通常,C 的循環(huán)也比 Rust 的迭代器組合更為簡(jiǎn)潔(當(dāng)然 Rust 也允許使用前一種方式,但 linter 并不滿意,它會(huì)建議你使用迭代器來(lái)代替)。類似地,memset()和 memmove()也是功能十分強(qiáng)大的工具。
在大多數(shù)情況下,你都可以預(yù)見(jiàn)到編譯的結(jié)果,即對(duì)象在內(nèi)存中的表示方式,以及如何通過(guò)不同的方式理解編譯后的結(jié)果(新版 C 標(biāo)準(zhǔn)中這一點(diǎn)變得更困難,這都要怪 C++,我稍后再詳細(xì)介紹)。另外,你也很清楚函數(shù)調(diào)用的結(jié)果等等。由于這個(gè)原因,C 被稱為可移植的匯編語(yǔ)言,所以我非常喜歡 C。
我們拿汽車做個(gè)類比,C 語(yǔ)言就像一輛跑車,擁有手動(dòng)變速箱,可以提供最佳性能,但是如果你不熟悉離合器和掛擋操作,那么變速箱很容易被損壞,甚至可能損壞發(fā)動(dòng)機(jī),當(dāng)然,油門踩得過(guò)大也有可能沖出馬路。然而,與自動(dòng)變速箱相比,這種車輛的發(fā)動(dòng)機(jī)能量更大,而且你可以預(yù)測(cè)性能,還可以炫車技,這些在其他車輛上都是不可能的。
3、這與C++有什么關(guān)系?
下面,我們來(lái)說(shuō)一說(shuō) C++,其實(shí)我不討厭 C++。我不能否認(rèn),與 C 相比, C++ 擁有兩個(gè)優(yōu)點(diǎn):
更好的程序結(jié)構(gòu):C++ 擁有命名空間和類,而且在某些方面Simula還是很出色的。
擁有 RAII 概念:一個(gè)簡(jiǎn)單的例子就是 C++ 擁有構(gòu)造函數(shù),可在創(chuàng)建對(duì)象時(shí)初始化對(duì)象;還擁有析構(gòu)函數(shù),在銷毀對(duì)象時(shí),做一些清理的工作。這個(gè)概念進(jìn)一步發(fā)展,就接近 Rust 的生命周期了。
另一方面,C++ 有兩個(gè)特征,我非常不喜歡。?
首先是這門語(yǔ)言的整體性質(zhì)。其他編程語(yǔ)言擁有的流行功能最終都會(huì)進(jìn)入 C++。因此,每過(guò)幾年,C++標(biāo)準(zhǔn)就會(huì)添加一些新功能。最終,這門語(yǔ)言就變得有點(diǎn)怪異,沒(méi)人能夠完全掌握,而且許多功能都是抄襲的其他語(yǔ)言?;旧厦總€(gè)人在編寫代碼的時(shí)候,都會(huì)選擇一個(gè) C++的子集,然后忽略其他功能的存在。另外,我們究竟應(yīng)該使用哪個(gè) C++版本的功能,并沒(méi)有一套標(biāo)準(zhǔn)的方法。Rust 在包的范圍內(nèi)提供了版本管理。據(jù)我所知,C++也曾嘗試過(guò)引入“代際”的概念來(lái)實(shí)現(xiàn)同樣的功能,但沒(méi)有成功。我經(jīng)常聽(tīng)到有人獨(dú)自編寫 C 編譯器,卻從來(lái)沒(méi)聽(tīng)說(shuō)過(guò)有人編寫 C++編譯器。
其次,實(shí)際上 C++不僅是多種語(yǔ)言,而且還是一種元語(yǔ)言(即模板)。我了解 C++的創(chuàng)建初衷,也同意它對(duì)于與類型無(wú)關(guān)的代碼的處理,比 C 預(yù)處理器更好。但實(shí)際上,它產(chǎn)生的代碼十分可怕,原本是“頭文件僅包含聲明,實(shí)現(xiàn)放在編譯好的代碼中”,變成了“頭文件包含所有項(xiàng)目會(huì)用到的代碼”。我不喜歡過(guò)于冗長(zhǎng)的編譯時(shí)間,但這種方式只能讓情況更糟。
最后,我覺(jué)得 C++的出現(xiàn)反而給 C 帶來(lái)了約束以及不良影響。我不是在討論 C/C++,也不是指 C 與 C++的共通之處,我討論的是耦合對(duì)標(biāo)準(zhǔn)和編譯器都有不良影響。一方面,C++建立在 C 之上,從而得到了極大的發(fā)展;另一方面,如果 C++中沒(méi)有 C 遺留下來(lái)的大多數(shù)功能的話,情況可能會(huì)更好(當(dāng)然,C++曾設(shè)法通過(guò)淘汰的方式逐步放棄某些 C 功能,但對(duì)于舊功能的支持仍然存在)。但是,C++ 24 能夠在 C++ 21 的基礎(chǔ)之上,發(fā)展成為一門獨(dú)立的編程語(yǔ)言嗎?大多數(shù)過(guò)時(shí)的功能都可以拋棄嗎?我對(duì)此表示懷疑。
4、C++編譯器對(duì)C的影響
?實(shí)際上,C 語(yǔ)言被當(dāng)成了沒(méi)有某些功能的 C++。比如微軟的 C 編譯器直到2015 版才開(kāi)始支持 C99 功能(即便如此,它還是以 bug 修復(fù) bug 的方式來(lái)支持兼容性,因?yàn)榭蛻艨赡軙?huì)震驚地發(fā)現(xiàn)可變參數(shù)宏居然可以運(yùn)行)。但是,無(wú)論是標(biāo)準(zhǔn)的編譯器還是其他編譯器中都可以看到相同的方法,這些都是相關(guān)的問(wèn)題。?
主要問(wèn)題在于,C 和 C++標(biāo)準(zhǔn)都是根據(jù)編譯器開(kāi)發(fā)人員的反饋而編寫的,而且大多數(shù)都是 C++開(kāi)發(fā)人員(有些人對(duì)現(xiàn)實(shí)世界編程一無(wú)所知,而且他們還認(rèn)為現(xiàn)實(shí)世界的做法與自己的觀點(diǎn)完全吻合,真是令人窒息的操作)。雖然我也沒(méi)有遵循標(biāo)準(zhǔn)的開(kāi)發(fā)程序,但是我很確定 C99 及其后版本中令人討厭的諸多功能皆來(lái)自那些編譯器開(kāi)發(fā)人員。他們只從 C++的角度出發(fā)考慮,而且還將這些功能強(qiáng)加給了 C,還美其名曰簡(jiǎn)化編譯器。
當(dāng)然我指的是“未定義的行為”以及編譯器的處理方式。這已成為一大毒瘤(只要你的代碼依賴于二進(jìn)制補(bǔ)碼算術(shù),就會(huì)被認(rèn)定具有未定義的行為,編譯器會(huì)拋棄整塊代碼)。
在我看來(lái),以下四種行為盡管不值得提倡,但前兩個(gè)也并非不可接受:
依賴于體系結(jié)構(gòu)的行為(即依賴于 CPU 體系結(jié)構(gòu)的行為)。包括絕大部分算術(shù)運(yùn)算。例如,如果我知道目標(biāo)及其使用了兩個(gè)協(xié)處理器,為什么編譯器會(huì)選擇另一種方式,僅僅是為了獲得理論上的優(yōu)化?同樣的問(wèn)題也適用于移位運(yùn)算。如果我知道 x86 會(huì)忽略移位偏移量的高比特,在 ARM 上負(fù)的左移相當(dāng)于右移,那么為什么不能專門針對(duì)該體系結(jié)構(gòu)編寫程序呢?畢竟,連整數(shù)的大小在不同平臺(tái)上都不一樣。這種不可移植性只需警告就好,讓用戶自行處理。
指針魔法和類型雙關(guān)。這似乎又是編譯器優(yōu)化帶來(lái)的限制。我同意,在重疊的內(nèi)存區(qū)域上使用 memcpy(),不同的實(shí)現(xiàn)可能會(huì)給出不同的行為(現(xiàn)代的 x86 實(shí)現(xiàn)會(huì)從區(qū)域尾部開(kāi)始復(fù)制),而且還依賴于地址的相對(duì)位置,但其他的規(guī)則就沒(méi)什么道理了。例如,無(wú)法使用兩個(gè)不同類型的指針同時(shí)操作同一塊內(nèi)存區(qū)域。我無(wú)法想象為什么這種行為被禁止,其原因只可能是編譯器優(yōu)化。這樣就不可能利用聯(lián)合體將整數(shù)轉(zhuǎn)換成浮點(diǎn)數(shù)。Linus 也曾吐槽過(guò)這一點(diǎn),我就不用重復(fù)了。但在我看來(lái),這樣做的目的或者是更好的編譯器優(yōu)化,或者是出于 C++的要求(由于類型跟蹤的要求)。
實(shí)現(xiàn)中定義的行為(即超出 C 標(biāo)準(zhǔn)規(guī)定的行為)。我常用的例子就是函數(shù)調(diào)用:根據(jù)調(diào)用的習(xí)慣約定和編譯器的實(shí)現(xiàn),函數(shù)的參數(shù)的求值順序可能完全是隨機(jī)的,因此 foo(*ptr++, *ptr++, *ptr++)的結(jié)果是未定義的,因此即使你知道目標(biāo)體系結(jié)構(gòu),也不應(yīng)該依賴于這種行為。
完全未定義的行為。最常見(jiàn)的例子就是在一條語(yǔ)句中改變變量狀態(tài),例如著名的 I++ + i++,或者更甚的 *ptr++ = *ptr++ +*ptr++。
由于 C++比 C 更高級(jí)(盡管它由許多來(lái)自 C 的特性,但都不建議使用,應(yīng)該使用 reinterpret_cast<>代替類型轉(zhuǎn)換,用引用代替指針,等等),所以不要期待 C++程序員能夠像 C 程序員那樣理解底層代碼。當(dāng)然,由于 C++程序員占絕大多數(shù),C/C++的耦合也極其常見(jiàn),所以 C 編譯器通常會(huì)進(jìn)行擴(kuò)展以支持C++,并使用 C++重寫,以適應(yīng)其復(fù)雜度。所以很不幸,你不得不使用 C++編譯器來(lái)編譯 C 編譯器(還好我們還有 LCC、PCC 和 TCC 等純 C 編譯器)。
5、總? ? 結(jié)?
總的來(lái)說(shuō),我喜歡C所處的中層位置,它既可以完成一些底層的實(shí)現(xiàn),例如輕松地操作內(nèi)存,同時(shí)又可以享受高級(jí)語(yǔ)言的好處。另一方面,我對(duì)C++強(qiáng)烈的不滿來(lái)自其在設(shè)計(jì)上的選擇,而且這些設(shè)計(jì)影響了C標(biāo)準(zhǔn)和編譯器。
至少我不可能用 C90 特別版取代 C90,并假裝原來(lái)的版本不存在。
審核編輯:湯梓紅
評(píng)論