首先,C++標(biāo)準(zhǔn)中提到,一個編譯單元[translation unit]是指一個.cpp文件以及它所include的所有.h文件,.h文件里的代碼將會被擴展到包含它的.cpp文件里,然后編譯器編譯該.cpp文件為一個.obj文件,后者擁有PE[Portable Executable,即windows可執(zhí)行文件]文件格式,并且本身包含的就已經(jīng)是二進制碼,但是,不一定能夠執(zhí)行,因為并不保證其中一定有main函數(shù)。當(dāng)編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢后,再由連接器(linker)進行連接成為一個.exe文件。
舉個例子:
//---------------test.h-------------------//
void f();//這里聲明一個函數(shù)f
//---------------test.cpp--------------//
#include”test.h”
void f()
{
…//do something
} //這里實現(xiàn)出test.h中聲明的f函數(shù)
//---------------main.cpp--------------//
#include”test.h”
int main()
{
f(); //調(diào)用f,f具有外部連接類型
}
在這個例子中,test. cpp和main.cpp各被編譯成為不同的.obj文件[姑且命名為test.obj和main.obj],在main.cpp中,調(diào)用了f函數(shù),然而當(dāng)編譯器編譯main.cpp時,它所僅僅知道的只是main.cpp中所包含的test.h文件中的一個關(guān)于void f();的聲明,所以,編譯器將這里的f看作外部連接類型,即認為它的函數(shù)實現(xiàn)代碼在另一個.obj文件中,本例也就是test.obj,也就是說,main.obj中實際沒有關(guān)于f函數(shù)的哪怕一行二進制代碼,而這些代碼實際存在于test.cpp所編譯成的test.obj中。在main.obj中對f的調(diào)用只會生成一行call指令,像這樣:
call f [C++中這個名字當(dāng)然是經(jīng)過mangling[處理]過的]
在編譯時,這個call指令顯然是錯誤的,因為main.obj中并無一行f的實現(xiàn)代碼。那怎么辦呢?這就是連接器的任務(wù),連接器負責(zé)在其它的.obj中[本例為test.obj]尋找f的實現(xiàn)代碼,找到以后將call f這個指令的調(diào)用地址換成實際的f的函數(shù)進入點地址。需要注意的是:連接器實際上將工程里的.obj“連接”成了一個.exe文件,而它最關(guān)鍵的任務(wù)就是上面說的,尋找一個外部連接符號在另一個.obj中的地址,然后替換原來的“虛假”地址。
這個過程如果說的更深入就是:
call f這行指令其實并不是這樣的,它實際上是所謂的stub,也就是一個
jmp 0x23423[這個地址可能是任意的,然而關(guān)鍵是這個地址上有一行指令來進行真正的call f動作。也就是說,這個.obj文件里面所有對f的調(diào)用都jmp向同一個地址,在后者那兒才真正”call”f。這樣做的好處就是連接器修改地址時只要對后者的call XXX地址作改動就行了。但是,連接器是如何找到f的實際地址的呢[在本例中這處于test.obj中],因為.obj于.exe的格式都是一樣的,在這樣的文件中有一個符號導(dǎo)入表和符號導(dǎo)出表[import table和export table]其中將所有符號和它們的地址關(guān)聯(lián)起來。這樣連接器只要在test.obj的符號導(dǎo)出表中尋找符號f[當(dāng)然C++對f作了mangling]的地址就行了,然后作一些偏移量處理后[因為是將兩個.obj文件合并,當(dāng)然地址會有一定的偏移,這個連接器清楚]寫入main.obj中的符號導(dǎo)入表中f所占有的那一項。
這就是大概的過程。其中關(guān)鍵就是:
編譯main.cpp時,編譯器不知道f的實現(xiàn),所有當(dāng)碰到對它的調(diào)用時只是給出一個指示,指示連接器應(yīng)該為它尋找f的實現(xiàn)體。這也就是說main.obj中沒有關(guān)于f的任何一行二進制代碼。
編譯test.cpp時,編譯器找到了f的實現(xiàn)。于是乎f的實現(xiàn)[二進制代碼]出現(xiàn)在test.obj里。
連接時,連接器在test.obj中找到f的實現(xiàn)代碼[二進制]的地址[通過符號導(dǎo)出表]。然后將main.obj中懸而未決的call XXX地址改成f實際的地址。
完成。
然而,對于模板,你知道,模板函數(shù)的代碼其實并不能直接編譯成二進制代碼,其中要有一個“具現(xiàn)化”的過程。舉個例子:
//----------main.cpp------//
template<class T>
void f(T t)
{}
int main()
{
…//do something
f(10); //call f<int> 編譯器在這里決定給f一個f<int>的具現(xiàn)體
…//do other thing
}
也就是說,如果你在main.cpp文件中沒有調(diào)用過f,f也就得不到具現(xiàn),從而main.obj中也就沒有關(guān)于f的任意一行二進制代碼!!如果你這樣調(diào)用了:
f(10); //f<int>得以具現(xiàn)化出來
f(10.0); //f<double>得以具現(xiàn)化出來
這樣main.obj中也就有了f<int>,f<double>兩個函數(shù)的二進制代碼段。以此類推。
然而具現(xiàn)化要求編譯器知道模板的定義,不是嗎?
看下面的例子:[將模板和它的實現(xiàn)分離]
//-------------test.h----------------//
template<class T>
class A
{
public:
void f(); //這里只是個聲明
};
//---------------test.cpp-------------//
#include”test.h”
template<class T>
void A<T>::f() //模板的實現(xiàn),但注意:不是具現(xiàn)
{
…//do something
}
//---------------main.cpp---------------//
#include”test.h”
int main()
{
A<int> a;
a. f(); //編譯器在這里并不知道A<int>::f的定義,因為它不在test.h里面
//于是編譯器只好寄希望于連接器,希望它能夠在其他.obj里面找到
//A<int>::f的實現(xiàn)體,在本例中就是test.obj,然而,后者中真有A<int>::f的
//二進制代碼嗎?NO!!!因為C++標(biāo)準(zhǔn)明確表示,當(dāng)一個模板不被用到的時
//侯它就不該被具現(xiàn)出來,test.cpp中用到了A<int>::f了嗎?沒有!!所以實
//際上test.cpp編譯出來的test.obj文件中關(guān)于A::f的一行二進制代碼也沒有
//于是連接器就傻眼了,只好給出一個連接錯誤
//但是,如果在test.cpp中寫一個函數(shù),其中調(diào)用A<int>::f,則編譯器會將其//具現(xiàn)出來,因為在這個點上[test.cpp中],編譯器知道模板的定義,所以能//夠具現(xiàn)化,于是,test.obj的符號導(dǎo)出表中就有了A<int>::f這個符號的地
//址,于是連接器就能夠完成任務(wù)。
}
關(guān)鍵是:在分離式編譯的環(huán)境下,編譯器編譯某一個.cpp文件時并不知道另一個.cpp文件的存在,也不會去查找[當(dāng)遇到未決符號時它會寄希望于連接器]。這種模式在沒有模板的情況下運行良好,但遇到模板時就傻眼了,因為模板僅在需要的時候才會具現(xiàn)化出來,所以,當(dāng)編譯器只看到模板的聲明時,它不能具現(xiàn)化該模板,只能創(chuàng)建一個具有外部連接的符號并期待連接器能夠?qū)⒎柕牡刂窙Q議出來。然而當(dāng)實現(xiàn)該模板的.cpp文件中沒有用到模板的具現(xiàn)體時,編譯器懶得去具現(xiàn),所以,整個工程的.obj中就找不到一行模板具現(xiàn)體的二進制代碼,于是連接器也黔驢技窮了。