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