C++ 模板類的聲明和定義都要放在 .h (頭)文件中的原因

首先乙嘀,一個編譯單元(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文件的原因

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末崇败,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子肩祥,更是在濱河造成了極大的恐慌后室,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,835評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件混狠,死亡現(xiàn)場離奇詭異岸霹,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)将饺,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,900評論 2 383
  • 文/潘曉璐 我一進(jìn)店門贡避,熙熙樓的掌柜王于貴愁眉苦臉地迎上來痛黎,“玉大人,你說我怎么就攤上這事刮吧【艘荩” “怎么了?”我有些...
    開封第一講書人閱讀 156,481評論 0 345
  • 文/不壞的土叔 我叫張陵皇筛,是天一觀的道長琉历。 經(jīng)常有香客問我,道長水醋,這世上最難降的妖魔是什么旗笔? 我笑而不...
    開封第一講書人閱讀 56,303評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮拄踪,結(jié)果婚禮上蝇恶,老公的妹妹穿的比我還像新娘。我一直安慰自己惶桐,他們只是感情好撮弧,可當(dāng)我...
    茶點故事閱讀 65,375評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著姚糊,像睡著了一般贿衍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上救恨,一...
    開封第一講書人閱讀 49,729評論 1 289
  • 那天贸辈,我揣著相機(jī)與錄音,去河邊找鬼肠槽。 笑死擎淤,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的秸仙。 我是一名探鬼主播嘴拢,決...
    沈念sama閱讀 38,877評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寂纪!你這毒婦竟也來了席吴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,633評論 0 266
  • 序言:老撾萬榮一對情侶失蹤弊攘,失蹤者是張志新(化名)和其女友劉穎抢腐,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體襟交,經(jīng)...
    沈念sama閱讀 44,088評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡迈倍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,443評論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捣域。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啼染。...
    茶點故事閱讀 38,563評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡宴合,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出迹鹅,到底是詐尸還是另有隱情卦洽,我是刑警寧澤,帶...
    沈念sama閱讀 34,251評論 4 328
  • 正文 年R本政府宣布斜棚,位于F島的核電站阀蒂,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏弟蚀。R本人自食惡果不足惜蚤霞,卻給世界環(huán)境...
    茶點故事閱讀 39,827評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望义钉。 院中可真熱鬧昧绣,春花似錦、人聲如沸捶闸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,712評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽删壮。三九已至贪绘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間醉锅,已是汗流浹背兔簇。 一陣腳步聲響...
    開封第一講書人閱讀 31,943評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留硬耍,地道東北人。 一個月前我還...
    沈念sama閱讀 46,240評論 2 360
  • 正文 我出身青樓边酒,卻偏偏與公主長得像经柴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子墩朦,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,435評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 概述:聲明是將一個名稱引入一個程序.定義提供了一個實體在程序中的唯一描述.聲明在單個作用域內(nèi)可以重復(fù)多次(類成員除...
    抓兔子的貓閱讀 618評論 0 3
  • mean to add the formatted="false" attribute?.[ 46% 47325/...
    ProZoom閱讀 2,693評論 0 3
  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對...
    cosWriter閱讀 11,090評論 1 32
  • 好久沒看C了坯认,本來就忘得一干二凈的,一臉懵逼的看著zend氓涣。 關(guān)于.c 和 .h 的區(qū)別 子程序不要定義在.h中牛哺。...
    仇諾伊閱讀 4,745評論 2 3
  • 你的財富+夢想=情感觸發(fā)器 情感觸發(fā)器是指:能觸發(fā)你內(nèi)心最為強(qiáng)烈的情感能量電荷的一些特殊的人引润,事,物痒玩,經(jīng)歷淳附,信息和...
    大果果ly閱讀 166評論 0 0