C語言是在B語言的基礎上發展起來的。
C的根源是ALGOL 60,1960年ALGOL 60是一種面向問題的高級語言,離硬件比較遠,1963年的時候推出了CPL語言,CPL在ALGOL 60的基礎上更接近硬件一些,但很難實現,1967年,對CPL語言做出了簡化,推出了BCPL語言,1970年又對BCPL語言為基礎,又做出了進一步的簡化,設計出了很簡單的的而且接近硬件的BCPL語言簡稱B語言(BCPL的第1個字母),并且用了B語言編寫的第1個UNIX操作系統,在PDP 7上實現,此時的B語言過于簡單,功能有限,在1972-1973年間,D.M.Ritchie在B語言的基礎上設計出了C語言(BCPL的第2個字母),C語言保持了B語言的優點(精煉,接近硬件)又克服了缺點(過于簡單,數據無類型等)。它就經受住了時間的考驗,在許多情況下仍然是最流行的編程語言之一。
Basic Combined Programming Language(BCPL),1967年由劍橋大學的Matin Richards在同樣由劍橋大學開發的CPL語言上改進而來。BCPL最早被用做牛津大學的OS6操作系統上面的開發工具。后來通過美國貝爾實驗室的改進和推廣成為了UNIX上的常用開發語言。
BCPL有些類似于Fortran,也是典型的面向過程的高級語言。BCPL的語法更加靠近機器本身,適合于開發精巧,高要求的應用程序,同時對編譯器的要求也不高。BCPL也是最早使用庫函數封裝基本輸入輸出的語言之一,這使得他跨平臺的移植性很好。BCPL的代碼用小寫字母書寫,有別于同時代的BASIC和PASCAL。BCPL對于字符串的支持很差,內存管理也很糟糕。
BCPL本身并沒有被使用太長時間。1970年,貝爾實驗室的Ken Thompson在BCPL的基礎上改進出了B語言,用于書寫UNIX。這個名字取自BCPL中的第一個字母。B語言使用的時間更短,三年后的1973年同樣是貝爾實驗室的D.M.RITCHIE將B語言進一步改進,并且取了BCPL中的第二個字母將其命名為C語言。而C語言和C++則在日后成為了最流行的高級語言。
B語言之父和C語言之父是同事,UNIX的作者
1964年,美國麻省理工、貝爾實驗室、通用電氣準備為GE-645大型機開發一套多人多任務操作系統MULTICS。
參與研發的一位貝爾實驗室研究員肯·湯普森搞了一臺廢棄的DEC PDP-7計算機,PDP-7字長為18位,其標準主內存為4K字(相當于9千字節),可以升級到64K字(144 KB)。
DEC PDP-7
肯·湯普森伙同好友丹尼斯·里奇在上面研發了一個操作系統
丹尼斯·里奇
雖然這個操作系統比較簡陋,但公認是UNIX操作系統的雛形。已經顯示出Unix的一些基本特征——簡潔、高效、比當時所有的操作系統都更注重交互性、對程序員友好。具備一個簡陋的文件系統,有特殊的文件類型及支持目錄和設備,甚至可以支持多任務。它的核心是用匯編寫的(匯編器也是肯·湯普森自己寫的),不具備可移植性。只支持兩個用戶。
這個系統除了使用匯編語言之外,還是用了一種在BCPL語言基礎上由肯·湯普森發明的B語言。叫B語言,就是把BCPL精簡提煉的意思。
B語言不支持數據類型和結構,接近底層。后來丹尼斯·里奇在B語言的基礎上增加了數據類型和結構的支持,推出了C語言(意思是“BCPL”中排在B之后)。
肯·湯普森1970年借為貝爾實驗室專利部開發一套文字處理系統的機會,搞到了一臺PDP-11/20。他們把UNIX從PDP-7上移植了過來,匯編寫的代碼沒什么可移植性,所以基本上就是在PDP-11上重寫了一次,讓C語言有了大顯身手的用武之地,也是第一次使用高級語言開發操作系統。
貝爾實驗室成了Unix的第一個商業用戶,這是在1971年11月,在與系統配套的手冊中,該版本被稱做“First Edition”。
這么好用的東西在業界引起了極大反響,無論是UNIX還是C語言,成了當時計算機科學界研究的熱門。兩位大神也從來不敝帚自珍,不但利用貝爾實驗室無法限制UNIX版權大量郵寄這款操作系統給當時的同仁,還經常幫助他們解決安裝使用UNIX中遇到的問題。
同時,計算機科學家和工程師們也不斷對UNIX添磚加瓦,各路大神編寫了各種Unix版本和各種Unix-like操作系統,其中有Linus Torvalds用C語言寫的linux。
B語言之父還是Go語言之父
后來肯·湯普森又在谷歌寫出了go語言代替c語言與Python/ target=_blank class=infotextkey>Python,谷歌的文化是提倡每周五天干工作事4天,剩余一天自己安排,go語言就是在這種情況下開發出來的。
Go的三個作者分別是:Robert Griesemer(羅伯特.格利茨默),Rob Pike(羅伯.派克)和 ken Thompson(肯.湯普森)。
.Robert在開發Go之前是Goole V8 、Chubby和HotSpot JVM的主要奉獻者;
.Rob主要是Unix 、UTF-8、 plan9的作者;
.ken主要是B語言、C語言的作者、Unix之父。
Go語言設計初衷
1、設計Go語言是為了解決當時google開發遇到的問題:
- .大量的C++代碼,同時又引入了JAVA和Python
- .數以萬行的代碼
- .分布式的編譯系統
- .數百萬的服務器
2、Google開發中的痛點 :
- .編譯慢
- .失控 的依賴
- .每個工程師只是用了一個語言里面的一部分
- .程序難以維護
- .更新的花費越來越長
- .交叉編譯困難
3、如何解決當前的問題和痛點?
Go希望成為互聯網時代的C語言。多數系統級語言(包括Java和C#)的根本編程哲學來源于C++,將C++的面向對象進一步發揚光大。但是Go語言的設計者卻有不同的看法,他們認為值得學習的是C語言。C語言經久不衰的根源是它足夠簡單。因此,Go語言也是足夠簡單。
他們當時設計Go語言的目標是為了消除各種緩慢和笨重、改進各種低效和擴展性。Go是有那些開發大型系統的人設計的,同時也是為了這些人服務的,它是為了解決工程上的問題,不是為了研究語言設計;它還是為了讓我們的編程變得更舒適和方便。
C語言為什么仍被廣泛使用?
在今天,有許多編程語言可以讓開發者研發出比C更高效的應用,這些語言擁有豐富的內置庫,可以簡化與JSON、XML、UI、網頁、客戶端請求、數據庫鏈接、媒體操作等工作。盡管如此,C依然仍將長期活躍在編程一線,為什么呢?
那讓我們一起來看看C語言都有哪些無與倫比的優勢。
可移植性和高效
匯編語言的可移植性差,可C語言卻是一門可移植性非常好的語言。它盡可能地接近機器,同時它幾乎普遍適用于現有的處理器架構。幾乎現有的每個架構至少有一個C語言編譯器。如今,由于現代編譯器產生高度優化的二進制文件,用手寫的匯編來改進它們的輸出并不是一件容易的事。
由于它的可移植性和效率高效,"其他編程語言的編譯器、庫和解釋器經常用C語言實現"。像Python、Ruby和php這些解釋性語言的主要實現都是基于C語言,它甚至被其他語言的編譯器用來與機器通信。例如,C是Eiffel和Forth的中間語言。意味著這些語言的編譯器不需要為每個要支持的架構生成機器代碼,而只是生成中間的C代碼,由C編譯器處理機器代碼的生成。
C語言也已成為開發人員之間交流的一種語言。正如Dropbox工程經理、Cprogramming.com創建者Alex Allain所說:
C語言作為一門偉大的語言,可以讓大多數人以能接受的方式來表達編程中的常見想法。此外,C語言在使用中也有語法結構也會出現在其他語言中,例如,用于命令行參數的argc和argv,以及循環結構和變量類型,因此,即使對方不懂C語言,你也能找到一些共同點來與他們交談。
內存操作
內存管理和指針運算是C語言的重要特征,使C語言成為系統級編程(操作系統與嵌入式系統)的最佳搭檔。
在硬件/軟件邊界,計算機系統和微控制器將其外設和I/O引腳映射到內存地址。系統應用程序必須讀取和寫入這些自定義的內存位置,以便與外界進行通信。因此,C語言操作任意內存地址的能力對于系統編程是必不可少的。
例如,一個微控制器可以這樣設計:每當地址0x40008001的第4位被設置為1時,內存地址0x40008000中的字節就會被通用異步接收/發送器(或UART,一種與外設通信的常見硬件組件)發送,并且在設置后,它將被外設自動取消。下來演示一個C函數代碼,它通過該UART發送一個字節:
#define UART_BYTE *(char *)0x40008000
#define UART_SEND *(volatile char *)0x40008001 |= 0x08
void send_uart(char byte)
{
UART_BYTE = byte; // write byte to 0x40008000 address
UART_SEND; // set bit number 4 of address 0x40008001
}
send_uart函數的第一行代碼可擴展為:
*(char *)0x40008000 = byte;
這一行代碼是告訴編譯器將值是0x40008000解釋為一個指向char的指針,然后解除對該指針的定義(給出該指針所指向的值)(用最左邊的*操作符),最后將字節值分配給該解除定義的指針。換句話說:把變量byte的值寫到內存地址0x40008000。
將該函數的下一行代碼擴展一下:
*(volatile char *)0x40008001 |= 0x08;
在這行代碼中,我們對地址0x40008001和數值0x08(二進制的00001000,即第4位的1)進行了or位運算操作,并將結果存回地址0x40008001。換句話說:我們設置地址為0x40008001的字節的第4位。我們還聲明地址為0x40008001的值是易失性的。這就告訴編譯器,該值可能會被我們代碼外部的進程所修改,所以編譯器在寫入該地址后不會對該地址的值做出任何假設。(在這種情況下,該字節在我們用軟件設置后就被UART硬件取消了)。這些信息對于編譯器的優化器來說是很重要的。例如,如果我們在for循環中這樣做,而沒有指定該值是易失性的,編譯器可能會認為該值在被設置后永遠不會改變,并在第一個循環后跳過執行該命令。
確定資源使用
開發人員進行系統編程不能依賴的一個常見語言特性就是垃圾收集,甚至對一些嵌入式系統來說,只能進行動態分配。嵌入式應用程序在時間和內存資源方面非常有限。對于一些實時的嵌入系統,它們無法承受垃圾收集器的非確定性調用。如果因為內存不足而不能使用動態分配,那么擁有其他內存管理機制就顯得尤為重要,比如將數據放在自定義地址中,就像C語言的指針所允許的那樣。那些嚴重依賴動態分配和垃圾回收的語言不適用于資源緊張的系統。
Code Size
C語言有一個非常小的運行時,其代碼的內存占用要小于其它語言。例如與C++相比,一個由C語言生成的二進制文件,其體積大約是由類似的C++代碼生成的二進制文件的一半。造成這種情況的主要原因之一是異常支持。
異常(Exceptions )機制是C++比C語言多出來的一個不錯功能,如果異常不被觸發和巧妙的實現,他們實際上是沒有執行時間的開銷,但代價便是增加代碼體積。
下面讓我們以C++代碼為例:
// Class A declaration. Methods defined somewhere else;
class A
{
public:
A(); // Constructor
~A(); // Destructor (called when the object goes out of scope or is deleted)
void myMethod(); // Just a method
};
// Class B declaration. Methods defined somewhere else;
class B
{
public:
B(); // Constructor
~B(); // Destructor
void myMethod(); // Just a method
};
// Class C declaration. Methods defined somewhere else;
class C
{
public:
C(); // Constructor
~C(); // Destructor
void myMethod(); // Just a method
};
void myFunction()
{
A a; // Constructor a.A() called. (Checkpoint 1)
{
B b; // Constructor b.B() called. (Checkpoint 2)
b.myMethod(); // (Checkpoint 3)
} // b.~B() destructor called. (Checkpoint 4)
{
C c; // Constructor c.C() called. (Checkpoint 5)
c.myMethod(); // (Checkpoint 6)
} // c.~C() destructor called. (Checkpoint 7)
a.myMethod(); // (Checkpoint 8)
} // a.~A() destructor called. (Checkpoint 9)
該段代碼中的A類、B類和C類中的方法都被定義在了外部(例如在其它文件中)。因此,編譯器無法對它們進行解析,也不知道是否會拋出異常。所以程序必須準備處理從它們的任何構造函數、析構函數或其他方法調用中拋出的異常。解構器不應該拋出(做法非常糟糕),但用戶還是可以拋出,或者他們可以通過調用一些拋出異常的函數或方法(顯式或隱式)間接地拋出。
如果myFunction中的任何調用拋出了異常,堆棧解開機制必須能夠調用所有已經構建的對象的析構器。堆棧解開機制的一個實現將使用這個函數的最后一次調用的返回地址來驗證觸發異常的調用的 "檢查點編號"(這是簡單的解釋)。它是通過利用一個輔助的自動生成的函數(一種查找表)來實現的,當該函數的主體拋出異常時,該函數將被用于堆棧解繞,這將與此類似。
如果myFunction函數的任何一個調用拋出異常,C++的棧展開(stack unwinding)機制必須能夠調用所有已構建對象的析構器。棧展開機制的一個實現是將使用這個函數的最后一次調用的返回地址來驗證觸發異常調用的 "檢查點編號"(這是簡單的解釋)。它是通過利用一個輔助的自動生成函數(一種查找表)來實現,在該函數的主體拋出異常時,該函數將被用于堆棧解繞,與下面這段代碼類似:
// Possible autogenerated function
void autogeneratedStackUnwindingFor_myFunction(int checkpoint)
{
switch (checkpoint)
{
// case 1 and 9: do nothing;
case 3: b.~B(); goto destroyA; // jumps to location of destroyA label
case 6: c.~C(); // also goes to destroyA as that is the next line
destroyA: // label
case 2: case 4: case 5: case 7: case 8: a.~A();
}
}
如果從case 1和9拋出異常,則沒有對象需要銷毀。對于case 3,則b和a必須被銷毀。對于case 6,c和a必須被銷毀。在所有情況下,銷毀順序必須得到尊重。對于檢查點2、4、5、7和8,只有對象a需要被銷毀。
這個輔助函數增加了代碼的體積。這是C++添加到C語言中的空間開銷的一部分。許多嵌入式應用無法負擔這種額外的空間。因此,用于嵌入式系統的C++編譯器通常有一個禁用異常的標志。在C++中禁用異常是不自由的,因為標準模板庫嚴重依賴異常來告知錯誤。使用這種修改過的方案,沒有異常,需要對C++開發人員進行更多的培訓,以檢測可能的問題或發現錯誤。
C++的一個原則就是“開發者無需為不使用的東西付費”。對于其他語言來說,二進制體積的增加會變得非常糟糕,通過其它功能來增加額外開銷,雖然這些功能有用,但嵌入式系統卻負擔不起。雖然C語言不會給你提供這些額外功能,但他可以比其它語言擁有更緊湊的代碼足跡(code footprint ),占用更小的磁盤空間。
為什么要學習C語言
C語言并不難學,作為一門老牌編程語言,有關它的教程跟學習資料非常多,那么學習C語言有哪些好處呢?
C語言是開發人員的通用語言,網上或者圖書里面的不少算法都是基于C語言實現,這也為實現提供了最大的可移植性,開發者也會從中受益。
Understand the machine(用C語言思考)
當我們與同事討論代碼的某些部分或其他語言的某些特征時,我們最終會 "用C語言說話":"這部分是向對象傳遞一個 "指針 "還是復制整個對象?這里會不會發生任何 "轉換"?等等。
在分析高級語言的一部分代碼的行為時,我們很少討論(或思考)一部分代碼正在執行的匯編指令。相反,在討論機器在做什么時,我們可以用C語言描述(或想)得很清楚。
在許多有趣的C語言項目上工作
從大型數據庫服務器或操作系統內核甚至是為了滿足個人樂趣而制作的小型家用嵌入式應用,你都可以用C語言實現,并且還可以在網上找到相關Demo。Daniel呼吁大家,不要停止自己喜歡做的事情,比如學習C語言,它古老但小巧,并且是一門經過時間驗證的編程語言。
當下許多編程語言在其預設的用途上都要優于C語言,但這并不意味著就能擊敗C,當考慮性能優先的時候,C依然是王者。世界正運行在C語言驅動的設備上,無論你是否意識到,你使用的諸多設備的的確確都用到了C語言。