目前正在學(xué)習(xí)《C++ Templates》一書(shū)。在有了一個(gè)初步的概念以后,我覺(jué)得有必要了解一下模板的編譯過(guò)程。而要了解模板的編譯過(guò)程就必須從普通的C++應(yīng)用程序開(kāi)始。下面是我對(duì)C++應(yīng)用程序的編譯過(guò)程的理解。敬請(qǐng)指教!
一:一般的C++應(yīng)用程序的編譯過(guò)程。 一般說(shuō)來(lái),C++應(yīng)用程序的編譯過(guò)程分為三個(gè)階段。模板也是一樣的。
1.include文件的展開(kāi)。 include文件的展開(kāi)是一個(gè)很簡(jiǎn)單的過(guò)程,只是將include文件包含的代碼拷貝到包含該文件的cpp文件(或者其它頭文件)中。被展開(kāi)的cpp文件就成了一個(gè)獨(dú)立的編譯單元。在一些文章中我看到將.h文件和.cpp文件一起看作一個(gè)編譯單元,我覺(jué)得這樣的理解有問(wèn)題。至于原因,看看下面的幾個(gè)注意點(diǎn)就可以了。 1):沒(méi)有被任何的其它c(diǎn)pp文件或者頭文件包含的.h文件將不會(huì)被編譯。也不會(huì)最終成為應(yīng)用程序的一部分。先看一個(gè)簡(jiǎn)單的例子: 1 ==========test.h文件==========
在你的應(yīng)用程序中添加一個(gè)test.h文件,如上面所示。但是,不要在任何的其它文件中include該文件。編譯C++工程后你會(huì)發(fā)現(xiàn),并沒(méi)有報(bào)告上面的代碼錯(cuò)誤。這說(shuō)明.h文件本身不是一個(gè)編譯單元。只有通過(guò)include語(yǔ)句最終包括到了一個(gè).cpp文件中后才會(huì)成為一個(gè)編譯單元。2 // 注意,后面沒(méi)有分號(hào)。也就是說(shuō),如果編譯的話這里將產(chǎn)生錯(cuò)誤。 3 void foo() 2):存在一種可能性,即一個(gè)cpp文件直接的或者間接的包括了多次同一個(gè).h文件。下面就是這樣的一種情況: 1 // ===========test.h============
上面的代碼展開(kāi)后就相當(dāng)于同時(shí)在main.cpp中定義了兩個(gè)變量i。因此將發(fā)生編譯錯(cuò)誤。解決辦法是使用#ifndef或者#pragma once宏,使得test.h只能在main.cpp中被包含一次。關(guān)于#ifndef和#pragma once請(qǐng)參考這里。2 // 定義一個(gè)變量 3 int i; 4 5 // ===========test1.h=========== 6 // 包含了test.h文件 7 #include "test.h" 8 9 // ===========main.cpp========= 10 // 這里同時(shí)包含了test.h和test1.h, 11 // 也就是說(shuō)同時(shí)定義了兩個(gè)變量i。 12 // 將發(fā)生編譯錯(cuò)誤。 13 #include "stdafx.h" 14 #include "test.h" 15 #include "test1.h" 16 17 void foo(); 18 void foo(); 19 20 int _tmain(int argc, _TCHAR* argv[]) 21 { 22 return 0; 23 } 3):還要注意一點(diǎn)的是,include文件是按照定義順序被展開(kāi)到cpp文件中的。關(guān)于這個(gè),請(qǐng)看下面的示例。 1 // ===========test.h============
如果單獨(dú)看上面的代碼中,test.h后面需要一個(gè)分號(hào)才能編譯通過(guò)。而test1.h中定義的分號(hào)剛好能夠補(bǔ)上test.h后面差的那個(gè)分號(hào)。因此,安這樣的順序定義在main.cpp中后都能正常的編譯通過(guò)。雖然在實(shí)際項(xiàng)目中并不推薦這樣做,但這個(gè)例子能夠說(shuō)明很多關(guān)于文件包含的內(nèi)容。2 // 聲明一個(gè)函數(shù)。注意后面沒(méi)有分號(hào)。 3 void foo() 4 5 // ===========test1.h=========== 6 // 僅寫(xiě)了一個(gè)分號(hào)。 7 ; 8 9 // ===========main.cpp========= 10 // 注意,這里按照test.h和test1.h的順序包含了頭文件。 11 #include "stdafx.h" 12 #include "test.h" 13 #include "test1.h" 14 15 int _tmain(int argc, _TCHAR* argv[]) 16 { 17 return 0; 18 } 有的人也許看見(jiàn)了,上面的示例中雖然聲明了一個(gè)函數(shù),但沒(méi)有實(shí)現(xiàn)且仍然能通過(guò)編譯。這就是下面cpp文件編譯時(shí)的內(nèi)容了。 2.CPP文件的編譯和鏈接。 大家都知道,C++的編譯實(shí)際上分為編譯和鏈接兩個(gè)階段,由于這兩個(gè)階段聯(lián)系緊密。因此放在一起來(lái)說(shuō)明。在編譯的時(shí)候,編譯器會(huì)為每個(gè)cpp文件生成一個(gè)obj文件。obj文件擁有PE[Portable Executable,即windows可執(zhí)行文件]文件格式,并且本身包含的就已經(jīng)是二進(jìn)制碼,但是,不一定能夠執(zhí)行,因?yàn)椴⒉槐WC其中一定有main函數(shù)。當(dāng)所有的cpp文件都編譯好了之后將會(huì)根據(jù)需要,將obj文件鏈接成為一個(gè)exe文件(或者其它形式的庫(kù))??聪旅娴拇a: 1 // ============test.h===============
注意到22行對(duì)foo函數(shù)進(jìn)行了調(diào)用。上面的代碼的實(shí)際操作過(guò)程是編譯器首先為每個(gè)cpp文件生成了一個(gè)obj,這里是test.obj和main.obj(還有一個(gè)stdafx.obj,這是由于使用了VS編輯器)。但這里有個(gè)問(wèn)題,雖然test.h對(duì)main.cpp是可見(jiàn)的(main.cpp包含了test.h),但是test.cpp對(duì)main.cpp并不可見(jiàn),那么main.cpp是如何找到foo函數(shù)的實(shí)現(xiàn)的呢?實(shí)際上,在單獨(dú)編譯main.cpp文件的時(shí)候編譯器并不先去關(guān)注foo函數(shù)是否已經(jīng)實(shí)現(xiàn),或者在哪里實(shí)現(xiàn)。它只是把它看作一個(gè)外部的鏈接類型,認(rèn)為foo函數(shù)的實(shí)現(xiàn)應(yīng)該在另外的一個(gè)obj文件中。在22行調(diào)用foo的時(shí)候,編譯器僅僅使用了一個(gè)地址跳轉(zhuǎn),即jump 0x23423之類的東西。但是由于并不知道foo具體存在于哪個(gè)地方,因此只是在jump后面填入了一個(gè)假的地址(具體應(yīng)該是什么還請(qǐng)高手指教)。然后就繼續(xù)編譯下面的代碼。當(dāng)所有的cpp文件都執(zhí)行完了之后就進(jìn)入鏈接階段。由于.obj和.exe的格式都是一樣的,在這樣的文件中有一個(gè)符號(hào)導(dǎo)入表和符號(hào)導(dǎo)出表[import table和export table]其中將所有符號(hào)和它們的地址關(guān)聯(lián)起來(lái)。這樣連接器只要在test.obj的符號(hào)導(dǎo)出表中尋找符號(hào)foo[當(dāng)然C++對(duì)foo作了mapping]的 地址就行了,然后作一些偏移量處理后[因?yàn)槭菍蓚€(gè).obj文件合并,當(dāng)然地址會(huì)有一定的偏移,這個(gè)連接器清楚]寫(xiě)入main.obj中的符號(hào)導(dǎo)入表中foo所占有的那一項(xiàng)。這樣foo就能被成功的執(zhí)行了。2 // 聲明一個(gè)函數(shù)。 3 void foo(); 4 5 // ============test.cpp============= 6 #include "stdafx.h" 7 #include <iostream> 8 #include "test.h" 9 10 // 實(shí)現(xiàn)test.h中定義的函數(shù)。 11 void foo() 12 { 13 std::cout<<"foo function in test has been called."<<std::endl; 14 } 15 16 // ============main.cpp============ 17 #include "stdafx.h" 18 #include "test.h" 19 20 int _tmain(int argc, _TCHAR* argv[]) 21 { 22 foo(); 23 24 return 0; 25 } 簡(jiǎn)要的說(shuō)來(lái),編譯main.cpp時(shí),編譯器不知道f的實(shí)現(xiàn),所有當(dāng)碰到對(duì)它的調(diào)用時(shí)只是給出一個(gè)指示,指示連接器應(yīng)該為它尋找f的實(shí)現(xiàn)體。這也就是說(shuō)main.obj中沒(méi)有關(guān)于f的任何一行二進(jìn)制代碼。編譯test.cpp時(shí),編譯器找到了f的實(shí)現(xiàn)。于是乎foo的實(shí)現(xiàn)[二進(jìn)制代碼]出現(xiàn)在test.obj里。連接時(shí),連接器在test.obj中找到foo的實(shí)現(xiàn)代碼[二進(jìn)制]的地址[通過(guò)符號(hào)導(dǎo)出表]。然后將main.obj中懸而未決的jump XXX地址改成foo實(shí)際的地址。 現(xiàn)在做個(gè)假設(shè),foo()的實(shí)現(xiàn)并不真正存在會(huì)怎么樣?先看下面的代碼: 1 #include "stdafx.h"
注意上面的代碼,我們把#include "test.h"注釋掉了,重新聲明了一個(gè)foo函數(shù)。當(dāng)然也可以直接使用test.h中的函數(shù)聲明。上面的代碼由于沒(méi)有函數(shù)實(shí)現(xiàn)。按照我們上面的分析,編譯器在發(fā)現(xiàn)foo()的調(diào)用的時(shí)候并不會(huì)報(bào)告錯(cuò)誤,而是期待連接器會(huì)在其它的obj文件中找到foo的實(shí)現(xiàn)。但是,連接器最終還是沒(méi)有找到。于是會(huì)報(bào)告一個(gè)鏈接錯(cuò)誤。2 //#include "test.h" 3 4 void foo(); 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 foo(); 9 10 return 0; 11 } LINK : 沒(méi)有找到 E:\CPP\CPPTemplate\Debug\CPPTemplate.exe 或上一個(gè)增量鏈接沒(méi)有生成它; 再看下面的一個(gè)例子: 1 #include "stdafx.h"
這里只有foo的聲明,我們把原來(lái)的foo的調(diào)用也去掉了。上面的代碼能編譯通過(guò)。原因就是由于沒(méi)有調(diào)用foo函數(shù),main.cpp沒(méi)有真正的去找foo的實(shí)現(xiàn)(main.obj內(nèi)部或者main.obj外部),編譯器也就不會(huì)在意foo是不是已經(jīng)實(shí)現(xiàn)了。2 //#include "test.h" 3 4 void foo(); 5 6 int _tmain(int argc, _TCHAR* argv[]) 7 { 8 // foo(); 9 10 return 0; 11 } 二:模板的編譯過(guò)程。 在明白了C++程序的編譯過(guò)程后再來(lái)看模板的編譯過(guò)程。大家知道,模板需要被模板參數(shù)實(shí)例化成為一個(gè)具體的類或者函數(shù)才能使用。但是,類模板成員函數(shù)的調(diào)用且有一個(gè)很重要的特征,那就是成員函數(shù)只有在被調(diào)用的時(shí)候才會(huì)被初始化。正是由于這個(gè)特征,使得類模板的代碼不能按照常規(guī)的C++類一樣來(lái)組織。先看下面的代碼: 1 // =========testTemplate.h=============
下面是main.cpp的文件內(nèi)容:2 template<typename T> 3 class MyClass{ 4 public: 5 void printValue(T value); 6 }; 7 8 // =========testTemplate.cpp=========== 9 #include "stdafx.h" 10 #include "testTemplate.h" 11 12 template<typename T> 13 void MyClass<T>::printValue(T value) 14 { 15 // 16 } 1 #include <iostream> 1):我們將testTemplate.cpp文件從工程中拿掉,即刪除testTemplate.cpp的定義。然后直接編譯上面的文件,能編譯通過(guò)。這說(shuō)明編譯器在展開(kāi)testTemplate.h后編譯main.cpp文件的時(shí)候并沒(méi)有去檢查模板類的實(shí)現(xiàn)。它只是記住了有這樣的一個(gè)模板聲明。由于沒(méi)有調(diào)用模板的成員函數(shù),編譯器鏈接階段也不會(huì)在別的obj文件中去查找類模板的實(shí)現(xiàn)代碼。因此上面的代碼沒(méi)有問(wèn)題。 2):把main.cpp文件中,第7行的注釋符號(hào)去掉。即加入類模板的實(shí)例化代碼。在編譯工程,會(huì)發(fā)現(xiàn)也能夠編譯通過(guò)?;叵胍幌逻@個(gè)過(guò)程,testTemplate.h被展開(kāi),也就是說(shuō)main.cpp在編譯是就能找到MyClass<T>的聲明。那么,在編譯第7行的時(shí)候就能正常的實(shí)例化一個(gè)類模板出來(lái)。這里注意:類模板的成員函數(shù)只有在調(diào)用的時(shí)候才會(huì)被實(shí)例化。因此,由于沒(méi)有對(duì)類模板成員函數(shù)的調(diào)用,編譯器也就不會(huì)去查找類模板的實(shí)現(xiàn)代碼。所以,上面的函數(shù)能編譯通過(guò)。 3):把上面第10行的代碼注釋符號(hào)去掉。即加入對(duì)類模板成員函數(shù)的調(diào)用。這個(gè)時(shí)候再編譯,會(huì)提示一個(gè)鏈接錯(cuò)誤。找不到printValue的實(shí)現(xiàn)。道理和上面只有函數(shù)的聲明,沒(méi)有函數(shù)的實(shí)現(xiàn)是一樣的。即,編譯器在編譯main.cpp第10行的時(shí)候發(fā)現(xiàn)了對(duì)myClass.PrintValue的調(diào)用,這時(shí)它在當(dāng)前文件內(nèi)部找不到具體的實(shí)現(xiàn),因此會(huì)做一個(gè)標(biāo)記,等待鏈接器在其他的obj文件中去查找函數(shù)實(shí)現(xiàn)。同樣,連接器也找不到一個(gè)包括MyClass<T>::PrintValue聲明的obj文件。因此報(bào)告鏈接錯(cuò)誤。 4):既然是由于找不到testTemplate.cpp文件,那么我們就將testTemplate.cpp文件包含在工程中。再次編譯,在VS中會(huì)提示一個(gè)鏈接錯(cuò)誤,說(shuō)找不到外部類型_thiscall MyClass<int>::PrintValue(int)。也許你會(huì)覺(jué)得很奇怪,我們已經(jīng)將testTemplate.cpp文件包含在了工程中了阿。先考慮一個(gè)問(wèn)題,我們說(shuō)過(guò)模板的編譯實(shí)際上是一個(gè)實(shí)例化的過(guò)程,它并不編譯產(chǎn)生二進(jìn)制代碼。另外,模板成員函數(shù)也只有在被調(diào)用的時(shí)候才會(huì)初始化。在testTemplate.cpp文件中,由于包含了testTemplate.h頭文件,因此這是一個(gè)獨(dú)立的可以編譯的類模板。但是,編譯器在編譯這個(gè)testTemplate.cpp文件的時(shí)候由于沒(méi)有任何成員函數(shù)被調(diào)用,因此并沒(méi)有實(shí)例化PrintValue成員。也許你會(huì)說(shuō)我們?cè)趍ain.cpp中調(diào)用了PrintValue函數(shù)。但是要知道testTemplate.cpp和main.cpp是兩個(gè)獨(dú)立的編譯單元,他們相互間并不知道對(duì)方的行為。因此,testTemplate.cpp在編譯的時(shí)候?qū)嶋H上還是只編譯了testTemplate.h中的內(nèi)容,即再次聲明了模板,并沒(méi)有實(shí)例化PrintValue成員。所以,當(dāng)main.cpp發(fā)現(xiàn)需要PrintValue成員,并在testTemplate.obj中去查找的時(shí)候就會(huì)找不到目標(biāo)函數(shù)。從而發(fā)出一個(gè)鏈接錯(cuò)誤。 5):由此可見(jiàn),模板代碼不能按照常規(guī)的C/C++代碼來(lái)組織。必須得保證使用模板的函數(shù)在編譯的時(shí)候就能找到模板代碼,從而實(shí)例化模板。在網(wǎng)上有很多關(guān)于這方面的文章。主要將模板編譯分為包含編譯和分離編譯。其實(shí),不管是包含編譯還是分離編譯,都是為了一個(gè)目標(biāo):使得實(shí)例化模板的時(shí)候就能找到相應(yīng)的模板實(shí)現(xiàn)代碼。大家可以參照這篇文章。 最后,作一個(gè)小總結(jié)。C++應(yīng)用程序的編譯一般要經(jīng)歷展開(kāi)頭文件->編譯cpp文件->鏈接三個(gè)階段。在編譯的時(shí)候如果需要外部類型,編譯器會(huì)做一個(gè)標(biāo)記,留待連接器來(lái)處理。連接器如果找不到需要的外部類型就會(huì)發(fā)生鏈接錯(cuò)誤。對(duì)于模板,單獨(dú)的模板代碼是不能被正確編譯的,需要一個(gè)實(shí)例化器產(chǎn)生一個(gè)模板實(shí)例后才能編譯。因此,不能寄希望于連接器來(lái)鏈接模板的成員函數(shù),必須保證在實(shí)例化模板的地方模板代碼是可見(jiàn)的。 上面只是我的個(gè)人理解。有不對(duì)的地方還請(qǐng)賜教! |
|
來(lái)自: 深淺不一 > 《我的圖書(shū)館》