原文
http://pl-learning-blog.logdown.com/posts/1049271-usually-terror-words-o-muhammad-c-ch11-reading-notes-unfinished
11.1 替換file的內容 (include)
第五章,接觸了include這個preprocessor的命令。這個命令是說在#include < file名稱 >的情況下,從MinGW底下的include或/usr/include資料夾下找到同名檔案並置換掉這一行。比如說,我們寫了
這一行,那麼就會被換成以下東西:
在稍微詳細說明一下include命令吧。file名稱通常是指定一種叫head file的file。head file一般來說會有data type、常數、還有函數的定義。用<>括起來的東西,會在MinGW底下的/include或/usr/include資料夾這兩個標準C語言library的head file放置處,尋找相同名稱的檔案。
如果想自己寫很多函數的話,那麼就會想要一個專為那群函數所用的head file。在這種時候,用""
把自己寫的head file名稱括起來。這樣就會在跟source code一樣的目錄中尋找。首先來把範例程式build起來吧。
source code
main.c
source code
function.h
source code
function.c
compile時要下三條指令
這樣會生出一個名為include的執行檔。如果是makefile的話,就會寫成這樣:
因為這次是自己定義的head file實驗,所以試試了「分割compile」。來解釋一下compile那三行命令的意義吧。第一行是compile main.c,assemble後生成main.o; 第二行是compile functions.c,assemble後生成functions.o; 第三行是把main.o跟functions.o一起跟標準C library link起來,生成include這個執行檔。
雖然有點繞遠路,但讓我們來思考一下為何分割compile還有自己定義的head file會很重要吧。在第十章時自己寫函數的時候,說明了一定要宣告prototype。為了分割compile,我們把程式分成好幾個file去寫,在functions.c裡追加了新的函數; 這樣的話,就不需要在main.c中追加新的prototype宣告。
還有一點,就是main.c跟function.c的開發者是不同人的情況。假如我們一開始是把prototype宣告寫在main.c,而且寫function.c的人把函數的宣告稍微改了一下,但寫main.c的人不知道這件事所以沒改prototype。在這種情況下,就算compile跟link都沒異常的結束了,在執行時也可能會出現錯誤。
要避免這種事,寫function.c的人也要同時負責寫宣告prototype的functions.h,而寫main.c的人只要include functions.h就可以。這樣的話,如果function.c有變更的話compile時就會出現警告error,寫main.c的人注意到就可以修正用到函數的地方。
還有,一人在開發的時候,如果想讓別的程式也使用function.c的話,只要複製那個file(?)跟functions.h,讓別的程式去include,就可以使用function.c的函數。
因為這樣的理由,所以我們在把程式分成好幾個檔案去寫並分割compile時,我們也會分開去定義head file。標準C library的函數的定義都被整理在head file裡面,也是這個理由所造成的。比如說,如果沒有標準C library的話,想要利用到printf()或fgets()函數時,每一次都要自己在程式開頭宣告prototype,如下圖:
每次都要寫這些,很麻煩吧,但是像下面這樣寫就輕鬆多了:
下面是include這個執行檔的執行畫面:
執行結果:
就和預想的一樣吧。那麼,我們輸入以下指令,來看看main.c中#include "function.h"是怎麼置換的吧:
gcc -E main.c > main.txt
可以發現輸出的txt檔有以下訊息:
順代一提,include這個命令除了head file外,還可以用在讀取的置換上。就當做是玩玩把下面的範例build看看吧。
source code
a.c
a.1
a.2
a.3
a.4
a.5
只要打
再打
就會執行經典的hello, world程式了
執行結果:
只要是文字文件什麼都可以include,雖然絕對不會像上面這樣用就是了,不過刻意這樣做可以讓大家有更強的「include就是文字置換」的感覺。
11.2 一對一置換 (define)
還有一個跟include類似的define命令。不過跟include置換檔案不同,define是可以置換source code中的內容。用法如下:
第九章求出質數的peime_3.c的範例程式,之中的end = 100;代表我們可以求到100為止的質數。可以用define來改寫成下面這樣:
在preprocess的時候,MAX這字串就會因為define的命令通通換成100。趕快來實驗看看吧。
source code
define.c
執行結果:
如何呢? 應該能了解就是單純的文字替換吧。
11.3 把define的東西無效化 (undef)
一但寫了define的命令,就對宣告那一行之後的程式碼就一直都有效。如果因為什麼理由而想把define無效化的話,可以用undef。使用方法很簡單如下:
範例:
可以對剛剛的define.c實驗看看,在main()之前寫上這一行,int a = INTEGER_NUM_1;這一行就會出現錯誤。
11.4 有條件的compile (if)
這個define,還有另一個使用方法; 而搭配這個使用方法的是if。
根據值的內容做有條件的compile (if)
使用if的話,會根據被define的值讓source code本身分歧(這時還沒被compile)。非常難懂,所以來實驗一下下面的範例程式吧。
source code
define_2.c
我們先打下面這一行:
gcc -E define_2.c > define_2.txt
再來看看define_2.txt的內容,會發現最底下有main()函數的內容。那麼,我們把#define TEST 1改成#define TEST 0實驗看看吧。我們會發現,define_2.txt的內容如下,幾乎是空的:
原因的話,在#define TEST 1的時候,符合#if TEST == 1的條件,所以會去用到#if~#else的範圍,但是當#define TEST 0的時候因為和if條件不符,所以會用到#else~#endif的範圍,也就是空的。
這次,我們把#define TEST 1這一行給刪掉,分別下這兩行命令:
gcc -E -DTEST=1 define_2.c > define_2.txt
gcc -E -DTEST=0 define_2.c > define_2.txt
這兩行命令所產生的define_2.txt內容不同,-DTEST=1是當作有main()函數,-DTEST=0則是沒有。像這樣,define的內容也可以透過preprocessor的option指定。這個機能非常特別,C語言以外很少看到。比如說程式中分別寫了Windows用還有Linux用兩種,可以使用Makefile的option來對應兩方的環境。為了這種用途,define在沒有寫該置換什麼的話,那麼就會自動補1,也就是說,
跟
是相同的。
調查是否已經定義過的條件compile
就算不用一個一個的寫if後面的條件,用ifdef這命令的話就跟#if TEST == 1是同樣的效果,反過來也有跟#if TEST == 0一樣意思的ifndef可以用。範例如下:
(筆者注: 筆者不知道這樣寫有什麼意義,因為不管TEST是1是0都是執行一樣的程式,再說,#define跑到哪了呢?)
已經定義的define
gcc的命令已經在內部define了許多的字串。比如說linux的gcc,就算gcc什麼option都不加,也在preprocess的時候#define linux 1。也就是說,如果想寫一個可以跨平台(linux,windows等不同作業系統)的程式,可以像下面這樣寫:
如果打了以下命令
就可以列出linux已經#define好的字串,筆者電腦顯示如下:
如果想寫可以在複數環境下執行的程式,利用這命令,可以利用已經定義好的值來寫程式。
恐怖實驗:如果已經define的字串跟變數名相同
利用像是#define linux 1等等的話,在寫需要在複數環境下執行的程式時非常便利,但是其中有陷阱。下面的範例程式,會發生什麼事呢?
source code
define_3.c
在compile時會出現以下的警告跟錯誤信息
這是因為系統已經#define linux 1了,所以對我們的程式碼做置換,變成了
所以就產生奇怪的結果。
在gcc裡定義的大部分#define都有__(兩個underscore),所以不太發生這種事,但還是注意一下。除了linux可能會不小心跟#define重複到,其他還有i386,unix也是跟linux同樣的情形。
11.5 像函數的macro函數
define是可以把接收的參數置換的命令,也就是說,可以用它做出類似函數的東西,這被稱為「macro函數」。
macro函數的寫法
比如說,想寫出跟function.c的sum()一樣的macro函數,就要寫成下面這樣:
先來看看範例程式的執行吧。
source code
define_4.c
執行結果:
跟預想一樣的結果吧。
macro函數不是函數!
這個macro函數,有著「函數名(參數)」這樣的型態,怎麼看都是函數。但是,實際上函數跟macro函數是完全不同的東西。要說的話,因為macro函數跟其它我們看過的define一樣,不過是單純的文字置換而已。
我們把define_4.c的範例程式編譯時gcc加上一些option觀察一下吧。
gcc -E define_4.c > define_4.txt
可以看到原本程式碼中的
在define_4.txt中的最後,變成了下面這樣子:
可以發現SUM(num_1, num_2);的行蹤完全消失,變成了num_1 + num_2;,其他式子也是如此。
恐怖實驗: macro函數的陷阱
因為macro函數的便利,在C語言裡常常被利用。雖然之前的SUM()不怎麼實用所以不太可能會為它去寫macro函數; 不過因為是文字置換,不需要在意data type,所以我們會把兩個變數a, b的交換寫成像下面的macro函數:
但是,macro函數是有陷阱的。首先來執行看看可以正確動作的的範例程式吧。
source code:
define_5.c
執行結果
變數num_1跟變數num_2確實交換了。接下來,我們使用if,使得只有在num_1等於0的時候進行交換,先故意不用block吧,就像下面:
執行結果
執行結果很奇怪吧。原本範例程式中的範例程式變數num_1是1才對,macro函數SWAP()應該沒有實行,而且還置換了奇怪的數字。我們用gcc -E
來調查原因吧。輸入:
gcc -E define_5.c > define_5.txt
在define_5.txt中,可以發現SWAP()的函數被置換變成下面這樣子:
因為沒有{},所以if的分歧處理就只有num_1^=num_2這一句; 要用{}解釋上面的code的話,就是下面這樣:
所以才會出現奇怪的結果。原本在if的下面block的話就沒問題了,共同作業的人也不一定會在if加上block,而且就算弄錯也不會出現警告error,我們希望可以讓SWAP()函數本身可以應對不加block的問題。利用置換文字的特性,寫法如下:
這樣寫的話,就可以依照我們的想法執行了。
順代一提,macro函數也可以寫出分歧這種複雜的處理。在這情況下,可能會因為使用的制御文而造成compile error,可以使用像下列的不重複的do while寫法。
這裡會寫成(a)還有(b)這樣把變數用括號括起來,是為了不要發生優先順位的問題。\是代表換行後define依然繼續的意思。還有,同常我們呼叫完函數後一定會加個;不過while(0)後面是不需要加的。
這些不是現在立刻要用到的知識,不過記起來也沒什麼損失。
11.6 結語
在這章,我們學習了C語言特有的preprocessor命令。
雖然是其他大部分的語言都沒有的機能所以不好懂,但是在gcc -E的命令下看到是怎樣置換的,應該會明白到這意外的簡單。
stdio.h等等的head file中有許多被define定義的常數,而為了讓UNIX系的OS可以像collection一樣利用list或quene的head file,其機能也幾乎都是用macro來實現的(為了使用時可以不受dat type影響)。
就算是說學習了preprocessor命令,能做的事也不會戲劇性的增加,不過C語言到處都會用到,所以記起來吧。
留言列表