“我們來重新組織一下我們的工程。”老C解釋,“順便再討論一下文件結(jié)構(gòu)的問題。”老C摸摸下巴,“你知道C語言的文件分為頭文件和源文件嗎?”
“在這里,a.c就是preprocesing file,而a.c,a.h和b.c合起來稱為preprocessing translation unit。”老C指著他畫的框框,“哦,對了……這些框框表示文件……”
“那么什么叫translation unit呢?”小P問。
“就是經(jīng)過preprocessing 后的preprocessing translation unit,你可以理解為a.c經(jīng)過預(yù)處理后,在頭部將a.h和b.c內(nèi)容展開后的某個中間件……”老C解釋道,“這樣不正規(guī)的解釋可以幫助你更快的理解……”
“這些與declaration,definition有什么關(guān)系?”
“當(dāng)然有,在解釋什么是declaration和definition時,我們需要用到translation unit的概念。”老C答道,“因?yàn)閠ranslation unit中包含有external definition……”
“等等,什么是external definition?”小P追問。
“哦,為了解釋這些概念,我們先看看最初的那些例子,然后再熟悉一下這些術(shù)語。”老C指了指剛才在白板上隨意寫下的聲明示例,然后又在旁邊寫下了以下文字。
declaration
definition
“所謂definition者,引起內(nèi)存分配的declaration也……”老C開始轉(zhuǎn)文……
“囧……請說地球話,反對火星語……”小P抗議。
“呵呵,簡單的說,declaration說明了一組標(biāo)識符的含義和屬性(A declaration specifies the
interpretation and attributes of a set of
identifiers),而definition就是引起內(nèi)存分配的那些declaration——詳細(xì)來說,如果對于對象,導(dǎo)致了內(nèi)存分配的動作;對
于函數(shù),包含了函數(shù)體;對于枚舉常量或typedef名稱,就是declaration本身。 ”
“哦……有些暈……”小P有些不明白。
“好吧,簡單的說,definition是一些特殊的declaration,如果在聲明一個對象時,引起了存儲空間配分配于該對象,那么這個
declaration就是definition;如果在聲明一個函數(shù)時,這個聲明包含了函數(shù)體,那么這個declaration就是
definition;如果是枚舉常量和typedef,那么這些declaration本身就是definition。這下可明白?”老C耐心的解釋起
來。
“嗯,就是說definition其實(shí)就是一些特殊的declaration,是吧?”小P有些理解,“但是在C里面怎么會有對象啊?”
“哦,基本上可以這樣理解。”老C回答小P的前一個問題,然后又開始回答下一個,“所謂對象,不過是統(tǒng)稱,比如int
a,a就是int的一個對象,如果你不習(xí)慣使用對象這個術(shù)語,我們可以用object來代替。”然后他指著上面的代碼例子,“你來寫寫哪些是
declaration,哪些是definition吧。”
“如果圖省事,這些全部都是declaration……”小P自作聰明。
“囧……對是對,可是……我說你就不能嚴(yán)肅一些嗎?”
“呵呵,開玩笑的,何必當(dāng)真呢?”小P一邊說,一邊在旁邊寫下注釋。
int a; // definition
extern int a; // declaration
extern int a = 5; // ?
int Func (void); // declaration
int Func(void) // definition
{
}
“有一個不知道是什么,所以我畫了問號。”小P指著代碼說道。
“沒有關(guān)系,我們先不管它到底是什么,我們再來看看其它幾個概念。”老C沒有著急給出小P答案,而是在白板上的一塊空白地方又寫下如下文字。
external linkage
internal linkage
none linkage
“一個標(biāo)識符(identifier),如果在不同的scope中被聲明,或者在同一個scope中被多次聲明,它總會被正確的指向同一個object或
者function,這一過程叫做linkage。”老C解釋,“比如我有兩個文件,a.c和b.c,一個函數(shù)FuncA()在a.c中定義,如果你想在
b.c中的FuncB()函數(shù)中使用函數(shù)FuncA(),在b.c中你可以這樣寫……”老C又開始在白板上涂抹。
a.c:
void FuncA(void)
{
}
b.c:
extern void FuncA (void);
void FuncB(void)
{
FuncA();
}
“喏,你只要在b.c中聲明這個函數(shù)就可以了,你看,函數(shù)被聲明了兩次——注意定義是聲明的特例——如果你的兩個文件被正確的編鏈,那么C語言規(guī)范保證可
以找到正確的FuncA()。”老C在白板上指指點(diǎn)點(diǎn),“同時要注意,這里說的是聲明多次,可沒有說定義多次,如果你把函數(shù)定義了超過一次,那么編鏈的時
候會報(bào)錯的……”老C咽了一口唾沫,“這個就是external linkage的一個例子。而且根據(jù)C ISO/IEC
9899規(guī)范,我們甚至不用在b.c中FuncA()函數(shù)的聲明前加exern,編鏈器一樣可以正確的找到FuncA()的定義。”
“哦?是嗎?那么我到要試試……”小P有些好奇。
“嗯,你等等再試。我再來說說internal linkage。”老C開始更改他在白板上寫下的代碼,“如果我在FuncA()的聲明前加上static,那么其它的translation unit無論如何無法找到這個函數(shù)。”
a.c:
static void FuncA(void)
{
}
“如果這個時候我們的代碼還是b.c的樣子,就會產(chǎn)生一個編鏈錯誤,告訴我們無法解析FuncA這個標(biāo)識符。”老C道,“這個就是一個internal linkage的例子。”
“那么none linkage呢?”小P追問。
“……自己看看 ISO/IEC 9899規(guī)范吧……”老C覺得小P自己也得花些功夫了,“下面我們就來詳細(xì)看看external
definitions。這里之所以講external,是因?yàn)檫@些definitons都在函數(shù)外部……什么?你不知道可以在函數(shù)內(nèi)部定義和聲明函
數(shù)?……這樣也好,這是C語言的怪癖……我們不管那么多,先看看又有哪些概念需要了解的……”老C撓撓頭,“哦,可能之前我們得先了解一下什么是
scope。”
“scope?就是作用域吧?”小P問。
“嘶……”老C抽了一口氣,“我不知道怎么解釋,在我理解作用域還包括了name spaces的概念,因此我更喜歡使用scope這個術(shù)語而不是很具有內(nèi)涵的作用域這個術(shù)語。”
“C語言也有name spaces嗎?”小P不解。
“有啊……自己去看吧。”老C不想多費(fèi)口舌,“所謂scope,又分為以下幾種……”他又在白板上涂抹起來。
function scope
file scope
block scope
function prototype scope
“呵呵,”老C笑道,“file scope最好解釋,如果一個標(biāo)識符沒有被聲明到其它三個scope當(dāng)中,那么它的scope就是file scope……至于其它三個scope的含義,我建議你……”
“……去看規(guī)范……”小P囧。
“哈哈……”老C突然覺得這是一個少費(fèi)口舌的好辦法,“其實(shí)簡單的理解,file
scope就是我們一般聲明的全局變量和函數(shù),因?yàn)橐?guī)范是很嚴(yán)肅的東西,所以才寫得那么羅嗦和晦澀,因?yàn)榭傆腥讼矚g找一些特殊的情況以顯示自己對規(guī)則的藐
視,所以規(guī)范不得不那么面面俱到……好啦好啦,我也是胡說的,呵呵。你只要知道我們說的external definitions是在file
scope中的定義就好了。在進(jìn)入我們正式的議題前,我再磨蹭一下。”說完老C在白板上寫下如下文字。
storage-class specifiers:
typedef
extern
static
auto
register
“我們主要討論extern和static,但是其它的你也要了解一下,所以……”
“……看規(guī)范……”
“呵呵,好了好了,我們現(xiàn)在來說說external
definitions吧。”老C覺得小P真是善解人意啊,“這里你只要了解一些簡單的規(guī)則就可以了。第一,function的規(guī)則與object不同;
第二,如果你沒有將function或者object聲明為static,那么它們自動的成為extern;第三,object的規(guī)則比較復(fù)雜一些,這
樣,我來說你來寫……”老C揉揉手,想偷懶一下,“這樣你印象比較深刻……”
“囧……好吧……”小P不情愿的回應(yīng),拿起彩筆一邊聽老C講,一邊在白板上寫下如下內(nèi)容。
1. 聲明一個object,若它的scope是file scope,且它被初始化,那么它的聲明就是一個external definition.
2.
聲明一個object,若它的scope是file scope,且它沒有被初始化,且它沒有storage-class
specifier,或者它的storage-class specifier是static,則此聲明就被命名為tentative
definition。如果一個translation unit中有一個或多個關(guān)于此一標(biāo)識符的tentative
definition,并且在此translation unit中沒有關(guān)于此標(biāo)識符的external
definition,那么此標(biāo)識符會被當(dāng)作此translation unit中的一個file
scope的一個declaration,其作用在整個file scope中,且有一個0初始化值。
3. 如果一個標(biāo)識符的聲明是tentative definition,并且有external linkage,則此被聲明的類型不能是不完整的類型。
4.
如果一個變量其聲明前帶有extern storage-class specifier,則其是否是exernal或internal
linkage要視其前面是否有在scope中可見的此相同變量的聲明,如果有,則其跟隨前一相同變量的聲明,否則就是exernal linkage。
4. 同一個標(biāo)識在一個translation unit當(dāng)中即表現(xiàn)exernal linkage,又表現(xiàn)internal linkage,則其行為未定義。
“唔……不是很好理解。”小P抱怨。
“呵呵,我們來看幾個例子好了。這些標(biāo)識符都被聲明在一個translation unit當(dāng)中。”老C說道,“但是我想提醒一下,external
definition與external linkage的external含義完全不同,不要搞混淆了。”然后他在小P寫的話下面又增加了一組代碼。
int i1 = 1; // definition, external linkage
static int i2 = 2; // definition, internal linkage
extern int i3 = 3; // definition, external linkage
int i4; // tentative definition, external linkage
static int i5; // tentative definition, internal linkage
int i1; // valid tentative definition, refers to previous
int i2; // undefined, linkage disagreement
int i3; // valid tentative definition, refers to pre vious
int i4; // valid tentative definition, refers to pre vious
int i5; // undefined, linkage disagreement
extern int i1; // refers to previous, whose linkage is external
extern int i2; // refers to previous, whose linkage is internal
extern int i3; // refers to previous, whose linkage is external
extern int i4; // refers to previous, whose linkage is external
extern int i5; // refers to previous, whose linkage is internal
int
i[]; // the array i still has incomplete type, the
implicit initializer causes it to have one element, which is set to
// zero on program startup.
“我想提醒一下,這里只是做說明,在實(shí)際編碼時我們可不要這么寫。”老C強(qiáng)調(diào),“那么現(xiàn)在你是否明白extern int a = 5 是declaration還是definition了嗎?”
小P仔細(xì)看了看老C寫的示例代碼,又把自己寫的話念了幾遍,說道:“嗯,這樣看來這個語句應(yīng)當(dāng)是具有exernal linkage 的exernal definition。”
“呵呵,不錯,我再總結(jié)一下。你可以簡單的理解為如果一個變量在聲明時被初始化,那么這個聲明就成為一個定義,而與storage-class
specifier無關(guān),如果其前面有storage-class specifier,那么只能說明其是否是internal
linkage或external
linkage;如果一個變量在聲明時前面帶有extern,且沒有被初始化,那么它就是一個declaration,且其是否是external
linkage要視前面是否有其它此相同變量的聲明,如果有,則其跟隨前面這一相同變量的聲明,如果沒有,則其為external
linkage;聲明總是傾向于exernal linkage,如果你不聲明static;根據(jù)規(guī)則不能出現(xiàn)既是internal
linkage又是external linkage的情況,否則其行為無定義。”老C覺得十分渴,找到茶杯大大的喝了一口水。
“好吧,我承認(rèn)很復(fù)雜……可是這個和我們討論的內(nèi)容有什么關(guān)系呢?”小P有些云里霧里。
“呵呵,只是一些理論基礎(chǔ)。”老C答道,“根據(jù)規(guī)則我們可以使用各種各樣的組合來管理我們的代碼,設(shè)計(jì)我們的文件組織,但是在實(shí)際開發(fā)中自然有一定的規(guī)
則。如果你按照這種規(guī)則進(jìn)行編碼,那么基本上不用關(guān)注這些標(biāo)準(zhǔn)的細(xì)節(jié),當(dāng)然,出現(xiàn)錯誤的時候你還是要根據(jù)標(biāo)準(zhǔn)來查找可能出錯的地方。”
“哦?是嗎?說來聽聽?”小P問。
“好吧。”老C答道。“我們以前討論過,我們?nèi)祟悓τ趶?fù)雜事物的處理能力是有限的,為了解決這些復(fù)雜問題,我們總是希望把它們分解成我們可以理解的規(guī)模。
通過信息隱藏的方式,我們可以將一個很大規(guī)模的問題分解分解再分解,直到我們的智力可以管理這些問題。而使用文件對代碼進(jìn)行劃分,可以有效的幫助我們對問
題的規(guī)模進(jìn)行控制——眼不見,心不亂嘛。”
“哦,具體怎么做呢?能不能舉個簡單的例子?”小P問道。
“可以啊。”老C答道,然后指揮小P將白板擦干凈,又在上面開始比劃,“一個比較簡單的問題,求解一個方程。”他在白板上寫下如下文字。
ax2 + bx + c = 0
“我們可以這樣來分解問題。”老解釋,“設(shè)計(jì)一個函數(shù),其返回值為實(shí)根的個數(shù),0為沒有實(shí)根,1為有兩個相等的實(shí)根,2為有兩個不等實(shí)根,3為有無窮多解,-1為無解。實(shí)根作為出口參數(shù),設(shè)計(jì)為函數(shù)接口的一部分。我們把這個函數(shù)放到solve.c文件中。”
solve.c:
int Solve (float a, float b, float c, float* root1, float* root2);
int Solve(float a, float b, float c, float* root1, float* root2)
{
}
“這樣如果我們在某個項(xiàng)目中需要解一個二元一次方程,那么我們,比如在main.c中,就可以很簡單的這樣寫。”老C接著在白板其它地方寫道。
main.c:
extern int Solve (float a, float b, float c, float* root1, float* root2);
int main()
{
float root1, root2;
...
Solv(1, -1, 1, &root1, &root2);
...
}
“只要我們將solve.c正確的添加到我們的工程中就可以了。”老C道。
“這樣寫有什么好處呢?”小P問。
“好處嘛,最明顯的是……復(fù)用,而且就算是我們要自己寫Solve()函數(shù),現(xiàn)在它也與main()函數(shù)分開,人為的將兩個關(guān)系比較遠(yuǎn)的模塊分開,這樣可
以強(qiáng)制的控制代碼的規(guī)模。”老C點(diǎn)點(diǎn)頭,“如果我們將extern 語句放入一個名叫solve.h的文件中,那么就更方便了。”
solve.h:
#if !defined(SOLVE_H_)
#define SOLVE_H_
extern int Solve (float a, float b, float c, float* root1, float* root2);
#endif /* SOLVE_H_ */
main.c:
#include "solve.h"
int main()
{
float root1, root2;
...
Solve(1, -1, 1, &root1, &root2);
...
}
“這樣的好處呢?”小P問。
“簡單,減少冗余。如果我們solve.c中有很多可以讓其它文件使用的函數(shù),這樣就不用在其它文件頭部寫出很多的extern...的聲明,而只用在solve.h中寫一次,在其它文件中#include就可以了。”老C補(bǔ)充道,“偷懶,是程序員的美德……”
“這里為什么要用.h文件呢?我用一個.c文件,在里面寫入extern...的聲明不行嗎?”小P接著問。
“……沒有什么不行,但,不符合行規(guī)……而且如果你使用automake工具的話,可能配置起來要麻煩一些……總之不要在這些地方釋放你多余的創(chuàng)造力,別
人怎么做的你就怎么做,這個是行業(yè)內(nèi)的規(guī)矩……”老C有些郁悶,心想這真是一個多動的家伙啊,“而且最好在.h文件中只出現(xiàn)聲明而不要出現(xiàn)定義,這樣你在
編譯的時候鏈接錯誤會少很多很多。”
“為什么在solve.c文件的前面要先寫一個
int Solve (float a, float b, float c, float* root1, float* root2)?”小P指著白板問。
“函數(shù)原型,這個就叫做function
prototype。”老C解釋,“當(dāng)然你也可以不用寫,但是根據(jù)行業(yè)內(nèi)許多經(jīng)驗(yàn)的總結(jié),這樣寫總有好處,因?yàn)閾?jù)說這樣在編譯的時候可以讓編譯器在函數(shù)調(diào)
用點(diǎn)做全面的類型檢查。”老C指著solve.c下面的代碼說,“其實(shí)這里又出現(xiàn)一處冗余,因?yàn)樵趕olve.c和solve.h文件中,Solve()
函數(shù)被聲明了兩次,這樣在Solve()函數(shù)接口被修改的時候,我們不得不修改兩處地方,而這是我們很討厭的事情。”
“那么有什么解決方法呢?”小P問。
“我們可以在solve.c中包含solve.h,這樣就可以了。”老C說,“然后我們可以進(jìn)行解決問題的細(xì)節(jié)工作。”他又在白板上比劃起來。
solve.c:
#include "solve.h"
#include <math.h>
#define EPSILON 0.000001F
static float Solve1stOrder (float b, float c);
static float Delta (float a, float b, float c);
static float DoSolve (float a, float b, float sqrtDelta);
int Solve(float a, float b, float c, float* root1, float* root2)
{
int rootNum;
float delta;
float sqrtDelta;
/* If a is 0, then the formula becomes 1st order. */
if ((a < EPSILON) && (a > -EPSILON))
{
if ((b < EPSILON) && (b > -EPSILON))
{/* b is 0 */
if ((c < EPSILON) && (c > -EPSILON))
{/* If c is 0, the formula has infinite roots. */
rootNum = 3;
return rootNum;
}
else
{/* If c is not 0, the formula has no root. */
rootNum = -1;
return rootNum;
}
}
else
{/* b is not 0 */
rootNum = 1;
*root1 = *root2 = Solve1stOrder(b, c);
return rootNum;
}
}
delta = Delta(a, b, c);
/* If delta < 0, the formula has no real root. */
if (delta < 0)
{
rootNum = 0;
return rootNum;
}
/* If delta is 0, the formula has two equal real roots. */
if ((delta < EPSILON) && (delta > -EPSILON))
{
rootNum = 1;
*root1 = *root2 = (-b) / (2 * a);
return rootNum;
}
/* If delta > 0, the formula has two different real roots. */
if (delta > 0)
{
rootNum = 2;
sqrtDelta = sqrt(delta);
*root1 = DoSolve(a, b, sqrtDelta);
*root2 = DoSolve(a, b, -sqrtDelta);
return rootNum;
}
}
static float Solve1stOrder(float b, float c)
{
return (-c) / b;
}
static float Delta(float a, float b, float c)
{
return b * b - 4 * a * c;
}
static float DoSolve(float a, float b, float sqrtDelta)
{
return (-b + sqrtDelta) / (2 * a);
}
“看,一些具體的解題過程我們并不想暴露給其它文件,所以使用static將其聲明為internal
linkage,這樣就相當(dāng)于隱藏了信息;而Solve()函數(shù)是我們希望暴露給其它文件的,所以使用extern將其聲明為external
linkage——這樣以文件為單位,我們組織了一個程序的模塊,并向其它模塊提供了接口,以供其它模塊使用。”看到小P還在看代碼,老C接著解釋道,“
哦,EPSILON這里只是一個需要注意的小技巧,因?yàn)槟悴荒鼙容^兩個float數(shù)值是否相等,只能比較它們的差是否小于一個很小的數(shù)值,來判斷它們是否
相等……原因?……與浮點(diǎn)數(shù)在內(nèi)存中的存放格式有關(guān)系。總之你認(rèn)為浮點(diǎn)數(shù)的最后幾位總是隨機(jī)的就可以了。”
“哦,這樣我就明白了。”小P點(diǎn)點(diǎn)頭,“那么這個solve.h中的條件編譯是怎么回事?”
“哦,這也是一些小技巧,用于防止頭文件被重復(fù)的包含而可能導(dǎo)致的遞歸。你只要認(rèn)為#include是將其所引用的文件原封不動的放到引用點(diǎn)就可以理解
了,”老C撓撓頭,“比如a.c包含a.h和b.h,而a.h包含c.h,b.h也包含c.h,那么c.h的這些條件編譯可以防止c.h在a.c中被包含
兩次。你可以自己在#include的包含處將文件展開看看就明白了。”
“是么?”小P在紙上畫了幾下,“哦,這樣我就清楚了。呵呵。但是……有沒有包含.c文件的情況呢?”小P又開始發(fā)揮想象力。
“唔……有的……”老C撓撓頭,“在某些需要裁剪和定制的項(xiàng)目中也許會根據(jù)某個.h文件中的條件編譯來選擇是否包含某些.c文件,但……這些工作也可以由
makefile來完成,而且感覺大多數(shù)的做法都是采用腳本+makefile完成的……無論怎么樣,你現(xiàn)在先不要使用包含.c文件的做法,等熟悉了以后
我們再慢慢研究……”他搓搓手,“好吧,廢話說了這么多的一大堆,我們也去休息休息睡午覺吧,下午3點(diǎn)到教研室,我們接著聊。”老C有些乏力的說。
“呵呵,好啊好啊。”兩人一邊說一邊向門口走去……
(繼續(xù)等待v0.03……)