“能否用痰盂盛飯”並非是一個技術問題,而是一個哲學問題。
哲學問題沒有標准答案,只存在不同的選擇。
有一種觀點認為,痰盂可以盛飯。理由是只要不漏能把飯吃到嘴里就行。我看這個理由任何人都無法反駁。
另有一種觀點認為,痰盂是用來吐痰的,不可以用來盛飯。他們覺得用痰盂盛飯是一種不可理喻的行為。然而這種看法可能會被“痰盂派”視為一種不必要的潔癖。
C語言中也有類似的“痰盂”問題:
頭文件除了可以包含函數原型和宏定義外,也可以包括結構體類型定義和全局變量定義。
————譚浩強,《C程序設計》(第四版)學習輔導,清華大學出版社,2010年7月,p188
在頭文件中究竟能否定義外部變量呢?這個問題同樣有兩種選擇。
“痰盂派”會認為可以。比如
在 abnormal.h 中寫
int e_v = 1 ;
然后在abnormal.c中寫
#include "abnormal.h" #include <stdio.h> int main(void) { printf("%d\n",e_v); return 0; }
沒有人會說這段源程序有什么語法問題,它也沒有違背C語言的任何規定。就像痰盂可以用於盛飯是一個道理。
但是,“非痰盂派”會覺得這段源程序透着一種莫名其妙的詭異:既然定義變量e_v是為了在abnormal.c中使用,那么把這個變量定義在abnormal.c中顯然最直接也最便利,為什么要舍近求遠地把它定義到abnormal.h文件中呢?
在“非痰盂派”的眼中,“*.h”文件不是用來寫變量定義的,就如痰盂不是用來盛飯的一樣。那么“*.h”文件是做什么用的呢?
首先來看最簡單的情況
#include <stdio.h> #include <math.h> int main(void) { printf("%f\n",sqrt( 9.0 )); return 0; }
在這段代碼中,出現了printf和sqrt這樣兩個標識符,由於編譯器在編譯時不認得這兩個標識符,所以需要告訴編譯器這兩個標識符的含義。這就是代碼前面兩條預處理命令
#include <stdio.h> #include <math.h>
的意義。這兩個頭文件中分別包含printf和sqrt這樣兩個標識符的類型聲明。只有告訴了編譯器這兩個標識符的含義,編譯器才能把它正確地編譯成目標文件(*.obj或*.o)。(如果函數返回值類型為int可不聲明,但這是一種落后的、逐漸被淘汰的風格)
所以“*.h”文件的一個基本功能就是提供函數類型聲明。
另外要注意到的一點是,stdio.h、math.h是由庫函數作者提供的,相當於給其他模塊提供了一個使用“說明書”,使用庫函數的模塊用這個“說明書”向編譯器說明自己所用到的在其他模塊定義的函數名的數據類型。
下面舉例進一步說明。
假設一個源程序由兩個*.c源文件組成,第一個*.c提供求兩個double類型數據和的函數,第二個使用這個函數。那么,第一個*.c文件的作者僅僅寫出
1.c
double add(double d1,double d2) { return d1 + d2 ; }
是遠遠不夠的,因為這個1.c雖然可以編譯目標文件(*.obj或*.o)以供連接時使用,但是在鏈接之前,2.c文件在編譯時還需要告訴編譯器add這個標識符的含義,否則無法正確地編譯出與之相應的目標文件。為此,第一個*.c文件的作者還應該給出一個“說明書”——*.h文件
1.h
double add(double,double);
2.c文件可以寫為
#include "1.h" int main(void) { printf("%f\n",add(3.0,4.0)); return 0; }
這樣就解決了第二個*.c文件的編譯問題(add得到了說明)。
需要說明的是,在很多情況下1.c自己也往往需要包含這個1.h文件,因為這個1.c中可能有其他函數也需要調用這個add()函數,因而也需要這個函數類型聲明。所以一般1.c寫為
1.c
#include "1.h" double add(double d1,double d2) { return d1 + d2 ; }
再來看*.h文件中出現數據類型聲明的情況。
假設在1.c中使用了一種新的用戶定義的數據類型(不一定是“結構體類型”),例如
typedef double DOUBLE;
這時通常也應該把該數據類型的聲明寫在“*.h"文件中提供給其他模塊,除非這個類型僅僅在1.c中使用。
這時的1.h應該為
1.h
typedef double DOUBLE; DOUBLE add(DOUBLE,DOUBLE);
這時的1.c為
1.c
#include "1.h" DOUBLE add(DOUBLE d1,DOUBLE d2) { return d1 + d2 ; }
而2.c則為
#include "1.h" int main(void) { DOUBLE d1=3.0,d2=4.0; printf("%f\n",add(d1,d2)); return 0; }
在這里共有兩處用到了DOUBLE類型,一次是定義d1,d2這兩個變量,另一次是調用add()函數。由於在1.h中這種類型已經得到了聲明,所以這段代碼可以順利通過編譯。
除了函數類型聲明、數據類型聲明出現在*.h文件中,宏定義也可能出現在*.h中。
如果1.c和2.c都需要一個共同的常數,把這常數作為一個符號常量寫在1.h中,顯然可以避免你寫你的、我寫我的,從而造成大家不協調一致的錯誤。因為這時大家都是在參照着同一個常數(宏)在寫代碼。
但是,如果把外部變量的定義寫在*.h中會出現什么情況呢?很顯然,會出現在1.c和2.c中兩次定義這個外部變量的情況,這是絕對不允許的。這就是“非痰盂派”認為在頭文件中不可以寫外部變量定義的理由。