[轉]c++應用程序文件的編譯過程

原文地址html

這裏講下C++文件的編譯過程及其中模板的編譯過程;

一:通常的C++應用程序的編譯過程。
    通常說來,C++應用程序的編譯過程分爲三個階段。模板也是同樣的。ios

  1. 在cpp文件中展開include文件。
  2. 將每一個cpp文件編譯爲一個對應的obj文件。
  3. 鏈接obj文件成爲一個exe文件(或者其它的庫文件)。

下面分別描述這幾個階段。
1.include文件的展開。
    include文件的展開是一個很簡單的過程,只是將include文件包含的代碼拷貝到包含該文件的cpp文件(或者其它頭文件)中。被展開的cpp文件就成了一個獨立的編譯單元。在一些文章中我看到將.h文件和.cpp文件一塊兒看做一個編譯單元,我以爲這樣的理解有問題。至於緣由,看看下面的幾個注意點就能夠了。
    1):沒有被任何的其它cpp文件或者頭文件包含的.h文件將不會被編譯。也不會最終成爲應用程序的一部分。先看一個簡單的例子:windows

1 ==========test.h文件==========
2 // 注意,後面沒有分號。也就是說,若是編譯的話這裏將產生錯誤。
3 void foo()

在你的應用程序中添加一個test.h文件,如上面所示。可是,不要在任何的其它文件中include該文件。編譯C++工程後你會發現,並無報告上面的代碼錯誤。這說明.h文件自己不是一個編譯單元。只有經過include語句最終包括到了一個.cpp文件中後纔會成爲一個編譯單元。

    2):存在一種可能性,即一個cpp文件直接的或者間接的包括了屢次同一個.h文件。下面就是這樣的一種狀況:app

複製代碼
 1 // ===========test.h============
 2 // 定義一個變量
 3 int i;
 4 
 5 // ===========test1.h===========
 6 // 包含了test.h文件
 7 #include "test.h"
 8 
 9 // ===========main.cpp=========
10 // 這裏同時包含了test.h和test1.h,
11 // 也就是說同時定義了兩個變量i。
12 // 將發生編譯錯誤。
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 }
複製代碼

上面的代碼展開後就至關於同時在main.cpp中定義了兩個變量i。所以將發生編譯錯誤。解決辦法是使用#ifndef或者#pragma once宏,使得test.h只能在main.cpp中被包含一次。關於#ifndef和#pragma once請參考這裏

    3):還要注意一點的是,include文件是按照定義順序被展開到cpp文件中的。關於這個,請看下面的示例。編輯器

複製代碼
 1 // ===========test.h============
 2 // 聲明一個函數。注意後面沒有分號。
 3 void foo()
 4 
 5 // ===========test1.h===========
 6 // 僅寫了一個分號。
 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 }
複製代碼

若是單獨看上面的代碼中,test.h後面須要一個分號才能編譯經過。而test1.h中定義的分號恰好可以補上test.h後面差的那個分號。所以,安這樣的順序定義在main.cpp中後都能正常的編譯經過。雖然在實際項目中並不推薦這樣作,但這個例子可以說明不少關於文件包含的內容。
有的人也許看見了,上面的示例中雖然聲明瞭一個函數,但沒有實現且仍然能經過編譯。這就是下面cpp文件編譯時的內容了。

2.CPP文件的編譯和連接。
你們都知道,C++的編譯實際上分爲編譯和連接兩個階段,因爲這兩個階段聯繫緊密。所以放在一塊兒來講明。在編譯的時候,編譯器會爲每一個cpp文件生成一個obj文件。obj文件擁有PE[Portable Executable,即windows可執行文件]文件格式,而且自己包含的就已是二進制碼,可是,不必定可以執行,由於並不保證其中必定有main函數。當全部的cpp文件都編譯好了以後將會根據須要,將obj文件連接成爲一個exe文件(或者其它形式的庫)。看下面的代碼:函數

複製代碼
 1 // ============test.h===============
 2 // 聲明一個函數。
 3 void foo();
 4 
 5 // ============test.cpp=============
 6 #include "stdafx.h"
 7 #include <iostream>
 8 #include "test.h"
 9 
10 // 實現test.h中定義的函數。
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 }
複製代碼

注意到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

複製代碼
 1 #include "stdafx.h"
 2 //#include "test.h"
 3 
 4 void foo();
 5 
 6 int _tmain(int argc, _TCHAR* argv[])
 7 {
 8     foo();
 9 
10     return 0;
11 }
複製代碼

注意上面的代碼,咱們把#include "test.h"註釋掉了,從新聲明瞭一個foo函數。固然也能夠直接使用test.h中的函數聲明。上面的代碼因爲沒有函數實現。按照咱們上面的分析,編譯器在發現foo()的調用的時候並不會報告錯誤,而是期待鏈接器會在其它的obj文件中找到foo的實現。可是,鏈接器最終仍是沒有找到。因而會報告一個連接錯誤。
LINK : 沒有找到 E:\CPP\CPPTemplate\Debug\CPPTemplate.exe 或上一個增量連接沒有生成它;

再看下面的一個例子:spa

複製代碼
 1 #include "stdafx.h"
 2 //#include "test.h"
 3 
 4 void foo();
 5 
 6 int _tmain(int argc, _TCHAR* argv[])
 7 {
 8     // foo();
 9 
10     return 0;
11 }
複製代碼

這裏只有foo的聲明,咱們把原來的foo的調用也去掉了。上面的代碼能編譯經過。緣由就是因爲沒有調用foo函數,main.cpp沒有真正的去找foo的實現(main.obj內部或者main.obj外部),編譯器也就不會在乎foo是否是已經實現了。


二:模板的編譯過程。
    在明白了C++程序的編譯過程後再來看模板的編譯過程。你們知道,模板須要被模板參數實例化成爲一個具體的類或者函數才能使用。可是,類模板成員函數的調用且有一個很重要的特徵,那就是成員函數只有在被調用的時候纔會被初始化。正是因爲這個特徵,使得類模板的代碼不能按照常規的C++類同樣來組織。先看下面的代碼:.net

複製代碼
 1 // =========testTemplate.h=============
 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 }
複製代碼

下面是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文件->連接三個階段。在編譯的時候若是須要外部類型,編譯器會作一個標記,留待鏈接器來處理。鏈接器若是找不到須要的外部類型就會發生連接錯誤。對於模板,單獨的模板代碼是不能被正確編譯的,須要一個實例化器產生一個模板實例後才能編譯。所以,不能寄但願於鏈接器來連接模板的成員函數,必須保證在實例化模板的地方模板代碼是可見的。

相關文章
相關標籤/搜索