多檔案組成的程式

一般來說寫到目前的程式好像都只有單一個檔案,而使用到基本的stdio.h這個標頭檔,但是往往很多人都不知道標頭檔真的意義是要幹麻的,而C程式也不是說寫一個大型的專案用一個檔案就寫的完的,所以在這邊會講解使用多個檔案組合成一個大型的程式,而檔案和檔案之間是如何共用或是傳遞變數,以及如何使用標頭檔。

要完全了解如何將檔案和檔案之間的變數傳遞或是共用變數的話,那要先了解何為「內部連結」和「外部連結」,其實這屬於變數的儲存週期。而談到為什麼需要使用標頭檔的話,那就先必須了解「宣告」和「定義」有何不同,最後才真的講到主要講的東西,就是如何使用標頭檔和程式檔,串成一個大型程式的基本架構。

※ 連結性(linkage)

一般在C中的變數都會有自動儲存期或是靜態儲存期或是由malloc配出來的動態儲存期,基本上連結性只和全域變數有關係,我們知道全域變數是靜態儲存期,這沒有問題,但是怎麼知道全域變數是什麼連結性的呢?

* 靜態儲存期外部連結性
* 靜態儲存期內部連結性
* 靜態儲存期沒有連結或

※ 靜態儲存期外部連結性

Example:

int data; (全域變數,且具有外部連結性)
int main()
{
....
}

最簡單的最普通的全域變數就具有外部連結性,那何為外部連結性呢?就是說這個變數可以跨檔案之間共享,也就是你在這邊的變數data可以被別的檔案使用。其實透過我們的編譯,會將上面的程式碼翻成。

Example:

extern int data = 0; (定義一個外部連結變數data為0)
int main()
{
....
}

其實int data;在編譯器眼中只是個「暫時定義」的宣告而已,而int data = 0;才是真的定義,但是一旦在編譯時期沒有定義,只有暫時的定義的話,那麼所有的暫時定義都會被合併成為一個定義,且設定初值為0,所以說「暫時定義」可以有很多個,只要型別和識別字一樣的話。所有的暫時定義宣告,如果沒加上static(內部連結性)那麼就是外部連結性,在編譯期間,都會翻成如下範例。

Example:

int data; (暫時定義宣告)
int main()
{
....
}

翻譯成為

extern int data = 0;
int main()
{
....
}

※ 靜態儲存期內部連結性

Example:

static int data; (全域變數,且具有內部連結性)
int main()
{
....
}

要讓一個變數只具有內部連結性的話,在全域變數加上一個static的修飾字就成為內部連結性了,那何為內部連結性呢?就是說這個全域變數只可以被這個檔案使用,不具有在幾個檔案之間共同分享的功能,因此算是一個檔案私有的全域變數。

※ 靜態儲存期沒有連結性

怎麼還有那種叫沒有連結性的阿,其實這就是它不是一個全域變數,因為只有全域變數才會有內外部的連結性,那這東西是什麼,就是你在函式中用到的靜態儲存期變數。

Example:

int a()
{
static int data;
}
int main()
{
int a();
}

所以只要記住不是全域變數以外的變數只要有靜態存在期的那種就是沒有連結性,沒有連結性代表這個變數沒有辦法被別的檔案所共享,而且此變數也不是在本檔案中任何地方都可以存取的到,因為會受到範疇的影響。

※ 透過外部連結性讓別的檔案參用到同一變數

Example:

file a.c

int a; (暫時定義變數具外部連結性)
static int b; (暫時定義變數具內部連結性)
const int c = 100; (定義常數變數具有外部連結性)

file main.c

extern int a; (參用宣告)
extern const int c; (參用宣告)
int main()
{
printf("%d%d", a, c);
}

在程式中,比較不懂的應該是extern到底是在幹麻的,extern這關鍵字,其實表示「外部連結性」的意思(其實可加可不加,因為如果是暫時定義宣告,最都會被翻成extern),而代表後面跟著的宣告其實是參用某個檔的外部連結性的變數,而不是真的在此宣告一個變數a或是cosnt c,而是告訴連結器說,曾經有個int a和const int c在程式中某個檔案有定義,請去參用它。

其實在只要是外部連結的變數,不管你在那裡宣告,對於所有的檔案此變數都可以共用,那幹麻在每個檔案裡面都要多加個宣告,阿不是在a.c檔中已宣告了,其它的檔都可以用,那幹麻其它的檔還要在宣告呢?主要就是編譯的時候需要讓compiler知道有這樣的一個變數,而變數共享的話(外部連結)是 linker要做的事情,所以下面會提到為什麼宣告可以很多次,但是定義只可以有一次,只是宣告很多次只是讓compiler認識這一個變數而已,共享完全是要看你變數是不是外部連結性。

※ 全域變數定義(definition)和暫時定義(tentative definition)的不同

常常有人把全域變數的定義和暫時定義完全的搞在一起,其實暫時定義和定義是完全不同的,暫時定義是不會配置記憶體空間的,但是定義會配置空間。

Example:

int i; (暫時定義宣告)
int i = 0; (定義)

