1. 程序和編程語言

程序(Program)告訴計算機應如何完成一個計算任務,這裡的計算可以是數學運算,比如解方程,也可以是符號運算,比如查找和替換文檔中的某個單詞。從根本上說,計算機是由數字電路組成的運算機器,只能對數字做運算,程序之所以能做符號運算,是因為符號在計算機內部也是用數字表示的。此外,程序還可以處理聲音和圖像,聲音和圖像在計算機內部必然也是用數字表示的,這些數字經過專門的硬件設備轉換成人可以聽到、看到的聲音和圖像。

程序由一系列指令(Instruction)組成,指令是指示計算機做某種運算的命令,通常包括以下幾類:

輸入(Input)

從鍵盤、檔案或者其它設備獲取數據。

輸出(Output)

把數據顯示到屏幕,或者存入一個檔案,或者發送到其它設備。

基本運算

執行最基本的數學運算(加減乘除)和數據存取。

測試和分支

測試某個條件,然後根據不同的測試結果執行不同的後續指令。

循環

重複執行一系列操作。

對於程序來說,有上面這幾類指令就足夠了。你曾用過的任何一個程序,不管它有多麼複雜,都是由這幾類指令組成的。程序是那麼的複雜,而編寫程序可以用的指令卻只有這麼簡單的幾種,這中間巨大的落差就要由程序員去填了,所以編寫程序理應是一件相當複雜的工作。編寫程序可以說就是這樣一個過程:把複雜的任務分解成子任務,把子任務再分解成更簡單的任務,層層分解,直到最後簡單得可以用以上指令來完成。

編程語言(Programming Language)分為低級語言(Low-level Language)和高級語言(High-level Language)。機器語言(Machine Language)和彙編語言(Assembly Language)屬於低級語言,直接用計算機指令編寫程序。而C、C++、Java、Python等屬於高級語言,用語句(Statement)編寫程序,語句是計算機指令的抽象表示。舉個例子,同樣一個語句用C語言、彙編語言和機器語言分別表示如下:

表 1.1. 一個語句的三種表示

編程語言表示形式
C語言a=b+1;
彙編語言

mov    0x804a01c,%eax
add    $0x1,%eax
mov    %eax,0x804a018

機器語言

a1 1c a0 04 08
83 c0 01
a3 18 a0 04 08


計算機只能對數字做運算,符號、聲音、圖像在計算機內部都要用數字表示,指令也不例外,上表中的機器語言完全由十六進制數字組成。最早的程序員都是直接用機器語言編程,但是很麻煩,需要查大量的表格來確定每個數字表示什麼意思,編寫出來的程序很不直觀,而且容易出錯,於是有了彙編語言,把機器語言中一組一組的數字用助記符(Mnemonic)表示,直接用這些助記符寫出彙編程序,然後讓彙編器(Assembler)去查表把助記符替換成數字,也就把彙編語言翻譯成了機器語言。從上面的例子可以看出,彙編語言和機器語言的指令是一一對應的,彙編語言有三條指令,機器語言也有三條指令,彙編器就是做一個簡單的替換工作,例如在第一條指令中,把movl ?,%eax這種格式的指令替換成機器碼a1 ?,?表示一個地址,在彙編指令中是0x804a01c,轉換成機器碼之後是1c a0 04 08(這是指令中的十六進制數的小端表示,小端表示將在第 5.1 節 “目標檔案”介紹)。

從上面的例子還可以看出,C語言的語句和低級語言的指令之間不是簡單的一一對應關係,一條a=b+1;語句要翻譯成三條彙編或機器指令,這個過程稱為編譯(Compile),由編譯器(Compiler)來完成,顯然編譯器的功能比彙編器要複雜得多。用C語言編寫的程序必須經過編譯轉成機器指令才能被計算機執行,編譯需要花一些時間,這是用高級語言編程的一個缺點,然而更多的是優點。首先,用C語言編程更容易,寫出來的代碼更緊湊,可讀性更強,出了錯也更容易改正。其次,C語言是可移植的(Portable)或者稱為平台無關的(Platform Independent)

