程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> 為什麼C++編譯器不能支持對模板的分離式編譯

為什麼C++編譯器不能支持對模板的分離式編譯

編輯:C++入門知識

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved