轉(zhuǎn)載~
首先,C++標(biāo)準(zhǔn)中提到,一個編譯單元[translation unit]是指一個.cpp文件以及它所include的所有.h文件,.h文件里的代碼將會被擴(kuò)展到包含它的.cpp文件里,然后編譯器編譯該.cpp 文件為一個.obj文件,后者擁有PE[Portable Executable,即windows可執(zhí)行文件]文件格式,并且本身包含的就已經(jīng)是二進(jìn)制碼,但是,不一定能夠執(zhí)行,因?yàn)椴⒉槐WC其中一定有main 函數(shù)。當(dāng)編譯器將一個工程里的所有.cpp文件以分離的方式編譯完畢后,再由連接器(linker)進(jìn)行連接成為一個.exe文件。?
舉個例子:?
//---------------test.h-------------------//
?void f();//這里聲明一個函數(shù)f
//---------------test.cpp--------------//
#i nclude”test.h” void f() { …//do something } //這里實(shí)現(xiàn)出test.h中聲明的f函數(shù)
?//---------------main.cpp--------------//
?? #i nclude”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時(shí),它所僅僅知道的只是main.cpp中所包含的test.h文件中的一個關(guān)于void f();的聲明,所以,編譯器將這里的f看作外部連接類型,即認(rèn)為它的函數(shù)實(shí)現(xiàn)代碼在另一個.obj文件中,本例也就是test.obj,也就是說,main.obj中實(shí)際沒有關(guān)于f函數(shù)的哪怕一行二進(jìn)制代碼,而這些代碼實(shí)際存在于test.cpp所編譯成的test.obj中。在 main.obj中對f的調(diào)用只會生成一行call指令,像這樣: call f [C++中這個名字當(dāng)然是經(jīng)過mangling[處理]過的] 在編譯時(shí),這個call指令顯然是錯誤的,因?yàn)閙ain.obj中并無一行f的實(shí)現(xiàn)代碼。
那怎么辦呢?這就是連接器的任務(wù),連接器負(fù)責(zé)在其它的.obj中 [本例為test.obj]尋找f的實(shí)現(xiàn)代碼,找到以后將call f這個指令的調(diào)用地址換成實(shí)際的f的函數(shù)進(jìn)入點(diǎn)地址。需要注意的是:連接器實(shí)際上將工程里的.obj“連接”成了一個.exe文件,而它最關(guān)鍵的任務(wù)就是上面說的,尋找一個外部連接符號在另一個.obj中的地址,然后替換原來的“虛假”地址。
這個過程如果說的更深入就是: call f這行指令其實(shí)并不是這樣的,它實(shí)際上是所謂的stub,也就是一個 jmp 0x23423[這個地址可能是任意的,然而關(guān)鍵是這個地址上有一行指令來進(jìn)行真正的call f動作。也就是說,這個.obj文件里面所有對f的調(diào)用都jmp向同一個地址,在后者那兒才真正”call”f。這樣做的好處就是連接器修改地址時(shí)只要對后者的call XXX地址作改動就行了。
但是,連接器是如何找到f的實(shí)際地址的呢[在本例中這處于test.obj中],因?yàn)?obj于.exe的格式都是一樣的,在這樣的文件中有一個符號導(dǎo)入表和符號導(dǎo)出表[import table和export table]其中將所有符號和它們的地址關(guān)聯(lián)起來。這樣連接器只要在test.obj的符號導(dǎo)出表中尋找符號f[當(dāng)然C++對f作了mangling]的地址就行了,然后作一些偏移量處理后[因?yàn)槭菍蓚€.obj文件合并,當(dāng)然地址會有一定的偏移,這個連接器清楚]寫入main.obj中的符號導(dǎo)入表中f 所占有的那一項(xiàng)。
這就是大概的過程。其中關(guān)鍵就是: 編譯main.cpp時(shí),編譯器不知道f的實(shí)現(xiàn),所有當(dāng)碰到對它的調(diào)用時(shí)只是給出一個指示,指示連接器應(yīng)該為它尋找f的實(shí)現(xiàn)體。這也就是說main.obj中沒有關(guān)于f的任何一行二進(jìn)制代碼。 編譯test.cpp時(shí),編譯器找到了f的實(shí)現(xiàn)。于是乎f的實(shí)現(xiàn)[二進(jìn)制代碼]出現(xiàn)在test.obj里。 連接時(shí),連接器在test.obj中找到f的實(shí)現(xiàn)代碼[二進(jìn)制]的地址[通過符號導(dǎo)出表]。然后將main.obj中懸而未決的call XXX地址改成f實(shí)際的地址。 完成。
?然而,對于模板,你知道,模板函數(shù)的代碼其實(shí)并不能直接編譯成二進(jìn)制代碼,其中要有一個“具現(xiàn)化”的過程。
舉個例子:
?//----------main.cpp------//
?template <typename T>
void f(T t) {}
?int main()
{
?…//do something f(10); //call f 編譯器在這里決定給f一個f的具現(xiàn)體 …//do other thing
?}
也就是說,如果你在main.cpp文件中沒有調(diào)用過f,f也就得不到具現(xiàn),從而main.obj中也就沒有關(guān)于f的任意一行二進(jìn)制代碼!!
如果你這樣調(diào)用了:
?f(10); //f得以具現(xiàn)化出來 f(10.0); //f得以具現(xiàn)化出來
這樣main.obj中也就有了f,f兩個函數(shù)的二進(jìn)制代碼段。以此類推。
然而具現(xiàn)化要求編譯器知道模板的定義,不是嗎? 看下面的例子:
[將模板和它的實(shí)現(xiàn)分離]
//-------------test.h----------------//
?template <typename T>
?class A
{
?public: void f(); //這里只是個聲明
};
?//---------------test.cpp-------------//
#i nclude”test.h”
template <typename T>
void A::f() //模板的實(shí)現(xiàn),但注意:不是具現(xiàn)
{
…//do something
}
?//---------------main.cpp---------------//
#i nclude”test.h”
?int main()
{
A<..> a;
a. f();
//編譯器在這里并不知道A::f的定義,因?yàn)樗辉趖est.h里面?
//于是編譯器只好寄希望于連接器,希望它能夠在其他.obj里面找到
?//A::f的實(shí)現(xiàn)體,在本例中就是test.obj,然而,后者中真有A::f的?
//二進(jìn)制代碼嗎?NO!!!因?yàn)镃++標(biāo)準(zhǔn)明確表示,當(dāng)一個模板不被用到的時(shí)?
//侯它就不該被具現(xiàn)出來,test.cpp中用到了A::f了嗎?沒有!!所以實(shí)?
//際上test.cpp編譯出來的test.obj文件中關(guān)于A::f的一行二進(jìn)制代碼也沒有?
//于是連接器就傻眼了,只好給出一個連接錯誤?
// 但是,如果在test.cpp中寫一個函數(shù),其中調(diào)用A::f,則編譯器會將其
//具現(xiàn)出來,因?yàn)樵谶@個點(diǎn)上[test.cpp 中],編譯器知道模板的定義,所以能
//夠具現(xiàn)化,于是,test.obj的符號導(dǎo)出表中就有了A::f這個符號的地
//址,于是連接器就能夠完成任務(wù)。
?}
關(guān)鍵是:在分離式編譯的環(huán)境下,編譯器編譯某一個.cpp文件時(shí)并不知道另一個.cpp文件的存在,也不會去查找[當(dāng)遇到未決符號時(shí)它會寄希望于連接器]。這種模式在沒有模板的情況下運(yùn)行良好,但遇到模板時(shí)就傻眼了,因?yàn)槟0鍍H在需要的時(shí)候才會具現(xiàn)化出來,所以,當(dāng)編譯器只看到模板的聲明時(shí),它不能具現(xiàn)化該模板,只能創(chuàng)建一個具有外部連接的符號并期待連接器能夠?qū)⒎柕牡刂窙Q議出來。然而當(dāng)實(shí)現(xiàn)該模板的.cpp文件中沒有用到模板的具現(xiàn)體時(shí),編譯器懶得去具現(xiàn),所以,整個工程的.obj中就找不到一行模板具現(xiàn)體的二進(jìn)制代碼,于是連接器也黔 /////////////////////////////////
http://dev.csdn.net/develop/article/19/19587.shtm C++模板代碼的組織方式 ——包含模式(Inclusion Model) 選擇自 sam1111 的 Blog 關(guān)鍵字 Template Inclusion Model 出處 C++ Template: The Complete Guide 說明:本文譯自《C++ Template: The Complete Guide》一書的第6章中的部分內(nèi)容。
最近看到C++論壇上常有關(guān)于模板的包含模式的帖子,聯(lián)想到自己初學(xué)模板時(shí),也為類似的問題困惑過,因此翻譯此文,希望對初學(xué)者有所幫助。 模板代碼有幾種不同的組織方式,本文介紹其中最流行的一種方式:包含模式。
?
鏈接錯誤 大多數(shù)C/C++程序員向下面這樣組織他們的非模板代碼:
·類和其他類型全部放在頭文件中,這些頭文件具有.hpp(或者.H, .h, .hh, .hxx)擴(kuò)展名。
·對于全局變量和(非內(nèi)聯(lián))函數(shù),只有聲明放在頭文件中,而定義放在點(diǎn)C文件中,這些文件具有.cpp(或者.C, .c, .cc, .cxx)擴(kuò)展名。 這種組織方式工作的很好:它使得在編程時(shí)可以方便地訪問所需的類型定義,并且避免了來自鏈接器的“變量或函數(shù)重復(fù)定義”的錯誤。
由于以上組織方式約定的影響,模板編程新手往往會犯一個同樣的錯誤。下面這一小段程序反映了這種錯誤。就像對待“普通代碼”那樣,我們在頭文件中定義模板:
// basics/myfirst.hpp
?#ifndef MYFIRST_HPP
#define MYFIRST_HPP
?// declaration of template template void print_typeof (T const&);
?#endif
// MYFIRST_HPP print_typeof()聲明了一個簡單的輔助函數(shù)用來打印一些類型信息。
函數(shù)的定義放在點(diǎn)C文件中:
// basics/myfirst.cpp
#i nclude "myfirst.hpp"
// implementation/definition of template
template void print_typeof (T const& x)
?{
std::cout << typeid(x).name() << std::endl;
?}
這個例子使用typeid操作符來打印一個字符串,這個字符串描述了傳入的參數(shù)的類型信息。
?最后,我們在另外一個點(diǎn)C文件中使用我們的模板,在這個文件中模板聲明被#i nclude:
// basics/myfirstmain.cpp
#i nclude "myfirst.hpp"
// use of the template
int main()
?{
double ice = 3.0;
print_typeof(ice); // call function template for type double
}
大部分C++編譯器(Compiler)很可能會接受這個程序,沒有任何問題,但是鏈接器(Linker)大概會報(bào)告一個錯誤,指出缺少函數(shù)print_typeof()的定義。 這個錯誤的原因在于,模板函數(shù)print_typeof()的定義還沒有被具現(xiàn)化(instantiate)。為了具現(xiàn)化一個模板,編譯器必須知道哪一個定義應(yīng)該被具現(xiàn)化,以及使用什么樣的模板參數(shù)來具現(xiàn)化。
不幸的是,在前面的例子中,這兩組信息存在于分開編譯的不同文件中。因此,當(dāng)我們的編譯器看到對print_typeof()的調(diào)用,但是沒有看到此函數(shù)為double類型具現(xiàn)化的定義時(shí),它只是假設(shè)這樣的定義在別處提供,并且創(chuàng)建一個那個定義的引用(鏈接器使用此引用解析)。另一方面,當(dāng)編譯器處理myfirst.cpp時(shí),該文件并沒有任何指示表明它必須為它所包含的特殊參數(shù)具現(xiàn)化模板定義。
?
頭文件中的模板 解決上面這個問題的通用解法是,采用與我們使用宏或者內(nèi)聯(lián)函數(shù)相同的方法:我們將模板的定義包含進(jìn)聲明模板的頭文件中。對于我們的例子,我們可以通過將#i nclude "myfirst.cpp"添加到myfirst.hpp文件尾部,或者在每一個使用我們的模板的點(diǎn)C文件中包含myfirst.cpp文件,來達(dá)到目的。當(dāng)然,還有第三種方法,就是刪掉myfirst.cpp文件,并重寫myfirst.hpp文件,使它包含所有的模板聲明與定義:
?// basics/myfirst2.hpp
#ifndef MYFIRST_HPP
#define MYFIRST_HPP?
// declaration of template template void print_typeof (T const&);
// implementation/definition of template
template void print_typeof (T const& x)
{ s
td::cout << typeid(x).name() << std::endl;
}
#endif
// MYFIRST_HPP
這種組織模板代碼的方式就稱作包含模式。經(jīng)過這樣的調(diào)整,你會發(fā)現(xiàn)我們的程序已經(jīng)能夠正確編譯、鏈接、執(zhí)行了。 從這個方法中我們可以得到一些觀察結(jié)果。最值得注意的一點(diǎn)是,這個方法在相當(dāng)程度上增加了包含myfirst.hpp的開銷。在這個例子中,這種開銷并不是由模板定義自身的尺寸引起的,而是由這樣一個事實(shí)引起的,即我們必須包含我們的模板用到的頭文件,在這個例子中是和。
你會發(fā)現(xiàn)這最終導(dǎo)致了成千上萬行的代碼,因?yàn)橹T如這樣的頭文件也包含了和我們類似的模板定義。 這在實(shí)踐中確實(shí)是一個問題,因?yàn)樗黾恿司幾g器在編譯一個實(shí)際程序時(shí)所需的時(shí)間。我們因此會在以后的章節(jié)中驗(yàn)證其他一些可能的方法來解決這個問題。但無論如何,現(xiàn)實(shí)世界中的程序花一小時(shí)來編譯鏈接已經(jīng)是快的了(我們曾經(jīng)遇到過花費(fèi)數(shù)天時(shí)間來從源碼編譯的程序)。 拋開編譯時(shí)間不談,我們強(qiáng)烈建議如果可能盡量按照包含模式組織模板代碼。
另一個觀察結(jié)果是,非內(nèi)聯(lián)模板函數(shù)與內(nèi)聯(lián)函數(shù)和宏的最重要的不同在于:它并不會在調(diào)用端展開。相反,當(dāng)模板函數(shù)被具現(xiàn)化時(shí),會產(chǎn)生此函數(shù)的一個新的拷貝。由于這是一個自動的過程,編譯器也許會在不同的文件中產(chǎn)生兩個相同的拷貝,從而引起鏈接器報(bào)告一個錯誤。理論上,我們并不關(guān)心這一點(diǎn):這是編譯器設(shè)計(jì)者應(yīng)當(dāng)關(guān)心的事情。實(shí)際上,大多數(shù)時(shí)候一切都運(yùn)轉(zhuǎn)正常,我們根本就不用處理這種狀況。然而,對于那些需要創(chuàng)建自己的庫的大型項(xiàng)目,這個問題偶爾會顯現(xiàn)出來。 最后,需要指出的是,在我們的例子中,應(yīng)用于普通模板函數(shù)的方法同樣適用于模板類的成員函數(shù)和靜態(tài)數(shù)據(jù)成員,以及模板成員函數(shù)。