社會進行曲——淺談計算機語言的發展 (轉)

worldblog發表於2007-08-17
社會進行曲——淺談計算機語言的發展 (轉)[@more@]  社會進行曲——淺談語言的發展

一.  原始社會 機器語言

人類剛剛誕生,社會只是雛形。生產力極度低下,人類卻享有最多的自由。

1946年,馮諾依曼的第一臺現代計算機誕生時的情形像極了原始社會。

“員”(我不知道這個名稱是否辱沒了他們)是精通電子技術的專家,他們透過設計複雜的電路板來完成各種計算工作,而計算機也僅僅用來滿足最根本的需要——軍事(由此可看出人類的本性)。“程式設計師”是計算機的主人,他們能操縱所有的資源,指揮那個笨拙的龐然大物完成種種不可思議的任務。

語言的誕生

IO的發展促使了語言的誕生。穿孔紙帶的產生使程式設計師不必具有太多的電子技術知識,他們只需要懂得計算機的語言(指令),就能與他們交流。計算機所能聽懂的語言是由0和1組成的一長串奇怪的數字,這就是 機器語言。程式設計師辛苦的在紙帶上打孔,向計算機發號施令。

讓我們推測一下下面這個簡單的程式用機器語言來寫應該是什麼樣子(既然是推測,那麼大多數的細節都可能是錯的,所以我一直認為做一個歷史學家應該是個好主意)。

Void main()

{

  int I, J, K; 

  I = 1;

  J = 1;

  K = I + J;

};

請原諒我將這種古老文字翻譯成了中文,因為我想沒有人會對它感興趣的:

將位置為40000的一個機器字(如果是32位計算機的話)置為1;

將記憶體位置為50000的一個機器字(如果是32位計算機的話)置為1;

將內  將記憶體位置為40000和50000的兩個機器字相加,並將結果置給記憶體位置為60000 的機器字。

因為程式設計師可以控制一切硬體資源,所以他們可以指定任意記憶體位置來使用(只要硬體允許),所以上述的40000等也可以是其他的數字。:namespace prefix = o ns = "urn:schemas--com::office" />

前輩程式設計師就這樣過著自由但簡樸的生活。

社會持續進步,IO繼續發展。新型的IO裝置,如磁帶、鍵盤等的出現使穿孔紙帶被丟進了垃圾堆。但程式設計師們依然平等、自由。

原始社會的崩潰

  特權階級的出現標誌著原始社會行將崩潰,貴族就是操作。貴族壟斷了部分特權,程式設計師們再不能像以前那樣操縱所有的硬體資源了,相當多的資源由接管,程式設計師們失去了部分自由,卻換來了開發的提高。

讓我們再考慮一下,上面那個小程式用機器語言實現應該是什麼樣子的:

版本1:

將位置為40000的一個機器字(如果是32位計算機的話)置為1;

將位置為50000的一個機器字(如果是32位計算機的話)置為1;

將位置為40000和50000的兩個機器字相加,並將結果置給位置為60000的機器字。

看起來,版本1似乎與以前的實現幾乎相同,但實際上,這裡的40000等數字已經不是實際的記憶體位置了,作業系統來負責選用實際的記憶體位置,也就是說,它將負責將程式(程式)地址對映為 實際的實體地址。

版本2:

將棧指標(pStack)向下移動12個位元組;

將記憶體位置為pStack – 12 的一個機器字置為1;

將記憶體位置為pStack – 8 的一個機器字置為1;

將上述兩個位置的機器字相加,並賦給記憶體位置為pStack – 4的機器字。

版本2的實現假定硬體中存在棧指標暫存器,幸運的是,我們最熟悉的x86系列滿足這一點。

棧則是非常重要的一個概念,我們將在以後討論這一點。

不管你喜歡與否,程式設計師們最自由的時代過去了。在這個偉大的時代裡,每個程式設計師都通曉一切,他們清楚的知道自己的程式是怎樣執行的,很少有迷忙與焦慮,他們是計算機的主人。但這個時代已經一去不復返了,

機器語言,這個最根本的語言也被丟給了編譯程式,程式設計師們再也不用記憶那些難懂的0、1字串了!

二 奴隸社會  語言

正因為機器語言十分難記,人們逐漸使用 助記符號來代替0、1字串,如ADD、MOVE等等。他們只是對機器語言的簡單的替換,但僅僅這樣仍算是一個巨大的進步,彙編的出現大大提高了程式設計師的開發效率。

