在為Linux開發應用程式時,絕大多數情況下使用的都是C語言,因此幾乎每一位Linux程式員面臨的首要問題都是如何靈活運用C編譯器。目前Linux 下最常用的C語言編譯器是GCC(GNU Compiler Collection),它是GNU項目中符合ANSI C標準的編譯系統,能夠編譯用C、C++和Object C等語言編寫的程式。GCC不僅功能非常強大,結構也異常靈活。最值得稱道的一點就是它可以通過不同的前端模組來支援各種語言,如Java、 Fortran、Pascal、Modula-3和Ada等。
開放、自由和靈活是Linux的魅力所在,而這一點在GCC上的體現就是程式員通過它能夠更好地控制整個編譯過程。在使用GCC編譯器時,編譯過程可以被細分為四個階段:
◆ 預先處理(Pre-Processing)
◆ 編譯(Compiling)
◆ 彙編(Assembling)
◆ 連結(Linking)
Linux程式員可以根據自己的需要讓 GCC在編譯的任何階段結束,以便檢查或使用編譯器在該階段的輸出資訊,或者對最後產生的二進位檔案進行控制,以便通過加入不同數量和種類的調試代碼來為今後的調試做好準備。和其它常用的編譯器一樣,GCC也提供了靈活而強大的代碼最佳化功能,利用它可以產生執行效率更高的代碼。
GCC提供了30多條警告資訊和三個警告層級,使用它們有助於增強程式的穩定性和可移植性。此外,GCC還對標準的C和C++語言進行了大量的擴充,提高程式的執行效率,有助於編譯器進行代碼最佳化,能夠減輕編程的工作量。
GCC起步
在學習使用GCC之前,下面的這個例子能夠協助使用者迅速理解GCC的工作原理,並將其立即運用到實際的項目開發中去。首先用熟悉的編輯器輸入清單1所示的代碼:
清單1:hello.c
#include int main(void){printf ("Hello world, Linux programming!//n");return 0;} |
然後執行下面的命令編譯和運行這段程式:
# gcc hello.c -o hello# ./helloHello world, Linux programming! |
從程式員的角度看,只需簡單地執行一條GCC命令就可以了,但從編譯器的角度來看,卻需要完成一系列非常繁雜的工作。首先,GCC需要調用預先處理程式 cpp,由它負責展開在源檔案中定義的宏,並向其中插入“#include”語句所包含的內容;接著,GCC會調用ccl和as將處理後的原始碼編譯成目標代碼;最後,GCC會調用連結程式ld,把產生的目標代碼連結成一個可執行程式。
為了更好地理解GCC的工作過程,可以把上述編譯過程分成幾個步驟單獨進行,並觀察每步的運行結果。第一步是進行先行編譯,使用-E參數可以讓GCC在預先處理結束後停止編譯過程:
# gcc -E hello.c -o hello.i |
此時若查看hello.cpp檔案中的內容,會發現stdio.h的內容確實都插到檔案裡去了,而其它應當被預先處理的宏定義也都做了相應的處理。下一步是將hello.i編譯為目標代碼,這可以通過使用-c參數來完成:
# gcc -c hello.i -o hello.o |
GCC預設將.i檔案看成是預先處理後的C語言原始碼,因此上述命令將自動跳過預先處理步驟而開始執行編譯過程,也可以使用-x參數讓GCC從指定的步驟開始編譯。最後一步是將產生的目標檔案連結成可執行檔:
在採用模組化的設計思想進行軟體開發時,通常整個程式是由多個源檔案組成的,相應地也就形成了多個編譯單元,使用GCC能夠很好地管理這些編譯單元。假設有一個由foo1.c和foo2.c兩個源檔案組成的程式,為了對它們進行編譯,並最終產生可執行程式foo,可以使用下面這條命令:
# gcc foo1.c foo2.c -o foo |
如果同時處理的檔案不止一個,GCC仍然會按照預先處理、編譯和連結的過程依次進行。如果深究起來,上面這條命令大致相當於依次執行如下三條命令:
# gcc -c foo1.c -o foo1.o# gcc -c foo2.c -o foo2.o# gcc foo1.o foo2.o -o foo |
在編譯一個包含許多源檔案的工程時,若只用一條GCC命令來完成編譯是非常浪費時間的。假設項目中有100個源檔案需要編譯,並且每個源檔案中都包含 10000行代碼,如果像上面那樣僅用一條GCC命令來完成編譯工作,那麼GCC需要將每個源檔案都重新編譯一遍,然後再全部串連起來。很顯然,這樣浪費的時間相當多,尤其是當使用者只是修改了其中某一個檔案的時候,完全沒有必要將每個檔案都重新編譯一遍,因為很多已經產生的目標檔案是不會改變的。要解決這個問題,關鍵是要靈活運用GCC,同時還要藉助像Make這樣的工具。
警告提示功能
GCC包含完整的出錯檢查和警告提示功能,它們可以協助Linux程式員寫出更加專業和優美的代碼。先來讀讀清單2所示的程式,這段代碼寫得很糟糕,仔細檢查一下不難挑出很多毛病:
◆main函數的傳回值被聲明為void,但實際上應該是int;
◆使用了GNU文法擴充,即使用long long來聲明64位整數,不符合ANSI/ISO C語言標準;
◆main函數在終止前沒有調用return語句。
清單2:illcode.c
#include void main(void){long long int var = 1;printf("It is not standard C code!//n");} |
下面來看看GCC是如何協助程式員來發現這些錯誤的。當GCC在編譯不符合ANSI/ISO C語言標準的原始碼時,如果加上了-pedantic選項,那麼使用了擴充文法的地方將產生相應的警告資訊:
# gcc -pedantic illcode.c -o illcodeillcode.c: In function `main':illcode.c:9: ISO C89 does not support `long long'illcode.c:8: return type of `main' is not `int' |
需要注意的是,-pedantic編譯選項並不能保證被編譯器與ANSI/ISO C標準的完全相容,它僅僅只能用來協助Linux程式員離這個目標越來越近。或者換句話說,-pedantic選項能夠協助程式員發現一些不符合ANSI/ISO C標準的代碼,但不是全部,事實上只有ANSI/ISO C語言標準中要求進行編譯器診斷的那些情況,才有可能被GCC發現並提出警告。
除了-pedantic之外,GCC還有一些其它編譯選項也能夠產生有用的警告資訊。這些選項大多以-W開頭,其中最有價值的當數-Wall了,使用它能夠使GCC產生儘可能多的警告資訊:
# gcc -Wall illcode.c -o illcodeillcode.c:8: warning: return type of `main' is not `int'illcode.c: In function `main':illcode.c:9: warning: unused variable `var' |
GCC給出的警告資訊雖然從嚴格意義上說不能算作是錯誤,但卻很可能成為錯誤的棲身之所。一個優秀的Linux程式員應該盡量避免產生警告資訊,使自己的代碼始終保持簡潔、優美和健壯的特性。
在處理警告方面,另一個常用的編譯選項是-Werror,它要求GCC將所有的警告當成錯誤進行處理,這在使用自動編譯工具(如Make等)時非常有用。如果編譯時間帶上-Werror選項,那麼GCC會在所有產生警告的地方停止編譯,迫使程式員對自己的代碼進行修改。只有當相應的警告資訊消除時,才可能將編譯過程繼續朝前推進。執行情況如下:
# gcc -Wall -Werror illcode.c -o illcodecc1: warnings being treated as errorsillcode.c:8: warning: return type of `main' is not `int'illcode.c: In function `main':illcode.c:9: warning: unused variable `var' |
對Linux程式員來講,GCC給出的警告資訊是很有價值的,它們不僅可以協助程式員寫出更加健壯的程式,而且還是跟蹤和偵錯工具的有力工具。建議在用GCC編譯原始碼時始終帶上-Wall選項,並把它逐漸培養成為一種習慣,這對找出常見的隱式編程錯誤很有協助。
庫依賴
在Linux 下開發軟體時,完全不使用第三方函數庫的情況是比較少見的,通常來講都需要藉助一個或多個函數庫的支援才能夠完成相應的功能。從程式員的角度看,函數庫實際上就是一些標頭檔(.h)和庫檔案(.so或者.a)的集合。雖然Linux下的大多數函數都預設將標頭檔放到/usr/include/目錄下,而庫檔案則放到/usr/lib/目錄下,但並不是所有的情況都是這樣。正因如此,GCC在編譯時間必須有自己的辦法來尋找所需要的標頭檔和庫檔案。
GCC採用搜尋目錄的辦法來尋找所需要的檔案,-I選項可以向GCC的標頭檔搜尋路徑中添加新的目錄。例如,如果在/home/xiaowp/include/目錄下有編譯時間所需要的標頭檔,為了讓GCC能夠順利地找到它們,就可以使用-I選項:
# gcc foo.c -I /home/xiaowp/include -o foo |
同樣,如果使用了不在標準位置的庫檔案,那麼可以通過-L選項向GCC的庫檔案搜尋路徑中添加新的目錄。例如,如果在/home/xiaowp/lib/目錄下有連結時所需要的庫檔案libfoo.so,為了讓GCC能夠順利地找到它,可以使用下面的命令:
# gcc foo.c -L /home/xiaowp/lib -lfoo -o foo |
值得好好解釋一下的是-l選項,它指示GCC去串連庫檔案libfoo.so。Linux下的庫檔案在命名時有一個約定,那就是應該以lib三個字母開頭,由於所有的庫檔案都遵循了同樣的規範,因此在用-l選項指定連結的庫檔案名稱時可以省去lib三個字母,也就是說GCC在對-lfoo進行處理時,會自動去連結名為libfoo.so的檔案。
Linux下的庫檔案分為兩大類分別是動態連結程式庫(通常以.so結尾)和靜態連結庫(通常以.a結尾),兩者的差別僅在程式執行時所需的代碼是在運行時動態載入的,還是在編譯時間靜態載入的。預設情況下,GCC在連結時優先使用動態連結程式庫,只有當動態連結程式庫不存在時才考慮使用靜態連結庫,如果需要的話可以在編譯時間加上-static選項,強制使用靜態連結庫。例如,如果在/home/xiaowp/lib/目錄下有連結時所需要的庫檔案libfoo.so和libfoo.a,為了讓 GCC在連結時只用到靜態連結庫,可以使用下面的命令:
# gcc foo.c -L /home/xiaowp/lib -static -lfoo -o foo |