本文將首先介紹為什么要將你的C源代碼分離成幾個合理的獨立檔案,什么時候需要分,怎么才能分的好。然后將會告訴你 GNU Make怎樣使你的編譯和連接步驟自動化。 對于其它 Make 工具的用戶來說,雖然在用其它類似工具時要做適當(dāng)?shù)恼{(diào)整,本文的內(nèi)容仍然是非常有用的。如果對你自己的編程工具有懷疑,可以實際的試一試,但請先閱讀用戶手冊。
1) 多文件項目
1.1為什么使用它們? 首先,多文件項目的好處在那里呢?
它們看起來把事情弄的復(fù)雜無比。又要 header 文件,又要 extern 聲明,而且如果需要查找一個文件,你要在更多的文件里搜索。但其實我們有很有力的理由支持我們把一個項目分解成小塊。當(dāng)你改動一行代碼,編譯器需要全部重新編譯來生成一個新的可執(zhí)行文件。但如果你的項目是分開在幾個小文件里,當(dāng)你改動其中一個文件的時候,別的源文件的目標(biāo)文件(object files)已經(jīng)存在,所以沒有什么原因去重新編譯它們。你所需要做的只是重現(xiàn)編譯被改動過的那個文件,然后重新連接所有的目標(biāo)文件罷了。在大型的項目中,這意味著從很長的(幾分鐘到幾小時)重新編譯縮短為十幾,二十幾秒的簡單調(diào)整。只要通過基本的規(guī)劃,將一個項目分解成多個小文件可使你更加容易的找到一段代碼。 很簡單,你根據(jù)代碼的作用把你的代碼分解到不同的文件里。當(dāng)你要看一段代碼時,你可以準(zhǔn)確的知道在那個文件中去尋找它。從很多目標(biāo)文件生成一個程序包 (Library)比從一個單一的大目標(biāo)文件生成要好的多。當(dāng)然實際上這是否真是一個優(yōu)勢則是由你所用的系統(tǒng)來決定的。但是當(dāng)使用 gcc/ld (一個 GNU C 編譯/連接器) 把一個程序包連接到一個程序時,在連接的過程中,它會 嘗試不去連接沒有使用到的部分。但它每次只能從程序包中把一個完整的目標(biāo)文件排除在外。因此如果你參考一個程序包中某一個目標(biāo)檔中任何一個符號的話,那么這個目標(biāo)文件整個都會被連接進(jìn)來。要是一個程序包被非常充分的分解了的話,那么經(jīng)連接后,得到的可執(zhí)行文件會比從一個大目標(biāo)文件組成的程序包連接得到的文件小得多。又因為你的程序是很模塊化的,文件之間的共享部分被減到最少,那就有很多好處—— 可以很容易的追蹤到臭蟲,這些模塊經(jīng)常是可以用在其它的項目里的,同時別人也可以 更容易的理解你的一段代碼是干什么的。當(dāng)然此外還有許多別的好處......
1.2 何時分解你的項目 很明顯,把任何東西都分解是不合理的。象"世界,你們好"這樣的簡單程序根本就不能分,因為實在也沒什么可分的。把用于測試用的小程序分解也是沒什么意思的。但一 般來說,當(dāng)分解項目有助于布局、發(fā)展和易讀性的時候,我都會采取它。在大多數(shù)的情況下,這都是適用的。(所謂"世界,你們好",既 'hello world' ,只是一個介紹一種編程語言時慣用的范例程序,它會在屏幕上顯示一行 'hello world' 。是最簡單的程序。)如果你需要開發(fā)一個相當(dāng)大的項目,在開始前,應(yīng)該考慮一下你將如何實現(xiàn)它,并且生成幾個文件(用適當(dāng)?shù)拿郑﹣矸拍愕拇a。當(dāng)然,在你的項目開發(fā)的過程中,你可以建立新的文件,但如果你這么做的話,說明你可能改變了當(dāng)初的想法,你應(yīng)該想想是否 需要對整體結(jié)構(gòu)也進(jìn)行相應(yīng)的調(diào)整。對于中型的項目,你當(dāng)然也可以采用上述技巧,但你也可以就那么開始輸入你的代碼,當(dāng)你的碼多到難以管理的時候再把它們分解成不同的檔案。但以我的經(jīng)驗來說,開始時 在腦子里形成一個大概的方案,并且盡量遵從它,或在開發(fā)過程中,隨著程序的需要而修改,會使開發(fā)變得更加容易。
1.3 怎樣分解項目 先說明,這完全是我個人的意見,你可以(也許你真的會?)用別的方式來做。這會觸 動到有關(guān)編碼風(fēng)格的問題,而大家從來就沒有停止過在這個問題上的爭論。在這里我只是給出我自己喜歡的做法(同時也給出這么做的原因):
i) 不要用一個 header 文件指向多個源碼文件(例外:程序包 的 header 文件)。用一個 header定義一個源碼文件的方式會更有效,也更容易查尋。否則改變一個源文件的結(jié)構(gòu)(并且 它的 header 文件)就必須重新編譯好幾個文件。
ii) 如果可以的話,完全可以用超過一個的 header 文件來指向同 一個源碼文件。有時將不可公開調(diào)用的函數(shù)原型,類型定義等等,從它們的C源碼文件中分離出來是非常有用的。使用一 個 header 文件裝公開符號,用另一個裝私人符號意味著如果你改變了這個源碼文件的內(nèi)部結(jié)構(gòu),你可以只是重新編譯它而 不需要重新編譯那些使用它的公開 header 文件的其它的源文 件。
iii) 不要在多個 header 文件中重復(fù)定義信息。 如果需要, 在其中一個 header 文件里 #include 另一個,但 是不要重復(fù)輸入相同的 header 信息兩次。原因是如果你 以后改 變了這個信息,你只需要把它改變一次,不用搜索并改變另外一 個重復(fù)的信 息。
iv) 在每一個源碼文件里, #include 那些聲明了源碼文件中的符 號的所有 header 文件。這樣一來,你在源碼文件和 header 文件對某些函數(shù)做出的矛盾聲明可以比較容易的被編譯器發(fā)現(xiàn)。
1.4 對于常見錯誤的注釋 a) 定義符 (Identifier) 在源碼文件中的矛盾:在C里,變量和函數(shù)的缺省狀態(tài)是公用的。因此,任何C源碼檔案都可以引用存在于其它源 碼檔中的通用 (global) 函數(shù)和通用變量,既使這個檔案沒有那個變量或函數(shù)的聲明或原型。因此你必須保證在不 同的兩個檔案里不能 用同一個符號名稱,否則會有連接錯誤或者在編譯時會有警告。一種避免這種錯誤的方法是在公用的符號前加上跟其所在源文件有 關(guān)的前綴。比如:
所有在 gfx.c 里的函數(shù)都加上前綴"gfx_"。如果 你很小心的分解你的程序,使用有 意義的函數(shù)名稱,并且不是過分使用通用變量,當(dāng)然這根本就不是問題。 要防止一個符號在它被定義的源文件以外被看到,可在它的定義前 加上關(guān)鍵字 "static"。這對只在一個檔案內(nèi)部使用,其它檔案都 都不會用到的簡單函數(shù)是很有用的。
b) 多次定義的符號: header 檔會被逐字的替換到你源文件里 #include 的位置的。 因此,如果 header 檔被 #include 到一個以上的源文件 里,這個 header 檔中所有 的定義就會出現(xiàn)在每一個有關(guān)的源碼文件里。這會使它們里的符號被定義一次以上, 從而出現(xiàn)連接錯誤(見 上)。
解決方法: 不要在 header 檔里定義變量。你只需要在 header 檔里聲明它們?nèi)缓笤?適當(dāng)?shù)模迷创a文件(應(yīng)該 #include 那個 header 檔的那個)里定義它們(一次)。對于初學(xué)者來說,定義和聲明是 很容易混淆的。聲明的作用是告訴編譯器其所聲明的符 號應(yīng)該存在,并且要有所指定的類型。但是,它并不會使編譯器分配貯存空間。 而定 義的做用是要求編譯器分配貯存空間。當(dāng)做一個聲明而不是做定義的時候,在聲明前放一個關(guān)鍵字"extern"。 例如,我們有一個叫"counter"的變量,如果想讓它成為公用的, 我們在一個源碼程序(只在一個里面)的開始定義它:"int counter;",再在相關(guān)的 header 檔里聲明 它: "extern int counter;"。 函數(shù)原型里隱含著 extern 的意思,所以不需顧慮這個問題。
c) 重復(fù)定義,重復(fù)聲明,矛盾類型:
請考慮如果在一個C源碼文件中 #include 兩個檔 a.h 和 b.h, 而 a.h 又 #include 了 b.h 檔(原因是 b.h 檔定義了一些 a.h 需要的類型),會發(fā)生什么事呢?這時該 C源碼文件 #include 了 b.h 兩次。因此每一個在 b.h 中的 #define 都發(fā)生了兩 次,每一 個聲明發(fā)生了兩次,等等。理論上,因為它們是完全一樣的拷貝, 所以應(yīng)該不會有什么問題,但在實際應(yīng)用上,這是不符合C的語法 的,可能在編譯時出現(xiàn)錯誤,或至少是警告。 解決的方法是要確定每一個 header 檔在任一個源碼文件中只被包 含了一次。我們一 般是用預(yù)處理器來達(dá)到這個目的的。當(dāng)我們進(jìn)入 每一個 header 檔時,我們?yōu)檫@個 header 檔 #define 一個巨集 指令。只有在這個巨集指令沒有被定義的前提下,我們 才真正使用 該 header 檔的主體。在實際應(yīng)用上,我們只要簡單的把下面一段 碼放在 每一個 header 檔的開始部分:
#ifndef FILENAME_H
#define FILENAME_H
然后把下面一行碼放在最后:
#endif
用 header 檔的檔名(大寫的)代替上面的 FILENAME_H,用底線 代替檔名中的點。有些人喜歡在 #endif 加上注釋來提醒他們這個 #endif 指的是什么。例如:
#endif /* #ifndef FILENAME_H */
我個人沒有這個習(xí)慣,因為這其實是很明顯的。當(dāng)然這只是各人的 風(fēng)格不同,無傷大雅。
你只需要在那些有編譯錯誤的 header 檔中加入這個技巧,但在所 有的 header 檔中都加入也沒什么損失,到底這是個好習(xí)慣。
1.5 重新編譯一個多文件項目 清楚的區(qū)別編譯和連接是很重要的。編譯器使用源碼文件來產(chǎn)生某種 形式的目標(biāo)文件 (object files)。在這個過程中,外部的符號參考并 沒有被解釋或替換。然后我們使用連接器來連接這些目標(biāo)文件和一些標(biāo)準(zhǔn)的程序包再加你指定的程序包,最后連接生 成一個可執(zhí)行程序。 在這個階段,一個目標(biāo)文件中對別的文件中的符號的參考被解釋,并報告不能被解釋的參考,一般是以錯誤信息的形式報告出來。 基本的步驟就應(yīng)該是,把你的源碼文件一個一個的編譯成目標(biāo)文件的格 式,最后把所有的目標(biāo)文件加上需要的程序包連接成一個可執(zhí)行文件。 具體怎么做是由你的編譯器 決定的。這里我只給出 gcc (GNU C 編譯器)的有關(guān)命令,這些有可能對你的非 gcc 編譯器也適用。
gcc 是一個多目標(biāo)的工具。它在需要的時候呼叫其它的元件(預(yù)處理 程序,編譯器,組合程序,連接器)。具體的哪些元件被呼叫取決于 輸入文件的類型和你傳遞給它的 開關(guān)。一般來說,如果你只給它C源碼文件,它將預(yù)處理,編譯,組合所有 的文件,然后把 所得的目標(biāo)文件連接成一個可執(zhí)行文件(一般生成的文件被命名為 a.out )。你當(dāng)然 可以這么做,但這會破壞很多我們 把一個項目分解成多個文件所得到的好處。 如果你給它一個 -c 開關(guān),gcc 只把給它的文件編譯成目標(biāo)文件, 用源碼文件的文件 名命名但把其后綴由".c" 或".cc"變成".o"。如果你給它的是一列目標(biāo)文件, gcc 會把它們連接成可執(zhí)行文件, 缺省文件名是 a.out 。你可以改變?nèi)笔∶瞄_ -o 后跟你指定 的文件名。因此,當(dāng)你改變了一個源碼文件后,你需要重新編譯它: 'gcc -c filename.c' 然后 重新連接你的項目: 'gcc -o exec_filename *.o'。 如果你改變了一個 header 檔, 你需要重新編譯所有 #include 過這個檔的源碼文件,你可以用 'gcc -c file1.c file2.c file3.c' 然后象上邊一樣連接。當(dāng)然這么做是很繁瑣的,幸虧我們有些工具使這個步驟變得簡單。 本文的第二部分就 是介紹其中的一件工具:GNU Make 工具。
(好家伙,現(xiàn)在才開始見真章。您學(xué)到點兒東西沒?)
2) GNU Make 工具
2.1 基本 makefile 結(jié)構(gòu) GNU Make 的主要工作是讀進(jìn)一個文本文件, makefile 。這個文 件里主要是有關(guān)哪些文件(‘target'目的文件)是從哪些別的 文件(‘dependencies'依靠文件)中產(chǎn) 生的,用什么命令來進(jìn)行這個產(chǎn)生過程。有了這些信息, make 會檢查磁碟上的文件,如果 目的文件的時間戳(該文件生成或被改動時的時間)比至少它的一個依靠文件舊的話, make 就執(zhí)行相應(yīng)的命令,以便更新目的文件。 (目的文件不一定是最后的可執(zhí)行檔,它可以是任何一個文件。) makefile 一般被叫做"makefile"或"Makefile"。當(dāng)然你可以 在 make 的命令行指 定別的文件名。如果你不特別指定,它會尋 找"makefile"或"Makefile",因此使用這兩個名字是最簡單 的。
一個 makefile 主要含有一系列的規(guī)則,如下:
例如,考慮以下的 makefile :
=== makefile 開始 ===
myprog : foo.o bar.o
gcc foo.o bar.o -o myprog
foo.o : foo.c foo.h bar.h
gcc -c foo.c -o foo.o
bar.o : bar.c bar.h
gcc -c bar.c -o bar.o
=== makefile 結(jié)束 ===
這是一個非常基本的 makefile —— make 從最上面開始,把上 面第一個目的, ‘myprog',做為它的主要目標(biāo)(一個它需要保證其總是最新的最終目標(biāo))。給出的 規(guī)則說明只要文件‘myprog' 比文件‘foo.o'或‘bar.o'中的任何一個舊,下一行的命令將會被執(zhí)行。但是,在檢查文件 foo.o 和 bar.o 的時間戳之前,它會往下查 找那些把 foo.o 或 bar.o 做為目標(biāo)文件的規(guī)則。它找到的關(guān)于 foo.o 的規(guī)則,該文件的依靠文件是 foo.c, foo.h 和 bar.h 。它從下面再找不到生成這些依靠文件的規(guī)則,它就開始檢 查磁碟 上這些依靠文件的時間戳。如果這些文件中任何一個的時間戳比 foo.o 的新, 命令 'gcc -o foo.o foo.c' 將會執(zhí)行,從而更新 文件 foo.o 。 接下來對文件 bar.o 做類似的檢查,依靠文件在這里是文件 bar.c 和 bar.h 。 現(xiàn)在, make 回到 ‘myprog'的規(guī)則。如果剛才兩個規(guī)則中的任何一個被執(zhí)行, myprog 就需要重建(因為其中一個 .o 檔就會比 ‘myprog'新),因此連接命令將被 執(zhí)行。
希望到此,你可以看出使用 make 工具來建立程序的好處——前 一章中所有繁瑣的檢查步驟都由 make 替你做了:檢查時間戳。 你的源碼文件里一個簡單改變都會造成那 個文件被重新編譯(因 為 .o 文件依靠 .c 文件),進(jìn)而可執(zhí)行文件被重新連接(因 為 .o 文件被改變了)。其實真正的得益是在當(dāng)你改變一個 header 檔的時候——你不再需要記住那個源碼文件依靠它,因為所有的 資料都在 makefile 里。 make 會很輕 松的替你重新編譯所有那 些因依靠這個 header 文件而改變了的源碼文件,如有需 要,再 進(jìn)行重新連接。 當(dāng)然,你要確定你在 makefile 中所寫的規(guī)則是正確無誤的,只列出那些在源碼文件 中被 #include 的 header 檔......
2.2 編寫 make 規(guī)則 (Rules) 最明顯的(也是最簡單的)編寫規(guī)則的方法是一個一個的查 看源碼文件,把它們的目標(biāo)文件做為目的,而C源碼文件和被它 #include 的 header 檔做為依靠文件。但是你 也要把其它被這些 header 檔 #include 的 header 檔也列為依靠文件,還有那些被包括的文件所包括的文件......然后你會發(fā)現(xiàn)要對越來越多的文件 進(jìn)行管理,然后你的頭發(fā)開始脫落,你的脾氣開始變壞,你的臉色變成菜色,你走在路上開始跟電線桿子 碰撞,終于你搗毀你的 電腦顯示器,停止編程。到低有沒有些容易點兒的方法呢?當(dāng)然有!向編譯器要!在編譯每一個源碼文件的時候,它實在應(yīng) 該知道應(yīng)該包括什么樣的 header 檔。使用 gcc 的時候,用 -M 開關(guān),它會為每一個你給它的C文件輸出一個規(guī)則,把目標(biāo)文件 做為目的,而這個C文件和所有應(yīng)該被 #include 的 header 文件將做為依靠文件。注意這個規(guī)則會加入所有 header 文件,包 括被角括號(`<', `>')和雙引號(`"')所包圍的文件。其實我們可以 相當(dāng)肯定系統(tǒng) header 檔(比如 stdio.h, stdlib.h 等等)不會 被我們更改,如果你用 -MM 來代替 -M 傳遞給 gcc, 那些用角括 號包圍的 header 檔將不會被包括。(這會節(jié)省一些編譯時間) 由 gcc 輸出的規(guī)則不會含有命令部分;你可以自己寫入你的命令 或者什么也不寫,而 讓 make 使用它的隱含的規(guī)則(參考下面的 2.4 節(jié))。
2.3 Makefile 變量 上面提到 makefiles 里主要包含一些規(guī)則。它們包含的其它的東 西是變量定義。 makefile 里的變量就像一個環(huán)境變量 (environment variable)。 事實上,環(huán)境變量在 make 過程中被解釋成 make 的變量。這些變量是大小寫敏感的,一般使用大寫字母。 它們可以從幾乎任何 地方被引用,也可以被用來做很多事情,比如:
i) 貯存一個文件名列表。在上面的例子里,生成可執(zhí)行文件的 規(guī)則包含一些目標(biāo)文件 名做為依靠。在這個規(guī)則的命令行 里同樣的那些文件被輸送給 gcc 做為命令參數(shù)。如果在這 里使用一個變數(shù)來貯存所有的目標(biāo)文件名,加入新的目標(biāo) 文件會變的簡單而且
較不易出錯。
ii) 貯存可執(zhí)行文件名。如果你的項目被用在一個非 gcc 的系 統(tǒng)里,或者如果你想使用一個不同的編譯器,你必須將所有使用編譯器的地方改成用新的編譯器名。但是如 果使用一 個變量來代替編譯器名,那么你只需要改變一個地方,其 它所有地方的命令名就都改變了。
iii) 貯存編譯器旗標(biāo)。假設(shè)你想給你所有的編譯命令傳遞一組 相同的選項(例 -Wall -O -g);如果你把這組選項存入一個變量,那么你可以把這個變量放在所有 呼叫編譯器 的地方。而當(dāng)你要改變選項的時候,你只需在一個地方改 變這個變量的內(nèi)容。要設(shè)定一個變量,你只要在一行的開始寫下這個變量的名字,后 面跟一個 = 號,后面 跟你要設(shè)定的這個變量的值。以后你要引用這個變量,寫一個 $ 符號,后面是圍在括 號里的變量名。比如在 下面,我們把前面的 makefile 利用變量重寫一遍:
=== makefile 開始 ===
OBJS = foo.o bar.o
CC = gcc
CFLAGS = -Wall -O -g
myprog : $(OBJS)
$(CC) $(OBJS) -o myprog
foo.o : foo.c foo.h bar.h
$(CC) $(CFLAGS) -c foo.c -o foo.o
bar.o : bar.c bar.h
$(CC) $(CFLAGS) -c bar.c -o bar.o
=== makefile 結(jié)束 ===
還有一些設(shè)定好的內(nèi)部變量,它們根據(jù)每一個規(guī)則內(nèi)容定義。三個 比較有用的變量是$@, $< 和 $^ (這些變量不需要括號括住)。 $@ 擴(kuò)展成當(dāng)前規(guī)則的目的文件名, $< 擴(kuò)展成依靠列表中的第 一個依靠文件,而 $^ 擴(kuò)展成整個依靠的列表(除掉了里面所有重復(fù)的文件名)。利用這些變量,我們可以把上面的 makefile 寫成:
=== makefile 開始 ===
OBJS = foo.o bar.o
CC = gcc
CFLAGS = -Wall -O -g
myprog : $(OBJS)
$(CC) $^ -o $@
foo.o : foo.c foo.h bar.h
$(CC) $(CFLAGS) -c $< -o $@
bar.o : bar.c bar.h
$(CC) $(CFLAGS) -c $< -o $@
=== makefile 結(jié)束 ===
你可以用變量做許多其它的事情,特別是當(dāng)你把它們和函數(shù)混合 使用的時候。如果需 要更進(jìn)一步的了解,請參考 GNU Make 手冊。 ('man make', 'man makefile')
2.4 隱含規(guī)則 (Implicit Rules) 請注意,在上面的例子里,幾個產(chǎn)生 .o 文件的命令都是一樣的。 都是從 .c 文件和 相關(guān)文件里產(chǎn)生 .o 文件,這是一個標(biāo)準(zhǔn)的步驟。其實 make 已經(jīng)知道怎么做——它 有一些叫做隱含規(guī)則的內(nèi) 置的規(guī)則,這些規(guī)則告訴它當(dāng)你沒有給出某些命令的時候, 應(yīng)該怎么辦。如果你把生成 foo.o 和 bar.o 的命令從它們的規(guī)則中刪除, make 將會查找它的隱含規(guī)則,然后會找到一個適當(dāng)?shù)拿睢K拿顣?使用一些變量,因此你可以按照你的 想法來設(shè)定它:它使用變量 CC 做為編譯器(象我們在前面的例子),并且傳遞變量 CFLAGS (給 C 編譯器,C++ 編譯器用 CXXFLAGS ),CPPFLAGS ( C 預(yù) 處理器旗 標(biāo)), TARGET_ARCH (現(xiàn)在不用考慮這個),然后它加 入旗標(biāo) '-c' ,后面跟變量 $< (第一個依靠名),然后是旗 標(biāo) '-o' 跟變量 $@ (目的文件名)。
一個C編譯的 具體命令將 會是:$(CC) $(CFLAGS) $(CPPFLAGS) $ (TARGET_ARCH) -c $< -o $@ 當(dāng)然你可以按照你自己的需要來定義這些變量。這就是為什么用 gcc 的 -M 或 - MM 開 關(guān)輸出的碼可以直接用在一個 makefile 里。
2.5 假象目的 (Phony Targets)
假設(shè)你的一個項目最后需要產(chǎn)生兩個可執(zhí)行文件。你的主要目標(biāo)是產(chǎn)生兩個可執(zhí)行文 件,但這兩個文件是相互獨立的——如果一 個文件需要重建,并不影響另一個。你可 以使用"假象目的"來 達(dá)到這種效果。一個假象目的跟一個正常的目的幾乎是一樣 的, 只是這個目的文件是不存在的。因此, make 總是會假設(shè)它需要 被生成,當(dāng)把它的依賴文件更新后,就會執(zhí)行它的規(guī)則里的命令 行。 如果在我們的 makefile 開始處輸入:all : exec1 exec2 其中 exec1 和 exec2 是我們做為目的的兩個可執(zhí)行文件。 make 把這個 'all' 做為 它的主要目的,每次執(zhí)行時都會嘗試把 'all' 更新。但既然這行規(guī)則里沒有哪個命令 來作用在一個叫 'all' 的 實際文件(事實上 all 并不會在磁碟上實際產(chǎn)生),所以 這個規(guī) 則并不真的改變 'all' 的狀態(tài)。可既然這個文件并不存在,所以 make 會嘗試 更新 all 規(guī)則,因此就檢查它的依靠 exec1, exec2 是否需要更新,如果需要,就把它們更新,從而達(dá)到我們的目的。 假象目的也可以用來描述一組非預(yù)設(shè)的動作。例如,你想把所有由 make 產(chǎn)生的文件刪 除,你可以在 makefile 里設(shè)立這樣一個規(guī)則:
veryclean :
rm *.o
rm myprog
前提是沒有其它的規(guī)則依靠這個 'veryclean' 目的,它將永遠(yuǎn) 不會被執(zhí)行。但是,如果你明確的使用命令 'make veryclean' , make 會把這個目的做為它的主要目標(biāo),執(zhí)行那些 rm 命令。如果你的磁碟上存在一個叫 veryclean 文件,會發(fā)生什么事?這 時因為在這個規(guī)則里 沒有任何依靠文件,所以這個目的文件一定是 最新的了(所有的依靠文件都已經(jīng)是最新的了),所以既使用戶明 確命令 make 重新產(chǎn)生它,也不會有任何事情發(fā)生。解決 方法是標(biāo) 明所有的假象目的(用 .PHONY),這就告訴 make 不用檢查它們 是否存在 于磁碟上,也不用查找任何隱含規(guī)則,直接假設(shè)指定的目 的需要被更新。在 makefile 里加入下面這行包含上面規(guī)則的規(guī)則:
..PHONY : veryclean
就可以了。注意,這是一個特殊的 make 規(guī)則,make 知道 .PHONY 是一個特殊目的, 當(dāng)然你可以在它的依靠里加入你想用的任何假象 目的,而 make 知道它們都是假象目的。
2.6 函數(shù) (Functions) makefile 里的函數(shù)跟它的變量很相似——使用的時候,你用一個 $ 符號跟開括號,函 數(shù)名,空格后跟一列由逗號分隔的參數(shù),最后用關(guān)括號結(jié)束。例如,在 GNU Make 里 有一個叫 'wildcard' 的函 數(shù),它有一個參數(shù),功能是展開成一列所有符合由其參數(shù)描述的文 件名,文件間以空格間隔。你可以像下面所示使用這個命令:
SOURCES = $(wildcard *.c)
這行會產(chǎn)生一個所有以 '.c' 結(jié)尾的文件的列表,然后存入變量 SOURCES 里。當(dāng)然你不需要一定要把結(jié)果存入一個變量。
另一個有用的函數(shù)是 patsubst ( patten substitude, 匹配替 換的縮寫)函數(shù)。它需要3個參數(shù)——第一個是一個需要匹配的 式樣,第二個表示用什么來替換它,第三 個是一個需要被處理的由空格分隔的字列。例如,處理那個經(jīng)過上面定義后的變量,
OBJS = $(patsubst %.c,%.o,$(SOURCES))
這行將處理所有在 SOURCES 字列中的字(一列文件名),如果它的 結(jié)尾是 '.c' ,就 用 '.o' 把 '.c' 取代。注意這里的 % 符號將匹 配一個或多個字符,而它每次所匹配 的字串叫做一個‘柄'(stem) 。 在第二個參數(shù)里, % 被解讀成用第一參數(shù)所匹配的 那個柄。
2.7 一個比較有效的 makefile 利用我們現(xiàn)在所學(xué)的,我們可以建立一個相當(dāng)有效的 makefile 。 這個 makefile 可 以完成大部分我們需要的依靠檢查,不用做太大 的改變就可直接用在大多數(shù)的項目里。
首先我們需要一個基本的 makefile 來建我們的程序。我們可以讓 它搜索當(dāng)前目錄,找到源碼文件,并且假設(shè)它們都是屬于我們的項目的,放進(jìn)一個叫 SOURCES 的變量。 這里如果也包含所有的 *.cc 文件,也許會更保險,因為源碼文件可能是 C++ 碼的。 SOURCES = $ (wildcard *.c *.cc) 利用 patsubst ,我們可以由源碼文件名產(chǎn)生目標(biāo)文件名,我們需要編譯出這些目標(biāo) 文件。如果我們的源碼文件既有 .c 文件,也有 .cc 文件,我們需要使用相嵌的 patsubst 函數(shù)呼叫:
OBJS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))
最里面一層 patsubst 的呼叫會對 .cc 文件進(jìn)行后綴替代,產(chǎn)生的結(jié) 果被外層的 patsubst 呼叫處理,進(jìn)行對 .c 文件后綴的替代。
現(xiàn)在我們可以設(shè)立一個規(guī)則來建可執(zhí)行文件:
myprog : $(OBJS)
gcc -o myprog $(OBJS)
進(jìn)一步的規(guī)則不一定需要, gcc 已經(jīng)知道怎么去生成目標(biāo)文件 (object files) 。下面我們可以設(shè)定產(chǎn)生依靠信息的規(guī)則:
depends : $(SOURCES)
gcc -M $(SOURCES) > depends
在這里如果一個叫 'depends' 的文件不存在,或任何一個源碼文件 比一個已存在的 depends 文件新,那么一個 depends 文件會被生 成。depends 文件將會含有由 gcc 產(chǎn)生的關(guān)于源碼文件的規(guī)則(注 意 -M 開關(guān))。現(xiàn)在我們要讓 make 把這些規(guī)則當(dāng)做 makefile 檔 的一部分。這里使用的技巧很像 C 語言中的 #include 系統(tǒng)——我 們要 求 make 把這個文件 include 到 makefile 里,如下:
include depends
GNU Make 看到這個,檢查 'depends' 目的是否更新了,如果沒有, 它用我們給它的命令重新產(chǎn)生 depends 檔。然后它會把這組(新) 規(guī)則包含進(jìn)來,繼續(xù)處理最終目標(biāo) 'myprog' 。當(dāng)看到有關(guān) myprog 的規(guī)則,它會檢查所有的目標(biāo)文件是否更新——利用 depends 文件 里的規(guī)則,當(dāng)然這些規(guī)則現(xiàn)在已經(jīng)是更新過的了。
這個系統(tǒng)其實效率很低,因為每當(dāng)一個源碼文件被改動,所有的源碼 文件都要被預(yù)處 理以產(chǎn)生一個新的 'depends' 文件。而且它也不是 100% 的安全,這是因為當(dāng)一個 header 檔被改動,依靠信息并不會 被更新。但就基本工作來說,它也算相當(dāng)有用的了。
2.8 一個更好的 makefile 這是一個我為我大多數(shù)項目設(shè)計的 makefile 。它應(yīng)該可以不需要修 改的用在大部分項目里。我主要把它用在 djgpp 上,那是一個 DOS 版的 gcc 編譯器。因此你可以看到執(zhí)行的命令名、 'alleg' 程序包、 和 RM -F 變量都反映了這一點。
=== makefile 開始 ===
######################################
#
# Generic makefile
#
# by George Foot
# email: george.foot@merton.ox.ac.uk
#
# Copyright (c) 1997 George Foot
# All rights reserved.
# 保留所有版權(quán)
#
# No warranty, no liability;
# you use this at your own risk.
# 沒保險,不負(fù)責(zé)
# 你要用這個,你自己擔(dān)風(fēng)險
#
# You are free to modify and
# distribute this without giving
# credit to the original author.
# 你可以隨便更改和散發(fā)這個文件
# 而不需要給原作者什么榮譽。
# (你好意思?)
#
######################################
### Customising
# 用戶設(shè)定
#
# Adjust the following if necessary; EXECUTABLE is the target
# executable's filename, and LIBS is a list of libraries to link in
# (e.g. alleg, stdcx, iostr, etc). You can override these on make's
# command line of course, if you prefer to do it that way.
#
# 如果需要,調(diào)整下面的東西。 EXECUTABLE 是目標(biāo)的可執(zhí)行文件名, LIBS
# 是一個需要連接的程序包列表(例如 alleg, stdcx, iostr 等等)。當(dāng)然你
# 可以在 make 的命令行覆蓋它們,你愿意就沒問題。
#
EXECUTABLE := mushroom.exe
LIBS := alleg
# Now alter any implicit rules' variables if you like, e.g.:
#
# 現(xiàn)在來改變?nèi)魏文阆敫膭拥碾[含規(guī)則中的變量,例如
CFLAGS := -g -Wall -O3 -m486
CXXFLAGS := $(CFLAGS)
# The next bit checks to see whether rm is in your djgpp bin
# directory; if not it uses del instead, but this can cause (harmless)
# `File not found' error messages. If you are not using DOS at all,
# set the variable to something which will unquestioningly remove
# files.
#
# 下面先檢查你的 djgpp 命令目錄下有沒有 rm 命令,如果沒有,我們使用 del 命令來代替,但有可能給我們 'File not found' 這個錯誤信息,這沒 # 什么大礙。如果你不是用 DOS ,把它設(shè)定成一個刪文件而不廢話的命令。 (其實這一步在 UNIX 類的系統(tǒng)上是多余的,只是方便 DOS 用戶。 UNIX 用戶可以刪除這5行命令。)
ifneq ($(wildcard $(DJDIR)/bin/rm.exe),)
RM-F := rm -f
else
RM-F := del
endif
# You shouldn't need to change anything below this point.
#
# 從這里開始,你應(yīng)該不需要改動任何東西。(我是不太相信,太NB了!)
SOURCE := $(wildcard *.c) $(wildcard *.cc)
OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
$(patsubst %.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS += -MD
..PHONY : everything deps objs clean veryclean rebuild
everything : $(EXECUTABLE)
deps : $(DEPS)
objs : $(OBJS)
clean :
@$(RM-F) *.o
@$(RM-F) *.d
veryclean: clean
@$(RM-F) $(EXECUTABLE)
rebuild: veryclean everything
ifneq ($(MISSING_DEPS),)
$(MISSING_DEPS) :
@$(RM-F) $(patsubst %.d,%.o,$@)
endif
-include $(DEPS)
$(EXECUTABLE) : $(OBJS)
gcc -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))
=== makefile 結(jié)束 ===
有幾個地方值得解釋一下的。首先,我在定義大部分變量的時候使 用的是 := 而不是
= 符號。它的作用是立即把定義中參考到的函 數(shù)和變量都展開了。如果使用 = 的話,
函數(shù)和變量參考會留在那 兒,就是說改變一個變量的值會導(dǎo)致其它變量的值也被改
變。例 如:
A = foo
B = $(A)
# 現(xiàn)在 B 是 $(A) ,而 $(A) 是 'foo' 。
A = bar
# 現(xiàn)在 B 仍然是 $(A) ,但它的值已隨著變成 'bar' 了。
B := $(A)
# 現(xiàn)在 B 的值是 'bar' 。
A = foo
# B 的值仍然是 'bar' 。
make 會忽略在 # 符號后面直到那一行結(jié)束的所有文字。
ifneg...else...endif 系統(tǒng)是 makefile 里讓某一部分碼有條件的 失效/有效的工具。 ifeq 使用兩個參數(shù),如果它們相同,它把直 到 else (或者 endif ,如果沒有 else 的話)的一段碼加進(jìn) makefile 里;如果不同,把 else 到 endif 間的一段碼加入 makefile (如果有 else )。 ifneq 的用法剛好相反。 'filter-out' 函數(shù)使用兩個用空格分開的列表,它把第二列表中所有的存在于第一列 表中的項目刪除。我用它來處理 DEPS 列表,把所 有已經(jīng)存在的項目都刪除,而只保留缺少的那些。
我前面說過, CPPFLAGS 存有用于隱含規(guī)則中傳給預(yù)處理器的一些 旗標(biāo)。而 -MD 開關(guān) 類似 -M 開關(guān),但是從源碼文件 .c 或 .cc 中 形成的文件名是使用后綴 .d 的(這就 解釋了我形成 DEPS 變量的 步驟)。DEPS 里提到的文件后來用 '-include' 加進(jìn)了 makefile 里,它隱藏了所有因文件不存在而產(chǎn)生的錯誤信息。 如果任何依靠文件不存在, makefile 會把相應(yīng)的 .o 文件從磁碟 上刪除,從而使得 make 重建它。因為 CPPFLAGS 指定了 -MD , 它的 .d 文件也被重新產(chǎn)生。 最后, 'addprefix' 函數(shù)把第二個參數(shù)列表的每一項前綴上第一 個參數(shù)值。 這個 makefile 的那些目的是(這些目的可以傳給 make 的命令行 來直接選用):
everything:(預(yù)設(shè)) 更新主要的可執(zhí)行程序,并且為每一個 源碼文件生成或更新一個 '.d' 文件和一個 '.o' 文件。 deps: 只是為每一個源碼程序產(chǎn)生或更新一個 '.d' 文件。 objs: 為每一個源碼程序生成或更新 '.d' 文件和目標(biāo)文件。 clean: 刪除所有中介/依靠文件( *.d 和 *.o )。
veryclean: 做 `clean' 和刪除可執(zhí)行文件。
rebuild: 先做 `veryclean' 然后 `everything' ;既完全重建。
除了預(yù)設(shè)的 everything 以外,這里頭只有 clean , veryclean , 和 rebuild 對用戶是有意義的。我還沒有發(fā)現(xiàn)當(dāng)給出一個源碼文件的目錄,這個 makefile 會失敗的 情況,除非依靠文件被弄亂。如果這種弄亂的情況發(fā)生了,只要輸入 `make clean' ,所有的目標(biāo)文件和依靠文件會被刪除,問題就應(yīng)該被解決了。當(dāng)然,最好不要把它們弄亂。如果你發(fā)現(xiàn)在某種情況下這 個 makefile 文件不能完成它的工作,請告訴我,我會把它整好的。
3 總結(jié) 我希望這篇文章足夠詳細(xì)的解釋了多文件項目是怎么運作的,也說明了 怎樣安全而合理的使用它。到此,你應(yīng)該可以輕松的利用 GNU Make 工具來管理小型的項目,如果 你完全理解了后面幾個部分的話,這些對于 你來說應(yīng)該沒什么困難。 GNU Make 是一件強(qiáng)大的工具,雖然它主要是用來建立程序,它還有很多 別的用處。如果想要知道更多有關(guān)這個工具的知識,它的句法,函數(shù),和許多別的特點,你應(yīng)該參
看它的參考文件 (info pages, 別的 GNU 工具也一樣,看它們的 info pages. 。
【發(fā)表回復(fù)】【查看CU論壇原帖】【關(guān)閉】
xiaowindy 回復(fù)于:2002-09-18 16:37:03
我的makefile如下:
EXECUTABLE := run_exe
LIB_TAG := ../lib/libsgip.a
LIBS := pthread
INSTALL := mv $(LIB_TAG) ../lib
# Now alter any implicit rules' variables if you like, e.g.:
#
# 現(xiàn)在來改變?nèi)魏文阆敫膭拥碾[含規(guī)則中的變量,例如
CFLAGS := -g -Wall -O3 -I../include
CXXFLAGS := $(CFLAGS)
# The next bit checks to see whether rm is in your djgpp bin
# directory; if not it uses del instead, but this can cause (harmless)
# `File not found' error messages. If you are not using DOS at all,
# set the variable to something which will unquestioningly remove
# files.
#
# 下面先檢查你的 djgpp 命令目錄下有沒有 rm 命令,如果沒有,我們使用
# del 命令來代替,但有可能給我們 'File not found' 這個錯誤信息,這沒
# 什么大礙。如果你不是用 DOS ,把它設(shè)定成一個刪文件而不廢話的命令。
RM:= rm -f
# You shouldn't need to change anything below this point.
#
# 從這里開始,你應(yīng)該不需要改動任何東西。(我是不太相信,太NB了!)
CC := g++
SOURCE := $(wildcard *.c) $(wildcard *.cc)
OBJS := $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCE)))
DEPS := $(patsubst %.o,%.d,$(OBJS))
MISSING_DEPS := $(filter-out $(wildcard $(DEPS)),$(DEPS))
MISSING_DEPS_SOURCES := $(wildcard $(patsubst %.d,%.c,$(MISSING_DEPS)) \
$(patsubst %.d,%.cc,$(MISSING_DEPS)))
CPPFLAGS += -MD
.PHONY : everything deps objs clean veryclean rebuild
everything : $(EXECUTABLE)
deps : $(DEPS)
objs : $(OBJS)
libs : $(OBJS)
ar cru $(LIB_TAG) $(OBJS)
install :
@$(INSTALL)
clean :
@$(RM) *.o
@$(RM) *.d
@$(RM) $(LIB_TAG)
veryclean: clean
@$(RM) $(EXECUTABLE)
rebuild: veryclean everything
ifneq ($(MISSING_DEPS),)
$(MISSING_DEPS) :
@$(RM) $(patsubst %.d,%.o,$@)
endif
-include $(DEPS)
$(EXECUTABLE) : $(OBJS)
g++ -o $(EXECUTABLE) $(OBJS) $(addprefix -l,$(LIBS))
當(dāng)我在linux7.3中間執(zhí)行make的時候,出現(xiàn)錯誤提示如下:
/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o: In function `_start':
/usr/lib/gcc-lib/i386-redhat-linux/2.96/../../../crt1.o(.text+0x1: undefined r
eference to `main'
collect2: ld returned 1 exit status
您能告訴原因嗎?謝謝您,我真的很著急
shine1977 回復(fù)于:2002-11-16 21:32:45
以上怎么沒有obj文件對源文件的依賴關(guān)系的規(guī)則??
HopeCao 回復(fù)于:2002-11-18 10:56:25
這是beaver的一個makefile,我覺得比上面的要比較容易理解一些:
能夠適用windows及l(fā)inux/unix!!!
##
## Makefile for Beaver
##
## Author: Marc Bevand (aka "After"
## Last update: Mon Jul 15 22:29:51 CEST 2002
##
############## Public section. Modify according to your needs. ###############
CC = gcc
RM = rm -f
CP = cp
DESTDIR = /usr/local
IPATH =
LPATH =
OPTI = -O3 -funroll-loops -fomit-frame-pointer #-mcpu=i686
DBUG = #-ggdb #-pg
WARN= #-W -Wall #-pedantic -ansi
### Unix version ###
CFLAGS = ${OPTI} ${DBUG} ${WARN} ${IPATH} `gtk-config --cflags`
LDFLAGS = ${OPTI} ${DBUG} ${WARN} ${LPATH} `gtk-config --libs`
NAME = beaver
### Windows version ###
#CFLAGS = ${OPTI} ${DBUG} ${WARN} ${IPATH}\
# -fnative-struct -mwindows
#LDFLAGS = ${OPTI} ${DBUG} ${WARN} ${LPATH}\
# -lgdk-1.3 -lgtk-1.3 -lgmodule-1.3 -lglib-1.3 -lm
#NAME = beaver.exe
###################### Private section. Do not modify ########################
ifndef DBUG
STRIP = strip --strip-all ${NAME}
endif
SRC= main.c search.c tools.c languages.c editor.c prefs.c conf.c msgbar.c\
toolbar.c interface.c filesops.c completion.c undoredo.c
INC= ${SRC:.c=.h} struct.h wordfile.h
OBJ= ${SRC:.c=.o}
all: ${OBJ}
${CC} -o ${NAME} ${OBJ} ${LDFLAGS}
@${STRIP}
.c.o:
${CC} ${CFLAGS} -c $< -o $@
${OBJ}: ${INC}
install :
install -d $(DESTDIR)/bin
install beaver $(DESTDIR)/bin
install -d $(DESTDIR)/share/beaver/bl
$(CP) ../bl/* $(DESTDIR)/share/beaver/bl
install -d $(DESTDIR)/share/pixmaps
$(CP) ../pixmaps/beaver.png $(DESTDIR)/share/pixmaps
gzip -c9 ../beaver.1x > ../beaver.1x.gz
install -d $(DESTDIR)/man/man1
$(CP) ../beaver.1x.gz $(DESTDIR)/man/man1
@echo
@echo "Files installed :"
@echo "-----------------"
@echo
@echo "$(DESTDIR)/bin/beaver"
@echo "$(DESTDIR)/share/beaver/bl/example1.bl"
@echo "$(DESTDIR)/share/beaver/bl/example2.bl"
@echo "$(DESTDIR)/share/beaver/bl/glib.bl"
@echo "$(DESTDIR)/share/pixmaps/beaver.png"
@echo "$(DESTDIR)/man/man1/beaver.1x.gz"
@echo
@echo ",----------------------------------."
@echo "| Beaver succesfully installed ^_^ |"
@echo "\`----------------------------------'"
.PHONY: clean re
clean:
${RM} *~ \#* .\#* *.core gmon.out ../beaver.1x.gz ${OBJ} ${NAME}
re: clean all
posted on 2008-07-01 16:17
ronliu 閱讀(839)
評論(0) 編輯 收藏 引用