1. 程式人生 > >【C++】多檔案程式結構

【C++】多檔案程式結構

以前寫一個C++多檔案程式的時候經常為哪些東西應該放在.h檔案裡,哪些東西應該放在.cpp檔案裡而疑惑。稍有不慎就搞出一個“error:LNK2005 已經在*.obj中定義”的重複定義錯誤,就算解決了這個問題自己實際上也還是一知半解。最近去了解了C++多檔案程式結構的知識,才搞清楚了這些問題的本質。在此總結一下,如有錯誤,歡迎指出。

宣告與定義

首先從宣告和定義說起。
宣告是資料物件的和函式的描述。宣告的作用就是讓編譯器知道實體的名字,以及其資料型別或函式簽名。如:
external int x; //變數宣告
void fun(); //函式宣告
class A; //類宣告

定義則是實體本身,代表著實體在一個作用域的唯一描述。
如:
int x; //變數定義
void fun() {…} //函式定義
class A {…}; //類定義

因此,可以理解為,宣告是定義的引用,而定義是實體本身。

外部連結性與內部連結性

定義具有連結性。連結性分為內部連結和外部連結。

外部連結:外部連結的定義可被定義所處的翻譯單元(.cpp)內看見,也可以被其他翻譯單元引用。
具有外部連結性的:
• 非inline函式。包括名稱空間中非靜態函式、類成員函式和類靜態成員函式
• 類靜態成員變數總有外部連結。
• 名稱空間(不包括無名名稱空間)中非靜態變數

內部連結:內部連結的定義只能在該定義所處的翻譯單元內看見。
具有內部連結性的:
• 所有的宣告
• 名稱空間(包括全域性名稱空間)中的靜態自由函式、靜態友元函式、靜態變數的定義、const常量定義
• enum定義
• inline函式定義(包括自由函式和非自由函式)
• 類(class、struct、union)的定義
注:在類體中定義的成員函式為內聯(inline)函式,屬於內部連結。

實質上宣告沒有連結性的概念,但可以理解成宣告總是內部連結的,因為它只對它所在的翻譯單元有效。如果我們把宣告置於標頭檔案,則由於包含該標頭檔案的每個翻譯單元都獨立複製了該宣告(見下文預處理部分的說明),因此每個翻譯單元都能“看到”這個宣告。

預處理、編譯和連結

C++中,源程式要被翻譯成可執行檔案,都要經過三個步驟:預處理、編譯和連結。

預處理:閱讀源程式,執行預處理指令,嵌入指定原始檔。預處理指令以“#”號開始。如#include指令實現檔案包含。當一個.cpp檔案編譯前,它首先遞迴地包含標頭檔案,形成一個含有所有必要資訊的單個原始檔,也就是一個翻譯單元。

編譯: 編譯器每次翻譯一個.cpp檔案(翻譯單元),並輸出物件檔案(.o或.obj)。物件檔案含有.cpp檔案內定義的所有函式編譯後的機器碼,也包含.cpp檔案內定義的全域性變數和靜態變數。此外,物件檔案也可能含有未定義引用,這些未定義的引用就是該翻譯單元內有宣告,但是在這個.cpp檔案中沒有定義的函式和全域性變數。
那麼,這些沒有定義的東西在哪?答案是這些東西定義在其他.cpp檔案中。
要怎麼找到呢?這就是連結器的任務了。

連結:有外部連結的定義可以在物件檔案中產生外部符號,這些外部符號可以被所有其他的翻譯單元訪問,用來解析他們未定義的引用。連結器的工作就是讀取所有物件檔案,並嘗試解決物件檔案之間的交差引用。如果成功,則產生可執行程式。當無法解決外部引用的時候,根據情況連結器有兩種報錯:
1、當找不到引用的目標時,就會產生“無法解決的外部符號”錯誤。
2、當找到兩個或以上相同名字的實體(函式或變數時),就會產生“符號被多重定義”錯誤。

因此,要讓程式正確地連結,首先不能宣告一個實體,卻沒有相應的定義。比如在A.cpp裡面宣告一個void fun();但在這個檔案和其他檔案中都沒有這個函式的定義(也就是函式的實現),這就會產生“無法解決的外部符號”錯誤。同時,也不能重複地定義具有外部連結性的實體。比如,在A.cpp檔案裡面定義int x,同時在B.cpp裡面又定義一個int x。這樣就會出現“符號被多重定義”。

標頭檔案

瞭解了上面幾點知識,我們就可以理解和回答一些問題了。

(1)為什麼不要把外部連結的定義放在標頭檔案裡面?
因為我們知道cpp在預編譯的時候會遞迴包含標頭檔案,因此,如果一個頭檔案包含了一個外部連結的定義,其他包含它的.cpp檔案都會有一個相同的外部連結的定義。出現“符號被多重定義“也就不難理解了。要特別注意的是,類的靜態成員變數和一般的靜態變數不一樣,它具有外部連結性,因此假設你在標頭檔案中定義一個:
class A
{
static int member;
}
那麼該靜態成員變數的定義不能放在這個標頭檔案裡面。而是應該在某個.cpp檔案裡面寫定義: int A::member;

(2)有內部連結性的定義可以放到標頭檔案中去嗎?
要讓內部連結的定義影響程式的其他部分,可以把它放到標頭檔案中,這樣包含這個標頭檔案的其他檔案都知道了這個定義。但不推薦在標頭檔案中定義const或者static常量,因為會汙染全域性名稱空間,同時在每個包含標頭檔案的翻譯單元中浪費了資料空間。由於宣告可以看做是內部連結,因此我們可以把宣告放在標頭檔案中的時候,例如在標頭檔案中放 void fun(); 所有包含這個標頭檔案的其他檔案都可以使用fun()函數了。

(3)為什麼我們經常在標頭檔案中定義類,卻在cpp檔案中定義類成員函式?
因為類定義是內部連結性,而類的成員函式的定義是外部連結性的。

(4)可不可以在一個頭檔案中包含其他所有的標頭檔案,然後其他cpp檔案都include這個標頭檔案?
不要這樣,因為這樣的話,每此更改這個標頭檔案,都會引起其他的所有cpp檔案的重新編譯,嚴重影響編譯速度,同時也浪費了資料空間。