計算機當然看不懂組合語言,編譯程式負責將彙編語句翻譯成機器語言。

奴隸社會後期——宏彙編

早期的彙編語句與機器語言語句之間是一一對應的關係。但這種機械的平均主義顯然成了社會發展的障礙。能者多勞,宏彙編語句一句可能抵的上好幾句的機器語言。這種抽象提高了程式設計師的工作效率,但卻使他們與機器越來越遠。隨著社會的進步,人類正逐步失去自由。

人的本性是貪婪的,為獲得更高的效率,程式設計師們很願意付出自由的代價,人們正呼喚著更加抽象的語言,可以遮蔽掉所有的機器細節,社會正醞釀著革命。

三   封建社會 高階語言的誕生

FORTRAN的出現標誌著一個新的時代的到來,人們從此得以從複雜的機器細節中抽身,人們再不用管記憶體、暫存器等瑣碎的事情了,這當然是計算機歷史上的一次偉大的革命。

封建社會早期:goto橫行的時代

  高階語言出現依始,goto由於他的靈活和高效成了程式設計師們競相追捧的工具,有關goto的種種複雜詭異的技巧在程式設計師之間傳誦。Goto甚至成了衡量程式設計師水平的標尺。

  程式設計師們雖然失去了控制硬體資源的自由,卻在高階語言的使用上不受任何限制。他們可以使用任意的風格,詭異的技巧,寫出除他們之外(大多數情況下包括他們)誰也看不懂的程式。

  封建社會後期:結構化

  當大多數人還沉浸在goto帶給他們的自由與榮耀時,不世出的天才人物Edsger Dijkstan(此君還是訊號量的發明人,卻不幸於數月之前架鶴西去)卻敏銳的發現了goto所帶來的種種問題。這位荷蘭傳奇科學家發表了他的著名論文《goto 有害論》,轟動了整個計算機世界。Edsger Dijkstan指出,goto是導致程式複雜、混亂、難以理解的罪魁禍首,它還使效率難以度量,程式難以維護。

  透過眾人的努力,人們總結出一套行之有效的程式設計方法,稱為結構化程式設計。程式設計師不能再隨心所欲的編碼了,goto成了程式設計師們避之不及的毒藥。又一次,人類為了社會的進步,付出了自由的代價。

高階語言編譯淺談

在這裡,我不想談編譯原理(我也不懂J),我只想根據自己理解,談幾個我認為比較重要的問題(以c/c++語言為例)。

1.  記憶體分配策略——棧和堆

棧和堆本來是不容易混淆的,但怪就怪在有些書上將棧稱為“堆疊”,憑空給人們帶來許多疑惑。

 說白了,棧和堆都是 程式空間內的一段虛存(將被對映到實體記憶體,所以也可說是記憶體中的一段空間),所不同的是,計算機對棧給予了更多支援。

  還記得棧指標暫存器嗎,當程式記憶體時,棧指標將指向棧頂,存貯空間的分配就是透過移動棧指標來完成的(見版本2)。那麼,哪些變數將在棧中分配空間呢:

l  引數

例如:

 int Add(int I, int J)

{

int K = I + J;

return K;

};

 

當此函式被呼叫時(a= Add(1, 2)),棧指標向下移動8個位元組,引數 I(1)和J(2)將被壓棧,當函式返回時,棧指標向上移動,空間自動被釋放。

l  區域性非靜態變數(自動變數)

仍以上面函式為例,變數K的空間也是在棧中分配的,當此函式被呼叫時,引數首先被壓棧,然後,棧指標繼續向下移動4個位元組,給K分配空間,I和J相加的結果首先被賦給K,然後,再將K的值賦給呼叫者的變數a。這時,函式就要退出,棧指標相上移動12個位元組,釋放掉所有的空間。正因為區域性非靜態變數變數空間的分配是由在編譯期間就確定,所以又被稱為自動變數。

既然棧用起來這麼方便,那麼要 堆 來幹什麼呢?顯然,如果我們在程式執行之前(編譯時)就能知道要申請多少空間,棧就足夠了,但在複雜的應用中,人們很難提前知道自己要申請多少空間——例如在空管系統中,只有透過外部訊號才能知道究竟有多少架飛機來。

我們可以把 堆 看成儲存空間的倉庫,當我們需要儲存空間時就從倉庫中領取,不使用這段空間時就把它還回去(這是一個相當複雜的演算法)。領取和歸還的過程是程式時決定的(所謂的動態決定),編譯時無法確定儲存空間的位置。

