C++程序的編譯連接過程主要有預處理, 編譯, 連接這幾個階段:
1 預處理:
預處理是在編譯以前, 由編譯器調用的一個獨立程序, 即預編譯處理器, 對源代碼進行處理, 預處理主要負責如下工做:
1) 替換宏
2) 刪除註釋
3) 處理預處理指令, 如#include, #ifndef函數
源程序文件通過預處理後, 就造成一個包含全部必要信息的單個源文件, 一個源文件就是一個編譯單元, 這個編譯單元, 即源文件會被編譯成同名的目標文件(.o或.obj)對象
2 編譯:
編譯過程負責如下工做:
1) 語法分析, 檢查語法錯誤
2) 編譯源單碼, 即編譯單元, 將編譯單元中以文本形式存在的源代碼編譯成機器語言形式的目標文件
內聯函數的替換也發生在這一階段, 編譯過程當中, 每一個編譯單元是相互獨立的, 即每一個源文件之間不知道對方的存在, 除了像include "xxx.cpp"這樣極其錯誤的寫法.
在目標文件中, 除了有數據和二進制代碼, 還有至少3張表: 未解決符號表, 導出符號表, 地址重定向表.
未解決符號表記錄全部在該編譯單元中引用(好比使用其它源文件中的函數, 全局變量等)可是定義不在該編譯單元中的符號及其在該編譯單元中出現的地址, 在連接階段, 連接器會從其它目標文件的導出符號表中查找該表中記錄的符號, 若是該表中記錄的符號在連接階段未所有找到, 就會報"unresolved external link"這樣的連接錯誤.
導出符號表記錄該編譯單元中定義的, 而且可以提供給其它編譯單元使用的符號及其地址, 其它編譯單元的未解決符號表中的記錄的符號就須要從導出符號表中查找地址重定向表記錄了該目標文件中全部對自身地址引用的記錄, 這些記錄實際上至關於在該目標文件中的地址偏移.編譯器
3 連接:
連接過程是將全部目標文件連接到一塊, 造成一個可執行文件.
連接器進行連接的時候, 首先決定各個目標文件在最終可執行文件裏的位置, 而後訪問全部目標文件的地址重定向表, 對其中記錄的地址進行重定向(即加上該編譯單元實際在可執行文件裏的起始地址), 而後遍歷全部目標文件的未解決符號表, 而且在全部的導出符號表中查找匹配的符號, 並在未解決符號表中所記錄的位置上填寫實際地址(即該符號在擁有其定義的目標文件中的實際地址),最後把全部目標文件的內容寫在各自的位置, 就生成了可執行文件.io
內部連接與外部連接:
外部連接: 編譯單元中, 若是一個名稱在連接期能提供給其它編譯單元使用, 能夠和其它編譯單元交互, 那麼這個名稱就有外部連接; 如下狀況有外部連接:
1) 類的非inline函數, 包括靜態和非靜態成員函數
2) 類靜態成員變量
3) 命名空間或全局的非靜態自由函數, 非靜態友元函數及非靜態變量
內部連接: 編譯單元中, 若是一個名稱是局部的, 而且在連接時不會與其它編譯單元中的一樣名稱相沖突, 那麼這個名稱就有內部連接; 如下狀況有內部連接:
1) 全部的聲明
2) 命名空間或全局的靜態自由函數, 靜態友元函數, 靜態變量的定義
3) enum定義
4) inline函數定義(包括自由函數和非自由函數)
5) 類定義
6) const常量定義
7) union的定義
extern關鍵字告訴編譯器, 這個符號在別的編譯單元中定義, 也就是要把這個符號放到未解決符號表中, 也就是外部連接.
static關鍵字位於全局函數或變量聲明的前面, 代表該編譯單元不導出這個符號, 所以沒法在別的編譯單元裏使用, 也就是內部連接.
自由函數和變量默認是外部連接, const默認是內部連接, 能夠經過使用extern和static改變連接屬性
常見問題
頭文件裏通常只能夠有聲明, 不能有定義, 由於頭文件能夠被多個編譯單元包含, 若是頭文件裏有定義, 那麼每一個包含該頭文件的編譯單元就都會同一個符號進行定義,若是該符號爲外部連接, 就會出現重複定義的連接錯誤, 因此若是頭文件若是要定義, 要麼確保該頭文件不會被多個編譯單元引用, 要麼確保定義的符號都具備內部連接.
const默認爲內部連接是爲了可以在頭文件中定義常量, 例如const int n = 0; 因爲常量是隻讀的, 因此即便每一個編譯單元都有一份定義也沒有關係, 不過有2種狀況須要考慮:
1 若是涉及對這個const對象取地址而且依賴於這個地址的惟一性, 那麼在不一樣的編譯單元裏, 取到的地址不一樣.
2 若是這個對象具備mutable屬性, 某個編譯單元對其進行修改, 其它編譯單元看不到這一改變.
若是一個定義於頭文件中的變量擁有內部連接, 那麼若是有多個編譯單元包含該頭文件, 即有多個編譯單元中都定義了該變量, 則其中一個編譯單元對該變量進行修改, 其它編譯單元中就看不到這一改變.
非靜態函數和類的非inline函數默認是外部連接, 由於若是是內部連接, 可能會有人傾向於把定義寫在頭文件裏, 這樣的話一旦函數修改, 全部包含該頭文件的編譯單元都要從新編譯.
不容許在類的定義中對類的靜態成員就地初始化, 由於類聲明一般在頭文件, 這樣就至關於在頭文件中定義類的靜態成員, 而靜態成員具備外部連接, 若是該頭文件被包含在多個編譯單元中, 就會出現連接錯誤.
內聯函數要定義於頭文件裏, 由於編譯單元之間相互獨立, 相互不知道, 若是內聯函數定義於源文件中, 編譯其它使用該函數的編譯單元時沒有辦法找到函數定義, 所以也沒法對函數展開.
若是定義於頭文件裏的內聯函數被拒, 那麼編譯器會自動在每一個包含了該頭文件的編譯單元裏定義這個函數而且不導出符號.編譯