原文地址html
這裏講下C++文件的編譯過程及其中模板的編譯過程;
一:通常的C++應用程序的編譯過程。
通常說來,C++應用程序的編譯過程分爲三個階段。模板也是同樣的。ios
下面分別描述這幾個階段。
1.include文件的展開。
include文件的展開是一個很簡單的過程,只是將include文件包含的代碼拷貝到包含該文件的cpp文件(或者其它頭文件)中。被展開的cpp文件就成了一個獨立的編譯單元。在一些文章中我看到將.h文件和.cpp文件一塊兒看做一個編譯單元,我以爲這樣的理解有問題。至於緣由,看看下面的幾個注意點就能夠了。
1):沒有被任何的其它cpp文件或者頭文件包含的.h文件將不會被編譯。也不會最終成爲應用程序的一部分。先看一個簡單的例子:windows
在你的應用程序中添加一個test.h文件,如上面所示。可是,不要在任何的其它文件中include該文件。編譯C++工程後你會發現,並無報告上面的代碼錯誤。這說明.h文件自己不是一個編譯單元。只有經過include語句最終包括到了一個.cpp文件中後纔會成爲一個編譯單元。
2):存在一種可能性,即一個cpp文件直接的或者間接的包括了屢次同一個.h文件。下面就是這樣的一種狀況:app
上面的代碼展開後就至關於同時在main.cpp中定義了兩個變量i。所以將發生編譯錯誤。解決辦法是使用#ifndef或者#pragma once宏,使得test.h只能在main.cpp中被包含一次。關於#ifndef和#pragma once請參考這裏。
3):還要注意一點的是,include文件是按照定義順序被展開到cpp文件中的。關於這個,請看下面的示例。編輯器
若是單獨看上面的代碼中,test.h後面須要一個分號才能編譯經過。而test1.h中定義的分號恰好可以補上test.h後面差的那個分號。所以,安這樣的順序定義在main.cpp中後都能正常的編譯經過。雖然在實際項目中並不推薦這樣作,但這個例子可以說明不少關於文件包含的內容。
有的人也許看見了,上面的示例中雖然聲明瞭一個函數,但沒有實現且仍然能經過編譯。這就是下面cpp文件編譯時的內容了。
2.CPP文件的編譯和連接。
你們都知道,C++的編譯實際上分爲編譯和連接兩個階段,因爲這兩個階段聯繫緊密。所以放在一塊兒來講明。在編譯的時候,編譯器會爲每一個cpp文件生成一個obj文件。obj文件擁有PE[Portable Executable,即windows可執行文件]文件格式,而且自己包含的就已是二進制碼,可是,不必定可以執行,由於並不保證其中必定有main函數。當全部的cpp文件都編譯好了以後將會根據須要,將obj文件連接成爲一個exe文件(或者其它形式的庫)。看下面的代碼:函數
注意到22行對foo函數進行了調用。上面的代碼的實際操做過程是編譯器首先爲每一個cpp文件生成了一個obj,這裏是test.obj和main.obj(還有一個stdafx.obj,這是因爲使用了VS編輯器)。但這裏有個問題,雖然test.h對main.cpp是可見的(main.cpp包含了test.h),可是test.cpp對main.cpp並不可見,那麼main.cpp是如何找到foo函數的實現的呢?實際上,在單獨編譯main.cpp文件的時候編譯器並不先去關注foo函數是否已經實現,或者在哪裏實現。它只是把它看做一個外部的連接類型,認爲foo函數的實現應該在另外的一個obj文件中。在22行調用foo的時候,編譯器僅僅使用了一個地址跳轉,即jump 0x23423之類的東西。可是因爲並不知道foo具體存在於哪一個地方,所以只是在jump後面填入了一個假的地址(具體應該是什麼還請高手指教)。而後就繼續編譯下面的代碼。當全部的cpp文件都執行完了以後就進入連接階段。因爲.obj和.exe的格式都是同樣的,在這樣的文件中有一個符號導入表和符號導出表[import table和export table]其中將全部符號和它們的地址關聯起來。這樣鏈接器只要在test.obj的符號導出表中尋找符號foo[固然C++對foo做了mapping]的 地址就好了,而後做一些偏移量處理後[由於是將兩個.obj文件合併,固然地址會有必定的偏移,這個鏈接器清楚]寫入main.obj中的符號導入表中foo所佔有的那一項。這樣foo就能被成功的執行了。
簡要的說來,編譯main.cpp時,編譯器不知道f的實現,全部當碰到對它的調用時只是給出一個指示,指示鏈接器應該爲它尋找f的實現體。這也就是說main.obj中沒有關於f的任何一行二進制代碼。編譯test.cpp時,編譯器找到了f的實現。因而乎foo的實現[二進制代碼]出如今test.obj裏。鏈接時,鏈接器在test.obj中找到foo的實現代碼[二進制]的地址[經過符號導出表]。而後將main.obj中懸而未決的jump XXX地址改爲foo實際的地址。
如今作個假設,foo()的實現並不真正存在會怎麼樣?先看下面的代碼:this
注意上面的代碼,咱們把#include "test.h"註釋掉了,從新聲明瞭一個foo函數。固然也能夠直接使用test.h中的函數聲明。上面的代碼因爲沒有函數實現。按照咱們上面的分析,編譯器在發現foo()的調用的時候並不會報告錯誤,而是期待鏈接器會在其它的obj文件中找到foo的實現。可是,鏈接器最終仍是沒有找到。因而會報告一個連接錯誤。
LINK : 沒有找到 E:\CPP\CPPTemplate\Debug\CPPTemplate.exe 或上一個增量連接沒有生成它;
再看下面的一個例子:spa
這裏只有foo的聲明,咱們把原來的foo的調用也去掉了。上面的代碼能編譯經過。緣由就是因爲沒有調用foo函數,main.cpp沒有真正的去找foo的實現(main.obj內部或者main.obj外部),編譯器也就不會在乎foo是否是已經實現了。
二:模板的編譯過程。
在明白了C++程序的編譯過程後再來看模板的編譯過程。你們知道,模板須要被模板參數實例化成爲一個具體的類或者函數才能使用。可是,類模板成員函數的調用且有一個很重要的特徵,那就是成員函數只有在被調用的時候纔會被初始化。正是因爲這個特徵,使得類模板的代碼不能按照常規的C++類同樣來組織。先看下面的代碼:.net
下面是main.cpp的文件內容:code
1 #include <iostream>
2 #include "testTemplate.h"
3
4 int main()
5 {
6 // 1:實例化一個類模板。
7 // MyClass<int> myClass;
8
9 // 2:調用類模板的成員函數。
10 // myClass.printValue(2);
11
12 std::cout << "Hello world!" << std::endl;
13 return 0;
14 }
注意到註釋掉的兩句代碼。咱們將會按步驟說明模板的編譯過程。
1):咱們將testTemplate.cpp文件從工程中拿掉,即刪除testTemplate.cpp的定義。而後直接編譯上面的文件,能編譯經過。這說明編譯器在展開testTemplate.h後編譯main.cpp文件的時候並無去檢查模板類的實現。它只是記住了有這樣的一個模板聲明。因爲沒有調用模板的成員函數,編譯器連接階段也不會在別的obj文件中去查找類模板的實現代碼。所以上面的代碼沒有問題。
2):把main.cpp文件中,第7行的註釋符號去掉。即加入類模板的實例化代碼。在編譯工程,會發現也可以編譯經過。回想一下這個過程,testTemplate.h被展開,也就是說main.cpp在編譯是就能找到MyClass<T>的聲明。那麼,在編譯第7行的時候就能正常的實例化一個類模板出來。這裏注意:類模板的成員函數只有在調用的時候纔會被實例化。所以,因爲沒有對類模板成員函數的調用,編譯器也就不會去查找類模板的實現代碼。因此,上面的函數能編譯經過。
3):把上面第10行的代碼註釋符號去掉。即加入對類模板成員函數的調用。這個時候再編譯,會提示一個連接錯誤。找不到printValue的實現。道理和上面只有函數的聲明,沒有函數的實現是同樣的。即,編譯器在編譯main.cpp第10行的時候發現了對myClass.PrintValue的調用,這時它在當前文件內部找不到具體的實現,所以會作一個標記,等待連接器在其餘的obj文件中去查找函數實現。一樣,鏈接器也找不到一個包括MyClass<T>::PrintValue聲明的obj文件。所以報告連接錯誤。
4):既然是因爲找不到testTemplate.cpp文件,那麼咱們就將testTemplate.cpp文件包含在工程中。再次編譯,在VS中會提示一個連接錯誤,說找不到外部類型_thiscall MyClass<int>::PrintValue(int)。也許你會以爲很奇怪,咱們已經將testTemplate.cpp文件包含在了工程中了阿。先考慮一個問題,咱們說過模板的編譯其實是一個實例化的過程,它並不編譯產生二進制代碼。另外,模板成員函數也只有在被調用的時候纔會初始化。在testTemplate.cpp文件中,因爲包含了testTemplate.h頭文件,所以這是一個獨立的能夠編譯的類模板。可是,編譯器在編譯這個testTemplate.cpp文件的時候因爲沒有任何成員函數被調用,所以並無實例化PrintValue成員。也許你會說咱們在main.cpp中調用了PrintValue函數。可是要知道testTemplate.cpp和main.cpp是兩個獨立的編譯單元,他們相互間並不知道對方的行爲。所以,testTemplate.cpp在編譯的時候實際上仍是隻編譯了testTemplate.h中的內容,即再次聲明瞭模板,並無實例化PrintValue成員。因此,當main.cpp發現須要PrintValue成員,並在testTemplate.obj中去查找的時候就會找不到目標函數。從而發出一個連接錯誤。
5):因而可知,模板代碼不能按照常規的C/C++代碼來組織。必須得保證使用模板的函數在編譯的時候就能找到模板代碼,從而實例化模板。在網上有不少關於這方面的文章。主要將模板編譯分爲包含編譯和分離編譯。其實,不論是包含編譯仍是分離編譯,都是爲了一個目標:使得實例化模板的時候就能找到相應的模板實現代碼。你們能夠參照這篇文章。最後,做一個小總結。C++應用程序的編譯通常要經歷展開頭文件->編譯cpp文件->連接三個階段。在編譯的時候若是須要外部類型,編譯器會作一個標記,留待鏈接器來處理。鏈接器若是找不到須要的外部類型就會發生連接錯誤。對於模板,單獨的模板代碼是不能被正確編譯的,須要一個實例化器產生一個模板實例後才能編譯。所以,不能寄但願於鏈接器來連接模板的成員函數,必須保證在實例化模板的地方模板代碼是可見的。