c程序的編譯&條件編譯 (預處理階段)

一、c程序的編譯過程


C源程序頭文件-->預編譯處理(cpp)-->編譯程序本身-->優化程序-->彙編程序-->鏈接程序-->可執行文件


編譯,編譯程序讀取源程序(字符流),對之進行詞法和語法的分析,將高級語言指令轉換爲功能等效的彙編代碼,再由彙編程序轉換爲機器語言,並且按照操作系統對可執行文件格式的要求鏈接生成可執行程序。如果想看某個具體的編譯過程,則可以分別使用-E,-S,-c和 -O。

1、執行命令

1.預處理
在該階段,編譯器將C源代碼中的包含的頭文件如stdio.h編譯進來,可通過gcc 的選項 -E 進行查看

如:[root@localhost 0001]#gcc -E main.c -o main.i
將main.c預處理輸出 main.i 文件


2.編譯
這個階段中,編譯器首先要檢查代碼的規範性、是否有語法錯誤等,以確定代碼的實際要做的工作。用戶可以使用”-S”選項來進行查看,該選項只進行編譯而不進行彙編,生成彙編代碼。
 選項 -S

如:[root@localhost 0001]# gcc –S main.i –o main.s 
將預處理輸出文件main.i彙編成main.s文件。

*3.優化程序


4.彙編
彙編階段是把編譯階段生成的”.s”文件轉成二進制目標代碼.
選項 -c

如:[root@localhost 0001]#gcc –c main.s –o main.o
將彙編輸出文件main.s編譯輸出main.o文件
 

5.鏈接
  鏈接就是將不同部分的代碼和數據收集和組合成爲一個單一文件的過程,這個文件可被加載或拷貝到存儲器執行.
  在成功編譯之後,就進入了鏈接階段。
  無選項鍊接
  如:[root@localhost 0001]# gcc main.o –o main.exe
  將編譯輸出文件main.o鏈接成最終可執行文件main.exe

 鏈接可以執行與編譯時(源代碼被翻譯成機器代碼時),也可以執行與加載時(在程序被加載器加載到存儲器並執行時),甚至執行與運行時,由應用程序來執行.在現代系統中,鏈接是由鏈接器自動執行的

.
鏈接器分爲:靜態鏈接器和動態鏈接器兩種.
靜態鏈接器
  靜態鏈接器以一組可重定位目標文件和命令行參數作爲輸入,生成一個完全鏈接的可以加載和運行的可執行目標文件作爲輸出.

  靜態鏈接器主要完成兩個任務:
  1>符號解析:目標文件定義和引用符號.符號解析的目的在於將每個符號引用和一個符號定義聯繫起來.
  2>重定位:編譯器和彙編器生成從地址零開始的代碼和數據節.鏈接器通過把每個符號定義和一個存儲器位置聯繫起來,然後修改所有對這些符號的引用,使得他們執行這個存儲位置,從而重定位這些節.

動態鏈接器
 動態鏈接方式下,函數的定義在動態鏈接庫或共享對象的目標文件中。在編譯的鏈接階段,動態鏈接庫只提供符號表和其他少量信 息用於保證所有符號引用都有定義,保證編譯順利通過。動態鏈接器(ld-linux.so)鏈接程序在運行過程中根據記錄的共享對象的符號定義來動態加載共享庫,然後完成重定位。在此可執行文件被執行時,動態鏈接庫的全部內容將被映射到運行時相應進程的虛地址空間。動態鏈接程序將根據可執行程序中記錄的信息找到相應的函數代碼。  

2、執行作用

1.編譯預處理,預處理過程讀入源代碼,語法檢查宏替換取消註釋取消多餘空白字符等,對其中的僞指令(以#開頭的指令)和特殊符號進行處理僞指令主要包括以下四個方面:

(1)宏定義指令,即宏替換
(2)條件編譯指令,僞指令的引入使得程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。預編譯程序將根據有關的文件,將那些不必要的代碼過濾掉
(3) 頭文件包含指令,預編譯程序將把頭文件中的定義統統都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。 包含到c源 程序中的頭文件可以是系統提供的,這些頭文件一般被放在/usr/include目錄下。
(4)特殊符號,預編譯程序可以識別一些特殊的符號。例如在源程序中出現的LINE標識將被解釋爲當前行號(十進制數),FILE則被解釋爲當前被編譯的C源程序的名稱。預編譯程序對於在源程序中出現的這些串將用合適的值進行替換。

 預編譯程序所完成的基本上是對源程序的“替代”工作。經過此種替代,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊符號的輸出文件


2.編譯階段 經過預編譯得到的輸出文件中,將只有常量。如數字、字符串、變量的定義,以及C語言的關鍵字,如main,if,else,for,while,{,}, +,-,*,\,等等。預編譯程序所要作得工作就是通過詞法分析和語法分析,在確認所有的指令都符合語法規則之後,將其翻譯成等價的中間代碼表示或彙編代 碼。


3.優化處理,它不僅同編譯技術本身有關,且同機器的硬件環境也有關係。優化一部分是對中間代碼的優化。提高代碼的執行效率,調整代碼使代碼簡短易讀, 經過優化得到的彙編代碼必須經過彙編程序的彙編轉換成相應的機器指令,方可能被機器執行。


4.彙編過程,彙編過程實際上指把彙編語言代碼翻譯成目標機器指令的過程。對於被翻譯系統處理的每一個C語言源程序,都將最終經過這一處理而得到相應的目標文件。目標文件中所存放的也就是與源程序等效的目標的機器語言代碼。 


目標文件由段組成。通常一個目標文件中至少有兩個段:

代碼段  該段中所包含的主要是程序的指令。該段一般是可讀和可執行的,但一般卻不可寫。

數據段  主要存放程序中要用到的各種全局變量或靜態的數據。一般數據段都是可讀,可寫,可執行的。


UNIX環境下主要有三種類型的目標文件:
(1)可重定位文件   包含二進制代碼和數據,其形式可以再編譯時與其他可定位目標文件合併起來,創建一個可執行目標文件.
(2)共享的目標文件  這種文件存放了適合於在兩種上下文裏鏈接的代碼和數據,,其形式可以被直接拷貝到存儲器並執行

(3)可執行文件    一種特殊的可重定位目標文件,可以再加載或運行時,被動態地夾在到存儲器並執行. 

編譯器和彙編器生成可重定位目標文件(包括共享目標文件),鏈接器生成可執行目標文件.


5.鏈接程序 由彙編程序生成的目標文件並不能立即就被執行,其中可能還有許多沒有解決的問題。例如,某個源文件中的函數可能引用了另一個 源文件中定義的某個符號(如變量或者函數調用等);在程序中可能調用了某個庫文件中的函數,等等。所有的這些問題,都需要經鏈接程序的處理方能得以解決。 鏈接程序的主要工作就是將有關的目標文件彼此相連接,也即將在一個文件中引用的符號同該符號在另外一個文件中的定義連接起來,使得所有的這些目標文件成爲一個能夠誒操作系統裝入執行的統一整體。

二、C語言條件編譯&編譯預處理階段

1、C源程序->編譯預處理->編譯->優化程序->彙編程序->鏈接程序->可執行文件
       其中 編譯預處理階段,讀取c源程序,對其中的僞指令(以#開頭的指令)和特殊符號進行處理。或者說是掃描源代碼,對其進行初步的轉換,產生新的源代碼提供給編譯器。預處理過程先於編譯器對源代碼進行處理。
       在C 語言中,並沒有任何內在的機制來完成如下一些功能:在編譯時包含其他源文件、定義宏、根據條件決定編譯時是否包含某些代碼。要完成這些工作,就需要使用預處理程序。儘管在目前絕大多數編譯器都包含了預處理程序,但通常認爲它們是獨立於編譯器的。預處理過程讀入源代碼,檢查包含預處理指令的語句和宏定義,並 對源代碼進行響應的轉換。預處理過程還會刪除程序中的註釋和多餘的空白字符。
2、僞指令(或預處理指令)定義
      預處理指令是以#號開頭的代碼行。#號必須是該行除了任何空白字符外的第一個字符。#後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字符。整行語句構成了一條預處理指令,該指令將在編譯器進行編譯之前對源代碼做某些轉換。下面是部分預處理指令:
<span style="font-family:Microsoft YaHei;font-size:14px;">    指令         用途
    #           空指令,無任何效果
    #include    包含一個源代碼文件
    #define     定義宏
    #undef      取消已定義的宏
    #if         如果給定條件爲真,則編譯下面代碼
    #ifdef      如果宏已經定義,則編譯下面代碼
    #ifndef     如果宏沒有定義,則編譯下面代碼
    #elif       如果前面的#if給定條件不爲真,當前條件爲真,則編譯下面代碼,其實就是else if的簡寫
    #endif      結束一個#if……#else條件編譯塊
    #error      停止編譯並顯示錯誤信息</span>

3、預處理指令主要包括以下四個方面:
1、宏定義指令
      宏定義了一個代表特定內容的標識符。預處理過程會把源代碼中出現的宏標識符替換成宏定義時的值。宏最常見的用法是定義代表某個值的全局符號。宏的第二種用 法是定義帶參數的宏(宏函數),這樣的宏可以象函數一樣被調用,但它是在調用語句處展開宏,並用調用時的實際參數來代替定義中的形式參數。
 
1.1 #define指令
1.1.1 #define預處理指令用來定義宏。該指令最簡單的格式是:聲明一個標識符,給出這個標識符代表的代碼(比如像圓周率這樣的數)。在後面的源代碼中,我們就可以使用定義的宏取代要使用的代碼,舉例如下:
<span style="font-family:Microsoft YaHei;font-size:14px;">//例1
#define MAX_NUM 10
int array[MAX_NUM];
for(i=0;i<MAX_NUM;i++) </span>
    在這個例子中,對於閱讀該程序的人來說,符號MAX_NUM就有特定的含義,它代表的值給出了數組所能容納的最大元素數目。程序中可以多次使用這個值。作爲一種約定,習慣上總是全部用大寫字母來定義宏,這樣易於把程序的宏標識符和一般變量標識符區別開來。如果想要改變數組的大小,只需要更改宏定義並重新編譯程序即可。


1.1.2 使用宏的好處有兩點:
一是使用方便。如下:
<span style="font-family:Microsoft YaHei;font-size:14px;">//例2
#define PAI 3.1415926</span>
PAI顯然比3.1415926寫着方便。


二是定義的宏有了意義,可讀性強。如例1,MAX_NUM,望文生意便知是最大數量的意思,比單純使用10這個數字可讀性要強的多。


三是容易修改。如例1,如果在程序中有幾十次會使用到MAX_NUM,修改只需要在宏定義裏面修改一次就可以,否則你會修改到崩潰。


1.1.3 宏表示的值可以是一個常量表達式,允許宏嵌套(必須在前面已定義)。例如:
<span style="font-family:Microsoft YaHei;font-size:14px;">//例3
#define ONE 1
#define TWO 2
#define SUM(ONE+TWO)</span>

這裏需要注意兩點:
一是注意上面的宏定義使用了括號。儘管它們並不是必須的。但出於謹慎考慮,還是應該加上括號的。例如:
            six=SUM*TWO;
    預處理過程把上面的一行代碼轉換成:
            six=(ONE+TWO)*TWO;
    如果沒有那個括號,就轉換成six=ONE+TWO*TWO;了。

也就是說預處理僅是簡單的字符替換,要時刻注意這一點,很多錯誤都會因此出現。


二是雖然我們舉例用了#define ONE 1 這個例子,但是一般要求宏定義要有其實際意義,#define ONE 1這種沒意義的宏定義是不推薦的。(大概是這麼個意思,忘記具體怎麼說了)


1.1.4 宏還可以代表一個字符串常量,例如:
    
<span style="font-family:Microsoft YaHei;font-size:14px;">#define VERSION "Version 1.0 Copyright(c) 2003"</span>

1.2 帶參數的#define指令(宏函數)
    帶參數的宏和函數調用看起來有些相似。看一個例子:
<span style="font-family:Microsoft YaHei;font-size:14px;">//例4
#define Cube(x) (x)*(x)*(x)</span>
    可以時任何數字表達式甚至函數調用來代替參數x。這裏再次提醒大家注意括號的使用。宏展開後完全包含在一對括號中,而且參數也包含在括號中,這樣就保證了宏和參數的完整性。看一個用法:
<span style="font-family:Microsoft YaHei;font-size:14px;">//例4用法
int num=8+2;
volume=Cube(num);</span>
    展開後爲(8+2)*(8+2)*(8+2);
    如果沒有那些括號就變爲8+2*8+2*8+2了。
    下面的用法是不安全的:
            
<span style="font-family:Microsoft YaHei;font-size:14px;">volume=Cube(num++);</span>


    如果Cube是一個函數,上面的寫法是可以理解的。但是,因爲Cube是一個宏,所以會產生副作用。這裏的書寫不是簡單的表達式,它們將產生意想不到的結果。它們展開後是這樣的:
            volume=(num++)*(num++)*(num++);
    很顯然,結果是10*11*12,而不是10*10*10;
    那麼怎樣安全的使用Cube宏呢?必須把可能產生副作用的操作移到宏調用的外面進行:
            int num=8+2;
            volume=Cube(num);
            num++;


宏函數使用不當會出現一些難以發現的錯誤,請慎重使用。




1.3 #運算符
    出現在宏定義中的#運算符把跟在其後的參數轉換成一個字符串。有時把這種用法的#稱爲字符串化運算符。例如:

<span style="font-family:Microsoft YaHei;font-size:14px;">//例5
#define PASTE(n) "adhfkj"#n
int main()
{
       printf("%s\n",PASTE(15));
       return 0;
}</span>


//輸出adhfj15

宏定義中的#運算符告訴預處理程序,把源代碼中任何傳遞給該宏的參數轉換成一個字符串。所以輸出應該是adhfkj15。




1.4 ##運算符(很少用)
    ##運算符用於把參數連接到一起。預處理程序把出現在##兩側的參數合併成一個符號。看下面的例子:

<span style="font-family:Microsoft YaHei;font-size:14px;">//例6
#define NUM(a,b,c) a##b##c
#define STR(a,b,c) a##b##c
int main()
 {
       printf("%d\n",NUM(1,2,3));
       printf("%s\n",STR("aa","bb","cc"));
       return 0;
 }</span>


//最後程序的輸出爲:
123
aabbcc
複製代碼
 
2、條件編譯指令。
      程序員可以通過定義不同的宏來決定編譯程序對哪些代碼進行處理。條件編譯指令將決定那些代碼被編譯,而哪些是不被編譯的。可以根據表達式的值或者某個特定的宏是否被定義來確定編譯條件。
2.1 #if/#endif/#else/#elif指令
    #if指令檢測跟在製造另關鍵字後的常量表達式。如果表達式爲真,則編譯後面的代碼,知道出現#else、#elif或#endif爲止;否則就不編譯。
    #endif用於終止#if預處理指令。
    #else指令用於某個#if指令之後,當前面的#if指令的條件不爲真時,就編譯#else後面的代碼。


<span style="font-family:Microsoft YaHei;font-size:14px;">//例7
#define DEBUG       //此時#ifdef DEBUG爲真
//#define DEBUG 0  //此時爲假
int main()
{
   #ifdef DEBUG
      printf("Debugging\n");
   #else
      printf("Not debugging\n");
   #endif
   printf("Running\n");
   return 0;
}</span>


這樣我們就可以實現debug功能,每次要輸出調試信息前,只需要#ifdef DEBUG判斷一次。不需要了就在文件開始定義#define DEBUG 0


#elif預處理指令綜合了#else和#if指令的作用。

<span style="font-family:Microsoft YaHei;font-size:14px;">//例8
#define TWO
int main()
{
   #ifdef ONE
          printf("1\n");
   #elif defined TWO
          printf("2\n");
   #else
          printf("3\n");
   #endif
}</span>


//輸出結果是2。 

2.2 #ifdef和#ifndef
這二者主要用於防止重複包含。我們一般在.h頭文件前面加上這麼一段:
<span style="font-family:Microsoft YaHei;font-size:14px;">
//頭文件防止重複包含
//funcA.h
#ifndef FUNCA_H
#define FUNCA_H
//頭文件內容
#end if</span>


這樣,如果a.h包含了funcA.h,b.h包含了a.h、funcA.h,重複包含,會出現一些type redefination之類的錯誤。
#if defined等價於#ifdef; #if !defined等價於#ifndef


3、頭文件包含指令。
採用頭文件的目的主要是爲了使某些定義可以供多個不同的C源程序使用。因爲在需要用到這些定義的C源程序中,只需加上一條#include語句即可,而不必再在此文件中將這些定義重複一遍。預編譯程序將把頭文件中的定義統統都加入到它所產生的輸出文件中,以供編譯程序對之進行處理。
  #include預處理指令的作用是在指令處展開被包含的文件。包含可以是多重的,也就是說一個被包含的文件中還可以包含其他文件。標準C編譯器至少支持八重嵌套包含。預處理過程不檢查在轉換單元中是否已經包含了某個文件並阻止對它的多次包含,這個的處理辦法上面已經給出。
  在程序中包含頭文件有兩種格式:
      
<span style="font-family:Microsoft YaHei;font-size:14px;">  #include <my.h>
  #include "my.h"</span>


    第一種方法是用尖括號把頭文件括起來。這種格式告訴預處理程序在編譯器自帶的或外部庫的頭文件中搜索被包含的頭文件。第二種方法是用雙引號把頭文件括起 來。這種格式告訴預處理程序在當前被編譯的應用程序的源代碼文件中搜索被包含的頭文件,如果找不到,再搜索編譯器自帶的頭文件。
    採用兩種不同包含格式的理由在於,編譯器是安裝在公共子目錄下的,而被編譯的應用程序是在它們自己的私有子目錄下的。一個應用程序既包含編譯器提供的公共 頭文件,也包含自定義的私有頭文件。採用兩種不同的包含格式使得編譯器能夠在很多頭文件中區別出一組公共的頭文件。
4、特殊符號。
預編譯程序可以識別一些特殊的符號。預編譯程序對於在源程序中出現的這些串將用合適的值進行替換。
4.1 __LINE__
注意,是雙下劃線,而不是單下劃線 。
__FILE__ 包含當前程序文件名的字符串
__LINE__  表示當前行號的整數
__DATE__ 包含當前日期的字符串
__STDC__  如果編譯器遵循ANSI C標準,它就是個非零值
__TIME__ 包含當前時間的字符串
<span style="font-family:Microsoft YaHei;font-size:14px;">//例9
#include<stdio.h>
int main()
{
   printf("Hello World!\n");
   printf("%s\n",__FILE__);
   printf("%d\n",__LINE__);
   return 0;
}
</span>

4.2 #line等
#error指令將使編譯器顯示一條錯誤信息,然後停止編譯。
#line指令改變_LINE_與_FILE_的內容,它們是在編譯程序中預先定義的標識符。
#pragma指令沒有正式的定義。編譯器可以自定義其用途。典型的用法是禁止或允許某些煩人的警告信息。
<span style="font-family:Microsoft YaHei;font-size:14px;">//例10,#line舉例
#line   100          //初始化行計數器 
#include<stdio.h>    //行號100
int main()
{
    printf("Hello World!\n");
    printf("%d",__LINE__);
    return 0;
}</span>

//輸出104

4、預編譯程序所完成的基本上是對源程序的“替代”工作。經過此種替代,生成一個沒有宏定義、沒有條件編譯指令、沒有特殊符號的輸出文件。這個文件的含義同沒有經過預處理的源文件是相同的,但內容有所不同。下一步,此輸出文件將作爲編譯程序的輸出而被翻譯成爲機器指令。

小結:

宏定義的使用需要括號的使用,不然很容易出錯!

宏定義的使用利弊權衡要清楚。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章