平台這個詞有很多種解釋,可以指計算機體繫結構(Architecture),也可以指操作系統(Operating System),也可以指開發平台(編譯器、連結器等)。不同的計算機體繫結構有不同的指令集(Instruction Set),可以識別的機器指令格式是不同的,直接用某種體繫結構的彙編或機器指令寫出來的程序只能在這種體繫結構的計算機上運行,然而各種體繫結構的計算機都有各自的C編譯器,可以把C程序編譯成各種不同體繫結構的機器指令,這意味着用C語言寫的程序只需稍加修改甚至不用修改就可以在各種不同的計算機上編譯運行。各種高級語言都具有C語言的這些優點,所以絶大部分程序是用高級語言編寫的,只有和硬件關係密切的少數程序(例如驅動程式)才會用到低級語言。還要注意一點,即使在相同的體繫結構和操作系統下,用不同的C編譯器(或者同一個C編譯器的不同版本)編譯同一個程序得到的結果也有可能不同,C語言有些語法特性在C標準中並沒有明確規定,各編譯器有不同的實現,編譯出來的指令的行為特性也會不同,應該儘量避免使用不可移植的語法特性。

總結一下編譯執行的過程,首先你用文本編輯器寫一個C程序,然後保存成一個檔案,例如program.c(通常C程序的檔案名尾碼是.c),這稱為原始碼(Source Code)或源檔案,然後運行編譯器對它進行編譯,編譯的過程並不執行程序,而是把原始碼全部翻譯成機器指令,再加上一些描述信息,生成一個新的檔案,例如a.out,這稱為執行檔,執行檔可以被操作系統加載運行,計算機執行該檔案中由編譯器生成的指令,如下圖所示:

圖 1.1. 編譯執行的過程

編譯執行的過程

有些高級語言以解釋(Interpret)的方式執行,解釋執行過程和C語言的編譯執行過程很不一樣。例如編寫一個Shell腳本script.sh,內容如下:

#! /bin/sh
VAR=1
VAR=$(($VAR+1))
echo $VAR

定義Shell變數VAR的初始值是1,然後自增1,然後打印VAR的值。用Shell程序/bin/sh解釋執行這個腳本,結果如下:

$ /bin/sh script.sh
2

這裡的/bin/sh稱為解釋器(Interpreter),它把腳本中的每一行當作一條命令解釋執行,而不需要先生成包含機器指令的執行檔再執行。如果把腳本中的這三行當作三條命令直接敲到Shell提示符下,也能得到同樣的結果:

$ VAR=1
$ VAR=$(($VAR+1))
$ echo $VAR
2

圖 1.2. 解釋執行的過程

解釋執行的過程

編程語言仍在發展演化。以上介紹的機器語言稱為第一代語言(1GL,1st Generation Programming Language),彙編語言稱為第二代語言(2GL,2nd Generation Programming Language),C、C++、Java、Python等可以稱為第三代語言(3GL,3rd Generation Programming Language)。目前已經有了4GL(4th Generation Programming Language)和5GL(5th Generation Programming Language)的概念。3GL的編程語言雖然是用語句編程而不直接用指令編程,但語句也分為輸入、輸出、基本運算、測試分支和循環等幾種,和指令有直接的對應關係。而4GL以後的編程語言更多是描述要做什麼(Declarative)而不描述具體一步一步怎麼做(Imperative),具體一步一步怎麼做完全由編譯器或解釋器決定,例如SQL語言(SQL,Structured Query Language,結構化查詢語言)就是這樣的例子。

習題

1、解釋執行的語言相比編譯執行的語言有什麼優缺點?

這是我們的第一個思考題。本書的思考題通常要求讀者係統地總結當前小節的知識,結合以前的知識,並經過一定的推理,然後作答。本書強調的是基本概念,讀者應該抓住概念的定義和概念之間的關係來總結,比如本節介紹了很多概念:程序語句指令組成,計算機只能執行低級語言中的指令(彙編語言的指令要先轉成機器碼才能執行),高級語言要執行就必須先翻譯成低級語言,翻譯的方法有兩種--編譯解釋,雖然有這樣的不便,但高級語言有一個好處是平台無關性。什麼是平台?一種平台,就是一種體繫結構,就是一種指令集,就是一種機器語言,這些都可看作是一一對應的,上文並沒有用“一一對應”這個詞,但讀者應該能推理出這個結論,而高級語言和它們不是一一對應的,因此高級語言是平台無關的,概念之間像這樣的數量對應關係尤其重要。那麼編譯和解釋的過程有哪些不同?主要的不同在於什麼時候翻譯和什麼時候執行。

現在回答這個思考題,根據編譯和解釋的不同原理,你能否在執行效率和平台無關性等方面做一下比較?

希望讀者掌握以概念為中心的閲讀思考習慣,每讀一節就總結一套概念之間的關係圖畫在書上空白處。如果讀到後面某一節看到一個講過的概念,但是記不清在哪一節講過了,沒關係,書後的索引可以幫你找到它是在哪一節定義的。