c++語言中透過關鍵字new來動態申請空間(c語言中使用函式malloc),如:

int * pInt = new int;

  將在堆中分配一個int型數(注意,指標pInt的儲存空間在棧中)。 

  堆中分配的儲存空間是在執行時確定的,編譯器無法自動的清除掉這些儲存空間,所以必須由程式設計師負責清除,c++中使用關鍵字delete(c中使用函式free),如:

  delete pInt;

   由於儲存空間是有限的資源,如果我們不及時釋放就會帶來種種問題,這種錯誤被稱作記憶體洩漏。

注意:一些語言如中,堆中的空間也能自動釋放。實際上Java是透過實現一個“垃圾收集器”在執行期間實現這一點的。

到這裡,棧和堆的問題基本已經談完了,但既然前面已經談了區域性非靜態變數,不妨瞭解一下靜態變數和全域性變數。

如下例:

int Sum(int I)

{

  static sum = 0;

  sum += I;

  return sum;

}

 函式中的變數sum就是靜態變數,靜態變數的儲存空間並不是分配在棧中。它被

  分配在一個特殊的位置,稱作靜態儲存區。我們不需要知道 靜態儲存區究竟在什麼地方,我們只需要明白,靜態儲存區內分配的儲存空間只有在程式退出時才會被釋放。所以,函式中的變數sum就具有了記憶功能,如:

第一次呼叫此函式時  a = Sum(1);  a = 1;

第二次呼叫此函式時  b = Sum(2);  b = 3.

全域性變數就是定義在所有函式之外的變數,它可以被所有的函式使用。當然,它也被存放在 靜態儲存區。

2.  編譯與連結

l  宣告與定義

要想區分編譯與連結,就先要明白宣告與定義的區別。

Bruce Eckel的經典名著《c++思想》2.1節中對此有清楚的解釋,我在這裡就引用他的講解。

“宣告”向計算機介紹名字,說明這個名字是什麼意思。而“定義”真正為這個名字分配儲存空間。

記住,c/c++中任何變數只能被定義一次!

  所以,在頭中定義變數不是一個好習慣,因為一旦此標頭檔案被多個其他檔案包含,連結期間就會多重定義錯誤(奇怪的是,在一些老式的c編譯器中這樣做居然不會產生錯誤,但無論如何,請不要這樣做)。

  關鍵字extern 表示現在我們只是宣告一個變數(函式),但對函式來說extern是可選的,不帶函式體的函式自動被看成一個宣告。

l  編譯與連結

編譯與連結是兩個階段,編譯的單元是檔案,也就是說,一次只能編譯一個檔案,編譯的結果是二進位制的目標檔案。而在連結階段,各個目標檔案被“連結”在一起形成可執行檔案(或動態連線庫等)。

編譯階段允許檔案中存在 宣告過但沒有定義的 變數或函式,變數名或函式名被存放在目標檔案中,在連結階段編譯器將綜合所有的目標檔案,找到所有變數和函式的定義。

例如:

檔案main.c如下:

void  NotExit(int i);

void main()

{

  NotExit(1);

}

 

cc main.c 能夠透過編譯,卻會在連結階段報錯:

  NotExit:unresolved function.

假如檔案 NoExit.c如下:

  void NotExit(int i)

{

  return;

}

 

cc main.c NotExit.c 就能透過編譯和連結。

同樣,倘若存在多個NotExit函式的定義,程式也將不能透過連結。

但遺憾的是,如果檔案NotExit.c中函式的定義如下:

 void NotExit(double i)

  {

  return;

}

cc main.c NotExit.c 竟能透過編譯和連結(如果cc呼叫的是c語言編譯器的話)。但是,如果是c++編譯器,上述程式不能透過連結。

那麼,c++是怎樣做到這一點的呢? 這要從函式過載談起:

過載使我們能夠定義具有相同名稱的函式(只要他們的引數不同),猛一看,這將給 連結 階段帶來混亂,如果函式名相同的話,連結器如何知道應該呼叫哪一個函式呢?答案就在函式名中,c++編譯器將改變函式的內部名稱!

