分離式編譯是指一個完整的程序或項目由若干個源文件共同實現,每個源文件單獨編譯生成目標文件,最後將該項目中的所有目標文件連接成一個單一的可執行文件的過程。
每個.cpp源文件經過預處理,它所包含的.h文件的代碼都會被展開到其中。再經過編譯器的編譯彙編等過程,將該.cpp文件轉變爲.obj文件,這是此文件已經變爲二進制文件,本身包含的就是二進制代碼。這時,該文件還不一定能夠執行,因爲並不保證其中一定有main函數,或者該源文件中的函數可能引用了另一個源文件中定義的某個變量或者函數調用,又或者在程序中可能調用了某個庫文件中的函數,等等。這些都要通過鏈接器將該項目中的所有目標文件連接成一個單一的可執行文件來解決。
舉個例子:
//****************************** Test.h *************************************/ #pragma once #include<iostream> using namespace std; #include<stdlib.h> void fun(); // 僅僅只聲明 //****************************** Test.h *************************************/
//****************************** Test.cpp *************************************/ #include"Test.h" void fun() // 對函數fun()進行定義 { cout << "fun" << endl; } //****************************** Test.cpp *************************************/
//****************************** main.cpp *************************************/ #include"Test.h" int main() { fun(); //調用fun()函數 system("pause"); return 0; } //****************************** Test.cpp *************************************/
main.cpp包含的Test.h頭文件只有對fun()函數的聲明,所以在main.cpp中沒有任何與fun()函數定義相關的代碼,這裏就會把fun函數看做爲外部鏈接類型。在主函數中調用fun函數時,會產生如上圖的紅框中的call命令,當然這裏的地址是一個虛假的地址。鏈接器在Test.obj文件中找到fun函數的實現代碼,將call fun地址通過jmp指令切換成真正的fun函數地址。
再看模板的分離編譯:
///************************** SeqList.h **************************// #include<iostream> using namespace std; #include<assert.h> // 定義模板類 (類型參數爲T) template<typename T> class SeqList { public: SeqList(); //構造函數聲明 private: T* _array; size_t _size; };
///************************** SeqList.cpp **************************// #include"SeqList.h" template<typename T> //模板聲明 SeqList<T>::SeqList() //在類模板體外定義構造函數 :_array(NULL) , _size(0) {}
///************************** Test.cpp **************************// #include"SeqList.h" int main() { SeqList<int> list1; system("pause"); return 0; }
編譯鏈接會出以下錯誤:
1>------ 已啓動生成: 項目: 4_4, 配置: Debug Win32 ------
1> Test.cpp
1> SeqList.cpp
1> 正在生成代碼...
1>Test.obj : error LNK2019: 無法解析的外部符號 "public: __thiscall SeqList<int>::SeqList<int>(void)" (??0?$SeqList@H@@QAE@XZ),該符號在函數 "void __cdecl Test(void)" (?Test@@YAXXZ) 中被引用
1>E:\CODE\4_4\Debug\4_4.exe : fatal error LNK1120: 1 個無法解析的外部命令
========== 生成: 成功 0 個,失敗 1 個,最新 0 個,跳過 0 個 ==========
這是因爲編譯時SeqList<T>沒有實例化出SeqList<int>實例,所以鏈接時出錯。
到底是怎麼回事呢?
對於模板來說,模板只有被調用的時候纔會被實例化,如果不被調用它是不會實例化的。因此,如果你不調用某模板函數或模板類時,該模板中的許多語法錯誤編譯器是懶得檢查的。
在執行主函數中的SeqList<int> list1語句時,要調用SeqList<int>的構造函數,鏈接器在Test.obj中找不到此構造函數的定義,於是在SeqList.obj文件中找。大家如果認爲SeqList<int>的構造函數的實現代碼就在SeqList.obj中,那就錯了。模板只有被調用的時候纔會被實例化! SeqList.cpp中沒有任何對此構造函數的調用,因此SeqList.obj中是沒有任何有關SeqList<int>構造函數實現的二進制代碼(因爲它沒有被實例化),鏈接器找不到函數實現的二進制代碼,因此只能報錯。
如果我們在SeqList.cpp中添加對SeqList<int>構造函數的調用,編譯器就會將構造函數實例化,產生相應的二進制代碼,這時候鏈接器就會在SeqList.obj文件中找此構造函數的定義。程序編譯就不會出錯了。
當然,我們也可以將聲明和定義放到一個文件 "xxx.hpp" 裏面,來解決這類問題。
附:
程序的編譯流程鏈接:http://iynu17.blog.51cto.com/10734157/1760638