首先乙嘀,一個編譯單元(translation unit)是指一個 .cpp 文件以及它所 #include 的所有 .h 文件摧茴,.h 文件里的代碼將會被擴(kuò)展到包含它的 .cpp 文件里,然后編譯器編譯該 .cpp 文件為一個 .obj 文件(假定我們的平臺是 win32),后者擁有 PE(Portable Executable,即 windows 可執(zhí)行文件)文件格式,并且本身包含的就已經(jīng)是二進(jìn)制碼袖肥,但是不一定能夠執(zhí)行,因為并不保證其中一定有 main 函數(shù)振劳。當(dāng)編譯器將一個工程里的所有 .cpp 文件以分離的方式編譯完畢后椎组,再由連接器(linker)進(jìn)行連接成為一個 .exe 文件。
例如:
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
void testOutput();
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in Test.cpp start
#include "Test.h"
#include <QDebug>
void testOutput()
{
qDebug() << "This is a test output.";
}
\\ ------- in Test.cpp end
\\ ------- in main.cpp start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testOutput();
return a.exec();
}
\\ ------- in main.cpp end
在這個例子中历恐,Test.cpp 和main.cpp 各自被編譯成不同的 .obj 文件(假設(shè)命名為 Test.obj 和 main.obj)寸癌,在 main.cpp 中,調(diào)用了 testOutput 函數(shù)弱贼,然而當(dāng)編譯器編譯 main.cpp 時蒸苇,它所僅僅知道的只是 main.cpp 中所包含的 Test.h 文件中的一個關(guān)于 void testOutput(); 的聲明,所以吮旅,編譯器將這里的 testOutput 看作外部連接類型溪烤,即認(rèn)為它的函數(shù)實現(xiàn)代碼在另一個 .obj 文件(Test.obj)中,也就是說,main.obj 中實際沒有關(guān)于 testOutput 函數(shù)的哪怕一行二進(jìn)制代碼檬嘀,而這些代碼實際存在于 Test.cpp 所編譯成的 Test.obj 中槽驶。在 main.obj 中對 testOutput 的調(diào)用只會生成一行 call 指令,像這樣:
call testOutput // 這里假設(shè)假設(shè)叫 testOutput
在編譯時鸳兽,這個 call 指令顯然是錯誤的掂铐,因為 main.obj 中并無一行 testOutput 的實現(xiàn)代碼。那怎么辦呢贸铜?這就是連接器的任務(wù)堡纬,連接器負(fù)責(zé)在其它的 .obj 中(本例為 Test.obj)尋找 testOutput 的實現(xiàn)代碼聂受,找到以后將 call testOutput 這個指令的調(diào)用地址換成實際的 testOutput 的函數(shù)進(jìn)入點地址蒿秦。需要注意的是:連接器實際上將工程里的 .obj “連接” 成了一個 .exe 文件,而它最關(guān)鍵的任務(wù)就是上面說的蛋济,尋找一個外部連接符號在另一個 .obj 中的地址棍鳖,然后替換原來的“虛假”地址。
這個過程如果說的更深入就是:
call testOutput 這行指令其實并不是這樣的碗旅,它實際上是所謂的 stub渡处,也就是一個 jmp 0xABCDEF。這個地址可能是任意的祟辟,然而關(guān)鍵是這個地址上有一行指令來進(jìn)行真正的 call testOutput 動作医瘫。也就是說,這個 .obj 文件里面所有對 testOutput 的調(diào)用都 jmp 向同一個地址旧困,在后者那兒才真正 “call” testOutput醇份。這樣做的好處就是連接器修改地址時只要對后者的 call XXX 地址作改動就行了。但是吼具,連接器是如何找到 testOutput 的實際地址的呢(在本例中這處于 Test.obj 中)僚纷,因為 .obj 與 .exe 的格式是一樣的,在這樣的文件中有一個符號導(dǎo)入表和符號導(dǎo)出表(import table 和 export table)其中將所有符號和它們的地址關(guān)聯(lián)起來拗盒。這樣連接器只要在 Test.obj 的符號導(dǎo)出表中尋找符號 testOutput(假設(shè)符號叫 testOutput)的地址就行了怖竭,然后作一些偏移量處理后(因為是將兩個 .obj 文件合并,當(dāng)然地址會有一定的偏移陡蝇,這個連接器清楚)寫入 main.obj 中的符號導(dǎo)入表中 testOutput 所占有的那一項即可痊臭。
這就是大概的過程。其中關(guān)鍵就是:
1. 編譯 main.cpp 時登夫,編譯器不知道 testOutput 的實現(xiàn)广匙,所以當(dāng)碰到對它的調(diào)用時只是給出一個指示,指示連接器應(yīng)該為它尋找 testOutput 的實現(xiàn)體悼嫉。這也就是說 main.obj 中沒有關(guān)于 testOutput 的任何一行二進(jìn)制代碼艇潭。
2. 編譯 Test.cpp 時,編譯器找到了 testOutput 的實現(xiàn)。于是乎f的實現(xiàn)(二進(jìn)制代碼)出現(xiàn)在 Test.obj里蹋凝。
3. 連接時鲁纠,連接器在 Test.obj 中找到f的實現(xiàn)代碼(二進(jìn)制)的地址(通過符號導(dǎo)出表)。然后將 main.obj 中[懸而未決]的 call XXX 地址改成 testOutput 實際的地址鳍寂。完成改含。
然而,對于模板迄汛,我們知道捍壤,模板函數(shù)的代碼其實并不能直接編譯成二進(jìn)制代碼,其中要有一個“實例化”的過程鞍爱。舉個例子:
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
#include <QDebug>
template<typename T>
void testOutput(T test) {
qDebug() << "This is a test output: " << test;
}
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in main.h start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
testOutput("haha");
return a.exec();
}
\\ ------- in main.h end
也就是說鹃觉,如果你在 main.cpp 文件中沒有調(diào)用過 testOutput,testOutput 也就得不到實例化睹逃,從而 main.obj 中也就沒有關(guān)于 testOutput 的任意一行二進(jìn)制代碼盗扇!如果你這樣調(diào)用了:
f(10); // f<int>得以實例化出來
f(10.0); // f<double>得以實例化出來
這樣 main.obj 中也就有了 testOutput<int>,testOutput<double> 兩個函數(shù)的二進(jìn)制代碼段沉填。[以此類推]
然而實例化要求編譯器知道模板的定義疗隶,不是嗎?
看下面的例子(將模板的聲明和實現(xiàn)分離):
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
template <typename T>
class Test
{
public:
Test(T v)
: t (v)
{}
void testOutput();
private:
T t;
};
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in Test.cpp start
#include "Test.h"
#include <QDebug>
template<typename T>
void Test<T>::testOutput()
{
qDebug() << "This is a test output: " << t;
}
\\ ------- in Test.cpp end
\\ ------- in main.cpp start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test<int> test(13);
test.testOutput(); // #1
return a.exec();
}
\\ ------- in main.cpp end
此時翼闹,運(yùn)行的話斑鼻,會提示:
main.obj:-1: error: LNK2019: 無法解析的外部符號 "public: void __thiscall Test<int>::testOutput(void)" (?testOutput@?$Test@H@@QAEXXZ),該符號在函數(shù) _main 中被引用
編譯器在#1處并不知道 Test<int>::testOutput 的定義猎荠,因為它不在 Test.h 里面坚弱,于是編譯器只好寄希望于連接器,希望它能夠在其他 .obj 里面找到 Test<int>::testOutput 的實例法牲,在本例中就是 Test.obj史汗,然而,后者中真有 Test<int>::testOutput 的二進(jìn)制代碼嗎拒垃?NOMW病!悼瓮!因為 C++ 標(biāo)準(zhǔn)明確表示戈毒,當(dāng)一個模板不被用到的時侯它就不該被實例化出來,Test.cpp 中用到了 Test<int>::testOutput 了嗎横堡?沒有B袷小!所以實際上 Test.cpp 編譯出來的 Test.obj 文件中關(guān)于 Test<int>::testOutput 一行二進(jìn)制代碼也沒有命贴,于是連接器就傻眼了道宅,只好給出一個連接錯誤食听。但是,如果在 Test.cpp 中寫一個函數(shù)污茵,使其調(diào)用 Test<int>::testOutput樱报,則編譯器會將其實例化出來,因為在(Test.cpp 中的)這個點上泞当,編譯器知道模板的定義迹蛤,所以能夠?qū)嵗谑墙笫浚琓est.obj 的符號導(dǎo)出表中就有了 Test<int>::testOutput 這個符號的地址盗飒,于是連接器就能夠完成任務(wù)。
關(guān)鍵是:在分離式編譯的環(huán)境下陋桂,編譯器編譯某一個 .cpp 文件時并不知道另一個 .cpp 文件的存在逆趣,也不會去查找(當(dāng)遇到未決符號時它會寄希望于連接器)。這種模式在沒有模板的情況下運(yùn)行良好章喉,但遇到模板時就傻眼了汗贫,因為模板僅在需要的時候才會實例化出來身坐,所以秸脱,當(dāng)編譯器只看到模板的聲明時,它不能實例化該模板部蛇,只能創(chuàng)建一個具有外部連接的符號并期待連接器能夠?qū)⒎柕牡刂窙Q議出來摊唇。然而當(dāng)實現(xiàn)該模板的 .cpp 文件中沒有用到模板的實例時,編譯器懶得去實例化涯鲁,所以巷查,整個工程的 .obj 中就找不到一行模板實例的二進(jìn)制代碼,于是連接器也黔驢技窮了抹腿。
所以岛请,目前我們必須要將 C++ 模板類的聲明和定義都要放在.h文件中:
\\ ------- in Test.h start
#ifndef TEST_H
#define TEST_H
#include <QDebug>
template <typename T>
class Test
{
public:
Test(T v)
: t (v)
{}
void testOutput();
private:
T t;
};
template<typename T>
void Test<T>::testOutput()
{
qDebug() << "This is a test output: " << t;
}
#endif // TEST_H
\\ ------- in Test.h end
\\ ------- in main.cpp start
#include <QCoreApplication>
#include "ZDS/Test.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test<int> test(13);
test.testOutput(); // #2
return a.exec();
}
\\ ------- in main.cpp end
此時,上述代碼才能夠順利運(yùn)行警绩。
注:本文參考自 《c++ 模板類 聲明和定義都放在.h文件的原因》