知识分享:值得学习的C语言经典开源项目
點(diǎn)擊上方藍(lán)字關(guān)注我,了解節(jié)氣咨詢
聽上去有些荒謬,C語言的產(chǎn)生竟然源于一個(gè)失敗的項(xiàng)目。1969年,通用電氣、麻省理工學(xué)院和貝爾實(shí)驗(yàn)室聯(lián)合創(chuàng)立了一個(gè)龐大的項(xiàng)目——Multics工程。該項(xiàng)目的目的是創(chuàng)建一個(gè)操作系統(tǒng),但顯然遇到了麻煩:它不但無法交付原先所承諾的快速而便捷的在線系統(tǒng),甚至連一點(diǎn)有用的東西都沒有弄出來。雖然開發(fā)小組最終勉強(qiáng)讓Multics開動(dòng)起來,但他們還是陷入了泥淖,就像IBM在OS/360上面一樣。他們?cè)噲D建立一個(gè)非常巨大的操作系統(tǒng),能夠應(yīng)用于規(guī)模很小的硬件系統(tǒng)中。Multics成了總結(jié)工程教訓(xùn)的寶庫,但它同時(shí)也為C語言體現(xiàn)“小即是美”鋪平了道路。
當(dāng)心灰意冷的貝爾實(shí)驗(yàn)室的專家們撤離Multics工程后,他們又去尋找其他任務(wù)。其中一位名叫Ken Thompson的研究人員對(duì)另一個(gè)操作系統(tǒng)很感興趣,他為此好幾次向貝爾管理層提議,但均遭否決。在等待官方批準(zhǔn)時(shí),Thompson和他的同事Dennis Ritchie自娛自樂,把Thompson的“太空旅行”軟件移植到不太常用的PDP-7系統(tǒng)上。太空旅行軟件模擬太陽系的主要星體,把它們顯示在圖形屏幕上,并創(chuàng)建了一架航天飛機(jī),它能夠飛行并降落到各個(gè)行星上。與此同時(shí),Thompson加緊工作,為PDP-7編寫了一個(gè)簡(jiǎn)易的新型操作系統(tǒng)。它比Multics簡(jiǎn)單得多,也輕便得多。整個(gè)系統(tǒng)都是用匯編語言編寫的。Brian Kernighan在1970年給它取名為UNIX,自嘲地總結(jié)了從Multics中獲得的那些不應(yīng)該做的教訓(xùn)。圖1-1描述了早期C、UNIX和相關(guān)硬件系統(tǒng)的關(guān)系。
是先有C語言還是先有UNIX呢?說起這個(gè)問題,人們很容易陷入先有雞還是先有蛋的套套中。確切地說,UNIX比C語言出現(xiàn)得早(這也是為什么UNIX的系統(tǒng)時(shí)間是從1970年1月1日起按秒計(jì)算的,它就是那時(shí)候產(chǎn)生的啊)。然而,我們這里討論的不是家禽趣聞,而是編程故事。用匯編語言編寫UNIX顯得很笨拙,在編制數(shù)據(jù)結(jié)構(gòu)時(shí)浪費(fèi)了大量的時(shí)間,而且系統(tǒng)難以調(diào)試,理解起來也很困難。Thompson想利用高級(jí)語言的一些優(yōu)點(diǎn),但又不想像PL/I[1]那樣效率低下,也不想碰見在Multics中曾遇到過的復(fù)雜問題。在用Fortran進(jìn)行了一番簡(jiǎn)短而又不成功的嘗試之后,Thompson創(chuàng)建了B語言,他把用于研究的語言BCPL[2]作了簡(jiǎn)化,使B的解釋器能常駐于PDP-7只有8KB大小的內(nèi)存中。B語言從來不曾真正成功過,因?yàn)橛布到y(tǒng)的內(nèi)存限制,它只允許放置解釋器,而不是編譯器,由此產(chǎn)生的低效阻礙了使用B語言進(jìn)行UNIX自身的系統(tǒng)編程。
編譯器設(shè)計(jì)者的金科玉律:效率(幾乎)就是一切
在編譯器中,效率幾乎就是一切。當(dāng)然還有一些其他需要關(guān)心的東西,如有意義的錯(cuò)誤信息、良好的文檔和產(chǎn)品支持。但與用戶需要的速度相比,這些因素就黯然失色了。編譯器的效率包括兩個(gè)方面:運(yùn)行效率(代碼的運(yùn)行速度)和編譯效率(產(chǎn)生可執(zhí)行代碼的速度)。除了一些開發(fā)和學(xué)習(xí)環(huán)境之外,運(yùn)行效率起決定性作用。
有很多編譯優(yōu)化措施會(huì)延長(zhǎng)編譯時(shí)間,但卻能縮短運(yùn)行時(shí)間。還有一些優(yōu)化措施(如清除無用代碼和忽略運(yùn)行時(shí)檢查等)既能縮短編譯時(shí)間,又能減少運(yùn)行時(shí)間,同時(shí)還能減少內(nèi)存的使用量。這些優(yōu)化措施的不利之處在于可能無法發(fā)現(xiàn)程序中無效的運(yùn)行結(jié)果。優(yōu)化措施本身在轉(zhuǎn)換代碼時(shí)是非常謹(jǐn)慎的,但如果程序員編寫了無效的代碼(如:越過數(shù)組邊界引用對(duì)象,因?yàn)樗麄儭爸馈备浇兴麄冃枰淖兞?#xff09;就可能引發(fā)錯(cuò)誤的結(jié)果。
這就是為什么說效率幾乎就是一切但也并不是絕對(duì)的道理。如果得到的結(jié)果是不正確的,那么效率再高又有什么意義呢?編譯器設(shè)計(jì)者通常會(huì)提供一些編譯器選項(xiàng)。這樣,每個(gè)程序員可以選擇自己想要的優(yōu)化措施。B語言不算成功,而Dennis Ritchie所創(chuàng)造的注重效率的“New B”卻獲得了成功,充分證明了編譯器設(shè)計(jì)者的這條金科玉律。
B語言通過省略一些特性(如嵌套過程和一些循環(huán)結(jié)構(gòu)),對(duì)BCPL語言作了簡(jiǎn)化,并發(fā)揚(yáng)了“引用數(shù)組元素相當(dāng)于對(duì)指針加上偏移量的引用”這個(gè)想法。B語言同時(shí)保持了BCPL語言無類型這個(gè)特點(diǎn),它僅有的操作數(shù)就是機(jī)器的字。Thomposon發(fā)明了++和--操作符,并把它加入到PDP-7的B編譯器中。它們?cè)贑語言中依然存在,很多人天真地以為這是由于PDP-11存在對(duì)應(yīng)的自動(dòng)增/減地址模型,這種想法是錯(cuò)誤的!自動(dòng)增/減機(jī)制的出現(xiàn)早于PDP-11硬件系統(tǒng)的出現(xiàn)。盡管在C語言中,拷貝字符串中的一個(gè)字符的語句:
*p++ = *s++;可以極其有效地被編譯為PDP-11代碼:
moveb (r0)+, (r1)+這使得許多人錯(cuò)誤地以為前者的語句形式是根據(jù)后者特意設(shè)計(jì)的。
當(dāng)1970年開發(fā)平臺(tái)轉(zhuǎn)移到PDP-11以后,無類型語言很快就顯得不合時(shí)宜了。這種處理器以硬件支持幾種不同長(zhǎng)度的數(shù)據(jù)類型為特色,而B語言無法表達(dá)不同的數(shù)據(jù)類型。效率也是一個(gè)問題,這也迫使Thompson在PDP-11上重新用匯編語言實(shí)現(xiàn)了UNIX。Dennis Ritchie利用PDP-11的強(qiáng)大性能,創(chuàng)立了能夠同時(shí)解決多種數(shù)據(jù)類型和效率的“New B”(這個(gè)名字很快變成了“C”)語言,它采用了編譯模式而不是解釋模式,并引入了類型系統(tǒng),每個(gè)變量在使用前必須先聲明。
C語言的早期體驗(yàn)
增加類型系統(tǒng)的主要目的是幫助編譯器設(shè)計(jì)者區(qū)分新型PDP-11機(jī)器所擁有的不同數(shù)據(jù)類型,如單精度浮點(diǎn)數(shù)、雙精度浮點(diǎn)數(shù)和字符等。這與其他一些語言如Pascal形成了鮮明的對(duì)比。在Pascal中,類型系統(tǒng)的目的是保護(hù)程序員,防止他們?cè)跀?shù)據(jù)上進(jìn)行無效的操作。由于設(shè)計(jì)哲學(xué)不同,C語言排斥強(qiáng)類型,它允許程序員需要時(shí)可以在不同類型的對(duì)象間賦值。類型系統(tǒng)的加入可以說是事后諸葛,從未在可用性方面進(jìn)行過認(rèn)真的評(píng)估和嚴(yán)格的測(cè)試。時(shí)至今日,許多C程序員仍然認(rèn)為“強(qiáng)類型”只不過是增加了敲擊鍵盤的無用功。
除了類型系統(tǒng)之外,C語言的許多其他特性是為了方便編譯器設(shè)計(jì)者而建立的(為什么不呢?開始幾年C語言的主要客戶就是那些編譯器設(shè)計(jì)者啊)。根據(jù)編譯器設(shè)計(jì)者的思路而發(fā)展形成的語言特性有:
數(shù)組下標(biāo)從0而不是1開始。絕大多數(shù)人習(xí)慣從1而不是0開始計(jì)數(shù)。編譯器設(shè)計(jì)者則選擇從0開始,因?yàn)槠屏康母拍钤谒麄冃闹幸咽歉畹俟獭5@種設(shè)計(jì)讓一般人感覺很別扭。盡管我們定義了一個(gè)數(shù)組a[100],你可千萬別往a[100]里存儲(chǔ)數(shù)據(jù),因?yàn)檫@個(gè)數(shù)組的合法范圍是從a[0]到a[99]。
C語言的基本數(shù)據(jù)類型直接與底層硬件相對(duì)應(yīng)。例如,不像Fortran,C語言中不存在內(nèi)置的復(fù)數(shù)類型。某種語言要素如果底層硬件沒有提供直接的支持,那么編譯器設(shè)計(jì)者就不會(huì)在它上面浪費(fèi)任何精力。C語言一開始并不支持浮點(diǎn)類型,直到硬件系統(tǒng)能夠直接支持浮點(diǎn)數(shù)之后才增加了對(duì)它的支持。
auto關(guān)鍵字顯然是擺設(shè)。這個(gè)關(guān)鍵字只對(duì)創(chuàng)建符號(hào)表入口的編譯器設(shè)計(jì)者有意義。它的意思是“在進(jìn)入程序塊時(shí)自動(dòng)進(jìn)行內(nèi)存分配”(與全局靜態(tài)分配或在堆上動(dòng)態(tài)分配相反)。其他程序員不必操心auto這個(gè)關(guān)鍵字,它是缺省的變量?jī)?nèi)存分配模式。
表達(dá)式中的數(shù)組名可以看作是指針。把數(shù)組當(dāng)作指針,簡(jiǎn)化了很多東西。我們不再需要一種復(fù)雜的機(jī)制區(qū)分它們,把它們傳遞到一個(gè)函數(shù)時(shí)不必忍受必須復(fù)制所有數(shù)組內(nèi)容的低效率。不過,數(shù)組和指針并不是在任何情況下都是等效的,更詳細(xì)的討論參見第4章。
float被自動(dòng)擴(kuò)展為double。盡管在ANSI C中情況不再如此,但最初浮點(diǎn)數(shù)常量的精度都是double型的,所有表達(dá)式中float變量總被自動(dòng)轉(zhuǎn)換成double。這樣做的理由從未公諸于眾,但它與PDP-11中浮點(diǎn)數(shù)的硬件表示方式有關(guān)。首先,在PDP-11或VAX中,從float轉(zhuǎn)換到double代價(jià)非常小,只要在后面增加一個(gè)每個(gè)位均為0的字即可。如果要轉(zhuǎn)換回來,去掉第二個(gè)字就可以了。其次,要知道在某些PDP-11的浮點(diǎn)數(shù)硬件表示形式中有一個(gè)運(yùn)算模式位(mode bit),你可以只進(jìn)行float的運(yùn)算,也可以只進(jìn)行double的運(yùn)算,但如果想在這兩種方式間進(jìn)行切換,就必須修改這個(gè)位來改變運(yùn)算模式。在早期的UNIX程序中,float用得不是太多,所以把運(yùn)算模式固定為double 是比較方便的,省得編譯器設(shè)計(jì)者去跟蹤它的變化。
不允許嵌套函數(shù)(函數(shù)內(nèi)部包含另一個(gè)函數(shù)的定義)。這簡(jiǎn)化了編譯器,并稍微提高了C程序的運(yùn)行時(shí)組織結(jié)構(gòu)。具體的機(jī)理在第6章“運(yùn)動(dòng)的詩章:運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)”中詳細(xì)描述。
register關(guān)鍵字。這個(gè)關(guān)鍵字能給編譯器設(shè)計(jì)者提供線索,就是程序中的哪些變量屬于熱門(經(jīng)常被使用),這樣就可以把它們存放到寄存器中。這個(gè)設(shè)計(jì)可以說是一個(gè)失誤,如果讓編譯器在使用各個(gè)變量時(shí)自動(dòng)處理寄存器的分配工作,顯然比一經(jīng)聲明就把這類變量在生命期內(nèi)始終保留在寄存器里要好。使用register關(guān)鍵字,簡(jiǎn)化了編譯器,卻把包袱丟給了程序員。
為了C編譯器設(shè)計(jì)者的方便而建立的其他語言特性還有很多。這本身不是一件壞事,它大大簡(jiǎn)化了C語言本身,而且通過回避一些復(fù)雜的語言要素(如Ada中的泛型和任務(wù),PL/I中的字符串處理,C++中的模板和多重繼承),C語言更容易學(xué)習(xí)和實(shí)現(xiàn),而且效率非常高。
和其他大多數(shù)語言不同,C語言有一個(gè)漫長(zhǎng)的進(jìn)化過程。在目前這個(gè)形式之前,它經(jīng)歷了許多中間狀態(tài)。它歷經(jīng)多年,從一個(gè)實(shí)用工具進(jìn)化為一種經(jīng)過大量試驗(yàn)和測(cè)試的語言。第一個(gè)C編譯器大約出現(xiàn)在1970年,距今20多年了[3]。時(shí)光荏苒,作為它的根基的UNIX系統(tǒng)得到了廣泛使用,C語言也隨之茁壯成長(zhǎng)。它對(duì)直接由硬件支持的底層操作的強(qiáng)調(diào),帶來了極高的效率和移植性,反過來也幫助UNIX獲得了巨大的成功。
K&R C
到了20世紀(jì)70年代中期,C語言已經(jīng)很接近目前這種我們所知道和喜愛的形式了。更多的改進(jìn)仍然存在,但大部分都只是一些細(xì)節(jié)的變化(比如允許函數(shù)返回結(jié)構(gòu)值)和一些對(duì)基本類型進(jìn)行擴(kuò)展以適應(yīng)新的硬件變化的改進(jìn)。(比如增加關(guān)鍵字unsigned和long)。1978年,Steve Johnson編寫了pcc這個(gè)可移植的C編譯器。它的源代碼對(duì)貝爾實(shí)驗(yàn)室之外開放,并被廣泛移植,形成了整整一代C編譯器的基礎(chǔ)。C語言的演化之路如圖1-2所示。
圖1-2 后期的C
軟件信條一個(gè)非比尋常的Bug
C語言從Algol-68中繼承了一個(gè)特性,就是復(fù)合賦值符。它允許對(duì)一個(gè)重復(fù)出現(xiàn)的操作數(shù)只寫一次而不是兩次,給代碼生成器一個(gè)提示,即操作數(shù)尋址也可以類似地緊湊。這方面的一個(gè)例子是用b+=3作為b=b+3的縮寫。復(fù)合賦值符最初的寫法是先寫賦值符,再寫操作符,就像:b=+3。在B語言的詞法分析器里有一個(gè)技巧,使實(shí)現(xiàn)=op這種形式要比實(shí)現(xiàn)目前所使用的op=形式更簡(jiǎn)單一些。但這種形式會(huì)引起混淆,它很容易把
b=-3; /* 從b中減去3 */
和
b= -3; /* 把-3賦給b */
搞混淆。
因此,這個(gè)特性被修改為目前所使用的這種形式。作為修改的一部分,代碼格式器程序indent也作了相應(yīng)修改,用于確定復(fù)合賦值符的過時(shí)形式,并交換兩者的位置,把它轉(zhuǎn)換為對(duì)應(yīng)的標(biāo)準(zhǔn)形式。這是個(gè)非常糟糕的決定,任何格式器都不應(yīng)該修改程序中除空白之外的任何東西。令人不快的是,這種做法會(huì)引入一個(gè)Bug,就是幾乎任何東西(只要不是變量),如果它出現(xiàn)在賦值符后面,就會(huì)與賦值符交換位置。
如果你運(yùn)氣好,這個(gè)Bug可能會(huì)引起語法錯(cuò)誤,如:
epsilon=.0001;
會(huì)被交換成:
epsilon.=0001;
這條語句將無法通過編譯器,你馬上就能發(fā)現(xiàn)錯(cuò)誤。但一條源語句也可能是這樣的:
valve=!open; /*valve被設(shè)置為open的邏輯反*/
會(huì)悄無聲息地交換成:
valve!=open; /*valve與open進(jìn)行不相等比較*/
這條語句同樣能夠通過編譯,但它的作用與源語句明顯不同,它并不改變valve的值。
在后面這種情況下,這個(gè)Bug會(huì)潛伏下來,并不會(huì)被馬上檢測(cè)到。在賦值后面加個(gè)空格是很自然的事,所以隨著復(fù)合賦值符的過時(shí)形式越來越罕見,人們也逐漸忘記了indent程序曾經(jīng)被用于“改進(jìn)”這種過時(shí)的形式。這個(gè)由indent程序引起的 Bug直到20世紀(jì)80年代中期才在各種C編譯器中銷聲匿跡。這是一個(gè)應(yīng)被堅(jiān)決摒棄的東西!
1978年,C語言經(jīng)典名著The C Programming Language出版了。這本書受到了廣泛的贊譽(yù),其作者Brian Kernighan和Dennis Ritchie也因此名聲大噪,所以這個(gè)版本的C語言就被稱為“K&R C”。出版商最初估計(jì)這本書將售出1000冊(cè)左右。截止到1994年,這本書大約售出了150萬冊(cè)(參見圖1-3)。C語言成為最近20年最成功的編程語言之一,可能就是最成功的。但隨著C語言的廣泛流行,許多人試圖從C語言中產(chǎn)生其他變種。
圖1-3 像貓王艾爾維斯一樣,C語言無處不在
UNIX系統(tǒng)
由于C語言因UNIX系統(tǒng)而生,也因此而流行,所以我們從UNIX系統(tǒng)開始(注意:我們提到的UNIX還包含其他系統(tǒng),如FreeBSD,它是UNIX的一個(gè)分支,但是由于法律原因不使用該名稱)。
1.在UNIX系統(tǒng)上編輯
UNIX C沒有自己的編輯器,但是可以使用通用的UNIX編輯器,如emacs、jove、vi或X Window System文本編輯器。
作為程序員,要負(fù)責(zé)輸入正確的程序和為儲(chǔ)存該程序的文件起一個(gè)合適的文件名。如前所述,文件名應(yīng)該以.c結(jié)尾。注意,UNIX區(qū)分大小寫。因此,budget.c、BUDGET.c和Budget.c是3個(gè)不同但都有效的C源文件名。但是BUDGET.C是無效文件名,因?yàn)樵撁Q的擴(kuò)展名使用了大寫C而不是小寫c。
假設(shè)我們?cè)趘i編譯器中編寫了下面的程序,并將其儲(chǔ)存在inform.c文件中:
#include <stdio.h> int main(void) {printf("A .c is used to end a C program filename.\n");return 0; }以上文本就是源代碼,inform.c是源文件。注意,源文件是整個(gè)編譯過程的開始,不是結(jié)束。
2.在UNIX系統(tǒng)上編譯
雖然在我們看來,程序完美無缺,但是對(duì)計(jì)算機(jī)而言,這是一堆亂碼。計(jì)算機(jī)不明白#include和printf是什么(也許你現(xiàn)在也不明白,但是學(xué)到后面就會(huì)明白,而計(jì)算機(jī)卻不會(huì))。如前所述,我們需要編譯器將我們編寫的代碼(源代碼)翻譯成計(jì)算機(jī)能看懂的代碼(機(jī)器代碼)。最后生成的可執(zhí)行文件中包含計(jì)算機(jī)要完成任務(wù)所需的所有機(jī)器代碼。
以前,UNIX C編譯器要調(diào)用語言定義的cc命令。但是,它沒有跟上標(biāo)準(zhǔn)發(fā)展的腳步,已經(jīng)退出了歷史舞臺(tái)。但是,UNIX系統(tǒng)提供的C編譯器通常來自一些其他源,然后以cc命令作為編譯器的別名。因此,雖然在不同的系統(tǒng)中會(huì)調(diào)用不同的編譯器,但用戶仍可以繼續(xù)使用相同的命令。
編譯inform.c,要輸入以下命令:
cc inform.c幾秒鐘后,會(huì)返回UNIX的提示,告訴用戶任務(wù)已完成。如果程序編寫錯(cuò)誤,你可能會(huì)看到警告或錯(cuò)誤消息,但我們先假設(shè)編寫的程序完全正確(如果編譯器報(bào)告void的錯(cuò)誤,說明你的系統(tǒng)未更新成ANSI C編譯器,只需刪除void即可)。如果使用ls命令列出文件,會(huì)發(fā)現(xiàn)有一個(gè)a.out文件(見圖1.5)。該文件是包含已翻譯(或已編譯)程序的可執(zhí)行文件。要運(yùn)行該文件,只需輸入:
a.out輸出內(nèi)容如下:
A .c is used to end a C program filename.圖1.5 用UNIX準(zhǔn)備C程序
如果要儲(chǔ)存可執(zhí)行文件(a.out),應(yīng)該把它重命名。否則,該文件會(huì)被下一次編譯程序時(shí)生成的新a.out文件替換。
如何處理目標(biāo)代碼?C編譯器會(huì)創(chuàng)建一個(gè)與源代碼基本名相同的目標(biāo)代碼文件,但是其擴(kuò)展名是.o。在該例中,目標(biāo)代碼文件是inform.o。然而,卻找不到這個(gè)文件,因?yàn)橐坏╂溄悠魃闪送暾目蓤?zhí)行程序,就會(huì)將其刪除。如果原始程序有多個(gè)源代碼文件,則保留目標(biāo)代碼文件。學(xué)到后面多文件程序時(shí),你會(huì)明白到這樣做的好處。
Linux系統(tǒng)
Linux是一個(gè)開源、流行、類似于UNIX的操作系統(tǒng),可在不同平臺(tái)(包括PC和Mac)上運(yùn)行。在Linux中準(zhǔn)備C程序與在UNIX系統(tǒng)中幾乎一樣,不同的是要使用GNU提供的GCC公共域C編譯器。編譯命令類似于:
gcc inform.c注意,在安裝Linux時(shí),可選擇是否安裝GCC。如果之前沒有安裝GCC,則必須安裝。通常,安裝過程會(huì)將cc作為gcc的別名,因此可以在命令行中使用cc來代替gcc。
END
*聲明:本文于網(wǎng)絡(luò)整理,版權(quán)歸原作者所有,如來源信息有誤或侵犯權(quán)益,請(qǐng)聯(lián)系我們刪除或授權(quán)事宜。
戳“閱讀原文”我們一起進(jìn)步
總結(jié)
以上是生活随笔為你收集整理的知识分享:值得学习的C语言经典开源项目的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓赚钱(手机赚钱安卓)
- 下一篇: 进口化妆品备案(进口护肤品备案)