雖然看上去沒有什麼不同,但是暫時定義和定義有以下幾點不同:

* 在同一編譯單元內,可為同一識別字作數個外部連結性的宣告,但是型別和連結性要一致。
* 如果全域變數帶有初值的話就是個定義了(extern int a = 0)。
* 如果全域變數沒有extern修飾字或是初值的話,就是一個「暫時定義宣告」(int a)。
* 可以很多個暫時定義,但是只能有一個定義(int a = 0)。
* 如果存在一個定義的話,那所有的暫時定義都會當做是多餘的宣告。
* 假設在編譯單位中沒有定義的話,所有的暫時定義都會變成設定初值為0的定義,且併合在一起,成為單一的定義。

所以流程如下:

1. 尋找是否有外部連結的變數已定義了。
2. 如果有定義:所有的暫時定義宣告被乎略。
3. 如果沒定義:所有的暫時定義合併成為一個定義,且被翻成初值為0的定義。

所以如果你寫成這樣,暫時定義很多次,只定義一次的話,編譯是不會出錯的。

Example:

int i; (多餘宣告)
int i; (多餘宣告)
int i = 0;

如此是可以的喔!因為可以暫時定義很多次但只定義一次。

在多檔案之中定義和宣告就非常有關係了,在某個檔案中定義,或是在某個檔案中處理這變數的值,但是又可以在另一個檔案中參用讀取修改這個變數值,所以「定義」一次,在別檔案「參用」。

Example:

extern int a; (參用宣告)
extern const int c; (參用宣告)

※ 推廣外部連結性和內部連結性

既然連全域變數都有內外部的連結性,那函數呢?這當然是沒有問題的,因為函式什麼都不修飾的情況下,就具有外部連結性了,和全域變數什麼都不修飾的情況下一樣。那是不是加上static就成了內部連結性了呢?這是完全正確的。

Example:

void show() (函式具有外部連結性)
{
....
}
static void print() (函式具有內部連結性)
{
....
}
int main()
{
show();
print();
}

那一樣的,我如果想讓本檔案去參考到另一個檔案我所寫的函式的話該怎麼做呢?

Example:

file a.c

void show() (具有外部連結性的函數)
{
....
}
static void print() (具有內部連結性的函式)
{
....
}

file main.c

void show(); (函式的參用宣告)
int main()
{
show();
}

對於函式的參用宣告可以加上修飾用extern也可以不加,因為外部連結的函式宣告本來就具有跨檔案的能力,所以如果加了extern可以讓你更清楚你函式是在別的地方定義的,而你這邊使用只是「參用函式」而已,如此就可以讓本檔案的程式去參用另一個檔案中的函式,但是一樣套上面的話,如果寫一個多檔案組成的程式,如果函式不是讓整個程式都可以使用到的話,請宣告和定義成static讓範疇只具有檔案範圍,避免手忙腳乩之中,在此檔案定義了一個 a()函式,在另一個檔案中又定義一次a()函式,這樣連結器就會傻掉,不知道要去跑那個函式了。

※ 標頭檔

幾乎程式的第一行我們寫的都是#include 應該會有人不知道為什麼要這樣寫,其實.h(標頭檔)主要的目的就是提供一個「介面」,所謂的介面意思為裡面宣告了滿滿的函式宣告以及一堆的常數,讓你在程式設計的過程式,不用去寫一些很麻煩的底層的程式,像是printf()就有宣告在stdio.h裡面,所以我們只要拿來使用就行了,而不用自己寫,在編譯的過程之中,編譯器會只會去看你函式使用的格式對不對,或是有沒有語義上的錯誤,而真的把函式庫和我們寫程式串起來是連結器的工作,總之一個大型的程式可以分成。

* 介面(.h的標頭檔)
* 介面實作(.h的實作檔)
* 主程式

簡單用個小小的程式把上面所講的「介面」、「介面實作」、「主程式」串起來。

Example:

file interface.h

extern int a;
extern const int b;
void show();
void print();

file interface.c

#include
#include "interface.h"
int a;
const int b = 100;
static int c;
static int get()
{
....
}
void show()
{
get();
....
}
void print()
{
....
}

file main.c

#include
#include "interface.h"
int main()
{
printf("%d%d", a, b);
show();
print();
}

由此我們可看見我們把個一個程式拆成了三個部份了,而主程式看起來非常的簡單,只需要呼叫而已,其它的工作都是在模組裡面都完成,而且 interface.c裡面含有隱藏的內部函式是私用的,在main.c裡面完全不知道有這個函式,只需要知道看「介面」檔裡面怎麼宣告,就怎麼使用,根本連管它怎麼做的都不知道,就好比我們使用printf()的時候,只要記得加入stdio.h標頭檔和如何使用printf()這個函式就夠了,剩下的事情實作檔會幫你做好的。而且具有重覆使用性,如果那天要在開發另一個程式的時候,將自己最常使用的函式做成「介面」「介面實作」,那以後程式開發,也可以如同使用stdio.h一樣方便的應用以前寫過的函式。

0 意見: