C/C++ 之 应用程序的编译过程
一、C/C++語言由源代碼生成的各階段如下
源程序->編譯預(yù)處理->編譯->優(yōu)化程序->匯編程序->鏈接程序->可執(zhí)行文件
其中編譯預(yù)處理階段,讀取源程序,對其中的偽指令(以#開頭的指令)和特殊符號進行處理。或者說是掃描源代碼,對其進行初步的轉(zhuǎn)換,產(chǎn)生新的源代碼提供給編譯器。預(yù)處理過程先于編譯器對源代碼進行處理。下面使用VC編譯為例,部分內(nèi)容在gcc中不支持
預(yù)處理階段
盡管在目前絕大多數(shù)編譯器都包含了預(yù)處理程序,但通常認為它們是獨立于編譯器的。預(yù)處理過程讀入源代碼,檢查包含預(yù)處理指令的語句和宏定義,并對源代碼進行響應(yīng)的轉(zhuǎn)換。預(yù)處理過程還會刪除程序中的注釋和多余的空白字符。
一、偽指令(或預(yù)處理指令)定義
預(yù)處理指令是以#號開頭的代碼行。#號必須是該行除了任何空白字符外的第一個字符。#后是指令關(guān)鍵字,在關(guān)鍵字和#號之間允許存在任意個數(shù)的空白字符。整行語句構(gòu)成了一條預(yù)處理指令,該指令將在編譯器進行編譯之前對源代碼做某些轉(zhuǎn)換。
二、預(yù)處理指令主要包括以下四個方面:
1、宏定義指令#define
宏定義了一個代表特定內(nèi)容的標識符。預(yù)處理過程會把源代碼中出現(xiàn)的宏標識符替換成宏定義時的值。宏定義最常見的用法是定義代表某個值的全局符號。宏的第二種用法是定義帶參數(shù)的宏(宏函數(shù)),這樣的宏可以像函數(shù)一樣被調(diào)用,但它是在調(diào)用語句處展開宏,并用調(diào)用時的實際參數(shù)來代替定義中的形式參數(shù)。
用法一:
#define PI 3.1415926
注意:
(1)作為一種約定,習(xí)慣上總是全部用大寫字母來定義宏,這樣易于把程序的宏標識符和一般變量標識符區(qū)別開來。使用宏的好處有:
一是使用方便。
二是定義的宏有了意義,可讀性強。
三是容易修改。
(2)宏表示的值可以是一個常量表達式,允許宏嵌套(必須在前面已定義)。例如:
#define ONE 1
#define TWO 2
#define SUM (ONE+TWO)
這里需要注意括號的使用,盡管它們并不是必須的。但出于謹慎考慮,還是應(yīng)該加上括號的。預(yù)處理僅是簡單的字符替換,并不會處理優(yōu)先級。
(3)宏還可以代表一個字符串常量,例如:
#define VERSION "Version 1.0"
(4)帶參數(shù)的#define指令(宏函數(shù))
帶參數(shù)的宏和函數(shù)調(diào)用看起來有些相似。看一個例子:
#define SUM(x,y) (x+y)
可以時任何數(shù)字表達式甚至函數(shù)調(diào)用來代替參數(shù)x、y。這里再次提醒大家注意括號的使用。宏展開后完全包含在一對括號中,而且參數(shù)也包含在括號中,這樣就保證了宏和參數(shù)的完整性。看一個用法:
sum = SUM(2, 3); 展開后變?yōu)?sum = (2 + 2);
1.2 #運算符
出現(xiàn)在宏定義中的#運算符把跟在其后的參數(shù)轉(zhuǎn)換成一個字符串。有時把這種用法的#稱為字符串化運算符。例如:
宏定義中的#運算符告訴預(yù)處理程序,把源代碼中任何傳遞給該宏的參數(shù)轉(zhuǎn)換成一個字符串。所以輸出應(yīng)該是12345。
1.3 ##運算符(很少用)
##運算符用于把參數(shù)連接到一起。預(yù)處理程序把出現(xiàn)在##兩側(cè)的參數(shù)合并成一個符號。看下面的例子:
2、條件編譯指令。
程序員可以通過定義不同的宏來決定編譯程序?qū)δ男┐a進行處理。條件編譯指令將決定那些代碼被編譯,而哪些是不被編譯的。可以根據(jù)表達式的值或者某個特定的宏是否被定義來確定編譯條件。這些指令包括:#if/#ifdef/#ifndef/#else/#elif/#endif
#if指令檢測跟在制造另關(guān)鍵字后的常量表達式。如果表達式為真,則編譯后面的代碼,直到出現(xiàn)#else、#elif或#endif為止;否則就不編譯。
#endif用于終止#if預(yù)處理指令。
#else指令用于某個#if指令之后,當前面的#if指令的條件不為真時,就編譯#else后面的代碼。
#elif預(yù)處理指令綜合了#else和#if指令的作用。
#ifdef和#ifndef這二者主要用于防止重復(fù)包含。我們一般在.h頭文件前面加上這么一段:
#ifndef FUNCA_H
#define FUNCA_H
????//頭文件內(nèi)容
#endif
這樣,如果a.h包含了funcA.h,b.h包含了a.h、funcA.h,重復(fù)包含,會出現(xiàn)一些type redefination之類的錯誤。
3、特殊符號。
預(yù)編譯程序可以識別一些特殊的符號。預(yù)編譯程序?qū)τ谠谠闯绦蛑谐霈F(xiàn)的這些串將用合適的值進行替換。
__FILE__ 包含當前程序文件名的字符串
__LINE__ 表示當前行號的整數(shù)
__DATE__ 包含當前日期的字符串
__STDC__ 如果編譯器遵循ANSI C標準,它就是個非零值
__TIME__ 包含當前時間的字符串
注意:是雙下劃線,而不是單下劃線 。
#error指令將使編譯器顯示一條錯誤信息,然后停止編譯。
#line指令改變_LINE_與_FILE_的內(nèi)容,它們是在編譯程序中預(yù)先定義的標識符。
#pragma指令沒有正式的定義。編譯器可以自定義其用途。典型的用法是禁止或允許某些煩人的警告信息。
4、頭文件包含指令。
這是最常見的。采用頭文件的目的主要是為了使某些定義可以供多個不同的C源程序使用。因為在需要用到這些定義的C源程序中,只需加上一條#include語句即可,而不必再在此文件中將這些定義重復(fù)一遍。預(yù)編譯程序?qū)杨^文件中的定義統(tǒng)統(tǒng)都加入到它所產(chǎn)生的輸出文件中,以供編譯程序?qū)χM行處理。
#include預(yù)處理指令的作用是在指令處展開被包含的文件。包含可以是多重的,也就是說一個被包含的文件中還可以包含其他文件。標準C編譯器至少支持八重嵌套包含。預(yù)處理過程不檢查在轉(zhuǎn)換單元中是否已經(jīng)包含了某個文件并阻止對它的多次包含,這個的處理辦法使用上面給出的條件預(yù)處理指令。
include文件的展開是一個很簡單的過程,只是將include文件包含的代碼拷貝到包含當前cpp文件中。
(1)沒有被任何的其它cpp文件或者頭文件包含的.h文件將不會被編譯。也不會最終成為應(yīng)用程序的一部分。
編譯C++工程后你會發(fā)現(xiàn),并沒有報告上面的代碼錯誤。這說明.h文件本身不是一個編譯單元。只有通過include語句最終包括到了一個.cpp文件中后才會成為一個編譯單元。
(2)存在一種可能性,即一個cpp文件直接的或者間接的包括了多次同一個.h文件。
上面這樣的多重包含 就出現(xiàn)編譯錯誤
(3)include文件是按照定義順序被展開到cpp文件中的。
編譯和鏈接。
C++的編譯實際上分為編譯和鏈接兩個階段,這兩個階段聯(lián)系緊密。根據(jù)C++標準,一個編譯單元(Translation Unit)是指一個.cpp文件以及它所include的所有.h文件,.h文件里面的代碼將會被擴展到.cpp文件里,然后編譯器編譯該.cpp文件生成一個.obj文件。obj文件擁有PE[Portable Executable,即windows可執(zhí)行文件]文件格式,并且本身包含的就已經(jīng)是二進制碼,但是,不一定能夠執(zhí)行。當編譯器將一個工程里的所有.cpp文件都編譯完畢后,再由鏈接器進行鏈接,成為一個.exe或庫文件。
編譯上面的項目,VS會生成如下文件
生成的目標文件為可重定位文件(Relocatable File)
這里有個問題,雖然test.h對main.cpp是可見的(main.cpp包含了test.h),但是test.cpp對main.cpp并不可見,那么main.cpp是如何找到func函數(shù)的實現(xiàn)的呢?
實際上,在單獨編譯main.cpp文件的時候編譯器并不先去關(guān)注func函數(shù)是否已經(jīng)實現(xiàn),或者在哪里實現(xiàn)。它只是把它看作一個外部的鏈接類型,認為func函數(shù)的實現(xiàn)應(yīng)該在另外的一個obj文件中。在調(diào)用func的時候,編譯器僅僅使用了一個地址跳轉(zhuǎn),但是由于并不知道foo具體存在于哪個地方,因此只是在jump后面填入了一個假的地址。然后就繼續(xù)編譯下面的代碼。當所有的cpp文件都執(zhí)行完了之后就進入鏈接階段。
總結(jié)
以上是生活随笔為你收集整理的C/C++ 之 应用程序的编译过程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++之C++命名空间
- 下一篇: C/C++之类的前置声明