模板不能分離編譯及相關問題

首先我們要知道程序跑起來的過程

  • 預處理:test.c -> test.i(頭文件展開、宏替換、去掉註釋)
  • 編譯:test.i -> test.s (語法檢查、生成彙編代碼)
  • 彙編:test.s -> test.o (把彙編代碼轉換成機器碼)
  • 鏈接:test.o ->.exe (生成可執行程序)

1 分離編譯

1.1理解概念:

分離編譯就是一個程序的由很多個源文件共同實現,而每個源文件單獨編譯生成目標文件,最後將所有的目標文件連接起來形成單一的可執行文件的過程

//聲明文件
//test.h
void Swap();//函數聲明

//定義文件
//test.cpp
#include "test.h"
void Swap(...)
{
	...
	...
}

//測試文件
//main.cpp
int main()
{
	int a = 1;
	int b = 2;
	Swap(a,b);//函數調用
	return 0;
}

一個編譯單元是指一個.cpp文件以及它所#include的所有.h文件,.h文件裏面的代碼會被擴展到包含它的.cpp文件裏面,然後編譯器編譯該.cpp文件爲一個.obj文件(平臺是win),當編譯器將一個工程裏面所有的.cpp文件以分離的方式編譯完成後,再由連接器進行連接成爲一個.exe文件
在這個例子裏面,test.cpp和main.cpp會被編譯程不同的.obj文件,(這裏就命名爲test.obj和main.obj文件),在main.cpp中,調用了Swap函數,然而當編譯器編譯main.cpp文件時,它只知道main.cpp包含test.h文件中的一個Swap函數的聲明,所以編譯器將這個的Swap看作是外部連接類型,就是認爲它的函數實現是在另外一個.obj文件中,在這裏也就是test.obj,也就是說main.obj中實際沒有關於Swap函數的一行二進制代碼,而這些代碼實際存在於test.cpp所編譯生成的test.obj中,在main.obj中對Swap的調用只會生成一行call指令:(類似這樣的)
call Swap【地址】
連接器的作用:尋找一個外部連接符號在另一個.obj中的地址,替換原來的“虛假”地址
在編譯時,這個call指令顯然是錯誤的,因爲main.obj中並沒有一行Swap實現代碼,那麼這時候就是連接器的作用,連接器負責在其他的.obj文件中尋找Swap的實現代碼,找到以後將call指令後面的調用地址換成實際的Swap函數的進入點地址。
那麼連接器是如何找到的,因爲在.obj文件中和.exe文件一樣有一個符號導入表和符號導出表,其中將所有符號和它們的地址關聯起來,這樣連接器只需要在test.obj文件中的符號導出表中尋找符號Swap的地址就可以了,然後做一些偏移處理後(因爲將兩個.obj文件合併,當然地址會有一定的偏移)寫入main.obj中的符號導入表中Swap所佔有的那一項。

總結的講就是:
編譯main.cpp時,編譯器不知道Swap函數的實現,所以當碰到對它的調用只是給出一個指示,指示連接器應該爲它尋找Swap的實現體,這也就是說main.obj中沒有關於Swap的任何一行二進制代碼

編譯test.cpp時,編譯器找到了Swap的實現,於是Swap的實現(二進制代碼)出現在test.obj中。

連接時,連接器在test.obj中找到Swap的實現代碼的地址(通過符號導出表)。然會將main.obj中懸而未決的call XXX地址改成Swap實際的地址。

1.2分離編譯優點

1.有錯誤可以迅速找到
2.實現模塊多用
但是當模板分離編譯的時候就有問題了

//test.h
template<class T>
void Swap(T& a, T& b);

//test.cpp
#include "test.h"
//template void Swap<int>(int &a, int &b);//顯式實例化
template<class T>
void Swap(T& a, T& b)
{
	T temp;
	temp = a;
	a = b;
	b = temp;
}

//main.cpp
#include "test.h"
int main()
{
	int a = 1;
	int b = 2;
	Swap(a, b);
	return 0;
}

當運行程序時:(vs2013)
在這裏插入圖片描述
編譯器在調用Swap(a,b);的時候並不知道這個函數的定義,因爲它不在test.h裏面,所以編譯器只能靠連接器,希望能夠在.obj裏面找Swap的實例,在這裏就是test.obj,然而,在test.obj中並沒有Swap的二進制代碼,這裏並沒有,因爲在C++標準明確表示,當一個模板被用到的時候它就不該被實例化出來,test.cpp中沒有用到Swap函數,所以test.cpp編譯出來的test.obj文件中沒有一行關於Swap的二進制代碼,所以這時候連接器也沒辦法,只好報錯。如上圖。但是這時候並不是沒有辦法,我們可以在test.cpp中實例化從而使其在.obj文件中生成關於Swap的二進制代碼,從而也就是test.obj的符號導出表中就有了福關於Swap的地址。當然也是因爲test.cpp中知道模板的定義才能夠實例化。

因爲模板僅在需要的時候才實例化出來,所以當編譯器看到模板的聲明時,他不能實例化該模板,只能創建一個具有外部連接的符號並等待連接器能夠將符號的地址填進來。

2 實例化

C++只有模板顯式實例化、隱式實例化、特化

2.1隱式實例化

int main()
{
	...
	Swap<int>(a,b);
	...
}

它會在運行到這裏的時候才生成相應的實例,很顯然影響效率

2.2 顯式實例化

前面提到隱式實例化可能影響效率,所以需要提高效率的顯式實例化,顯式實例化在編譯期間就會生成實例,方法如下:

template void Swap<int>(int& a,int& b);

這樣就不會影響運行時的效率,但編譯時間會隨之增加。但是這僅限於是在Vs中,因爲在linux操作系統下會產生這樣的錯誤:
main.cpp:(.text+0x25):對‘void Swap(int&, int&)’未定義的引用
collect2: 錯誤:ld 返回 1

3 特化

這個Swap可以處理一些基本內置類型如int、long等等,但是如果象處理用戶自定義的就不行了,特化就是爲了解決這個問題而出現的:

template <> void Swap<job>(job a,job b){...}

其中job就是用戶自定義的類型

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章