從上篇文章中,我們可以看到一點頭文件的作用:就是聲明各個函數或變量,以供調用;而至於函數或變量的本體,在鏈接階段補上。在main.c中。我們手動聲明了兩個函數,但其實這樣比較費力不討好,因為如果還有很多其他文件也需要調用這兩個函數,那麼也要在那些文件中一次次的聲明;兩個函數還好,如果是成千上百個呢?還要一個一個的去聲明嗎?這時候,頭文件就是一個更好的選擇:只要把那些需要用到的函數或變量寫進頭文件,然後include這個頭文件就可以了。頭文件就是聲明的替代,或者說是批量的聲明。
我們的頭文件file1.h可以這樣寫:
#include
#include
#include
extern int gen_rnd(void);
extern void judge(int, int);
以前我在寫代碼的時候,經常會有這樣的疑問:我能不能調用另一個文件中的函數?或者我該如何調用另一個文件中的函數?是不是只要“另一個文件”在同一目錄下,我就可以隨意調用它的代碼?不是有所謂的外鏈嗎?
現在終於有了答案:如果想要調用另一個文件的函數或變量,首先被調用的函數或變量必須先聲明為具有外鏈的(詳見我介紹存儲類的那篇文章);然後在本文件中聲明相應的函數或變量(包括引用相關頭文件);最後,要鏈接相應的目標文件。這樣,我們就完成了所謂的跨文件調用。
我在介紹存儲類的那篇文章中提到過,函數聲明只要不被static聲明,都是具有外鏈的(extern可以被省略);而想要變量具有外鏈,必須將其聲明在所有函數之外(即使其具有文件作用域),同時用關鍵字extern修飾。這樣以來,這些函數或變量除了在定義所在的文件中有聲明外(定義本身即是一種聲明),在所有引用它們的文件中也會有一份對應的聲明,這些“備份”聲明的作用就是告訴引用它們的文件:被聲明的函數或變量,是同一個函數或變量,在完成鏈接之後指向相同的地址。因而,在我們的例子中,main.c聲明了judge()函數,就不能再定義一個同名的函數了,除非決定不再鏈接judge.o。
下面在談一談如何尋找頭文件以及如何根據頭文件(或聲明)在鏈接階段尋找對應的目標文件。#include命令後面的頭文件其實分為兩類,一種是用尖括號擴起來的,而另一種是用雙引號擴起來的。在我們的例子中,假設頭文件file1.h放在main.c所在的目錄,那麼編譯main.c的命令是這樣的:
gcc -s main.c
gcc會自動在main.c所在的目錄搜索頭文件。如果file1.h放在main.c所在的目錄的一個子目錄sub中,那麼編譯命令是:
gcc -s main.c -I sub
-I 選項用來指定頭文件的搜索路徑,這個路徑是被編譯文件的相對路徑,起始目錄就是被編譯文件所在目錄。當然,這個相對路徑也可以體現在#include命令中,比如,#include "sub/file1.h" 那麼編譯的時候就不需要-I選項了。
對於用角括號包含的頭文件, gcc 首先查找 -I 選項指定的目錄,然後查找系統的頭文件目錄(通常是 /usr/include );而對於用引號包含的頭文件, gcc 首先查找包含頭文件的 .c 文件所在的目錄,然後查找 -I 選項指定的目錄,然後查找系統的頭文件目錄。
指定頭文件的路徑是為了在編譯的時候生成正確的聲明,聲明有了,如何去找到對應的目標文件呢?對於自己寫的頭文件,當然是自己在鏈接的時候給出路徑來;而對於用尖括號括起來的頭文件呢?gcc會自動去尋找相應的庫文件,比如printf函數就在libc庫中。這些標准庫文件的路徑都是系統固定的,gcc會默認去這些目錄搜索,編譯器默認會找的目錄可以用 -print-search-dirs 選項查看。
那麼庫文件(暫時只考慮靜態庫)和目標文件有啥區別?其實我們也可以把剛才的judge.o和gen_rnd.o做成一個靜態庫:
$ ar rs libgame.a judge.o gen_rnd.o
庫文件名都是以 lib 開頭的,靜態庫以 .a 作為後綴,表示Archive。 ar 命令類似於 tar 命令,起一個打包的作用,但是把目標文件打包成靜態庫只能用 ar 命令而不能用 tar 命令。選項 r 表示將後面的文件列表添加到文件包,如果文件包不存在就創建它,如果文件包中已有同名文件就替換成新的。 s 是專用於生成靜態庫的,表示為靜態庫創建索引,這個索引被鏈接器使用。
然後我們可以這樣編譯main.c:
gcc main.c -L . -I game -o main
-L 選項告訴編譯器去哪裡找需要的庫文件,-L. 表示在當前目錄找, -l (小寫的L)選項告訴編譯器要鏈接libgame庫.注意,即使庫文件就在當前目錄,編譯器默認也不會去找的,所以 -L選項不能少。
那為什麼要把目標文件做成庫文件呢?首先,如果有太多目標文件的話,gcc命令會敲的手疼(⊙﹏⊙b汗),而庫文件的編譯命令就很簡潔;其次,假設我們又在judge.c中添加了一個無關的add函數,那麼直接鏈接目標文件,會把這些無關代碼也加進可執行文件中,於是如果無關函數很多的話,就是使得可執行文件變得很大,但是如果鏈接庫文件的話,鏈接器可以從靜態庫中只取出需要的部分來做。
最後留個尾巴:為什麼不直接include “judge.c”等那些源文件呢?