舉個最簡單的例子,

  void  NotExit(int i)在內部將被命名為 NotExit_int;而void NotExit(double i)將被命名為 NotExit_double。

  extern “C” 的產生:由於編譯器將在內部改變函式的名稱,而不幸的是並不存在一個標準來規範這種行為,所以不同的編譯器產生的函式名是不同的。所以,如果想要使用其他 c++編譯器產生的庫,將變得十分困難。為了解決這個問題,人們想出了extern “C”。這個符號表示,要以c編譯器的方式產生函式,也就是說,編譯器不能改變函式名稱。但這樣一來,也就意味著此函式不能擁有c++函式的好處:不能過載、也不能成為成員函式。

四  資本主義社會 面向的語言

歷史的車輪不可阻擋,結構化程式設計沒有風光太久,就不得不將風頭讓給了新興貴族——物件導向。

自第一個成功的面嚮物件語言smalltalk問世以後,人們紛紛搭乘oo快車,種種新興的面相物件語言不斷出現,而一些古老的語言也不甘寂寞,為自己披上了oo外衣。最成功的面嚮物件語言有:c++、Java等。

c++是對c的擴充套件。c是封建社會中最有影響力的語言,它以他的靈活、高效聞名於世。c++繼承了c的優點,奇蹟般的在沒有損失太多效率的情況下支援了所有的oo特徵(Bjarne Stroustrup真是個天才)。但是,正因為c++有太多的傳統需要傳承,它身上有太多非oo的特點,所以它並不是一個“純”的面嚮物件語言。而且,c++支援了很多oo中很有爭議的特徵,如:多重繼承、虛繼承(如果你不想跟自己過不去,就別使用他們)等,使c++成為一種非常複雜的語言。有人甚至認為,c++的規模之大,有甚於Ada(就我看來,Ada真是一種複雜的語言,它的強型別檢查使習慣自由的我感覺像是帶上了鐐銬)。

相比之下Java就“純潔”多了。Java脫胎於c++卻摒棄c++中許多不符合oo規範的特徵,並努力使自己簡單。Java並不僅僅是一種程式設計語言,它還代表了一種潮流,Java程式擁有一個統一的執行環境——Java虛擬機器,所以它可以輕易的跨越平臺,成為各大廠商的新寵。與c++相比,Java最大的缺點就是執行效率低,但隨著Java本身和硬體的持續發展這個缺點越來越不明顯,而開發效率的顯著提高使大批c++程式設計師轉投Java陣營。有人甚至認為,Java將會像高階語言取代組合語言那樣取代c++語言。

 物件導向若干問題淺談

1.  物件導向三大基石

封裝、繼承、多型

2.  關於程式碼重用

物件導向出現依始,大量類庫被設計使用,相比於傳統的函式庫,他們更加容易使用,人們認為找到了一個良好的程式碼重用機制,繼承的作用被過分誇大。但是,許多年過去了,人們發現 程式碼重用 仍然是很困難的任務。

人們進行反思,提出了一些新的觀點:

l  慎用繼承,多用組合

l  關於 實現繼承 和 介面繼承

  實現繼承就是傳統的繼承方式,子類不僅繼承基類的介面(成員函式)還繼承基類的實現。

  介面繼承指 子類僅僅繼承基類的 介面,但不繼承基類的實現。為方便理解我舉一個c++的例子:

  class  IAdd

  {

  virtual int add(int I, int J) = 0;

  }

  class Add: public IAdd

{

  int add(int I, int J)

{

return I+J;

}

}

作為基類的提供者僅僅提供一個介面的描述,具體的實現由實現廠商來完成,不同廠商的實現由於使用同樣的介面,可以互換。

如果使用標準的介面的話,作為的開發者就可以在市場上選購他所需要的實現,此時,軟體將可以像搭積木一樣被搭建起來,這就是所謂的“程式設計”。

當前,新技術、新思想、新名詞層出不窮,令人眼花繚亂。各種技術領域越來越走向分化,程式設計師們距離底層實現越來越遠,不懂的領域越來越多,也越來越感到焦慮和迷忙,他們已經由計算機的主人變成了他的奴隸。

總之,這是一個紛繁複雜的時代,這是一個人類徹底失去自由的時代,這是一個令程式設計師迷忙焦慮的時代,不管你喜歡與否,我們現在正處在這一偉大的時代中。

五  共產主義社會  ??語言

這是一個理想的社會,人類將邁向 自由王國,獲得最大的自由。但這一時代的來臨還遙遙無期……

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-962953/,如需轉載,請註明出處,否則將追究法律責任。

相關文章