c++進階(一)C語言條件編譯及編譯預處理階段

firedragonpzy發表於2012-07-14
一、C語言由原始碼生成的各階段如下:
C源程式->編譯預處理->編譯->優化程式->彙編程式->連結程式->可執行檔案
其中 編譯預處理階段,讀取c源程式,對其中的偽指令(以#開頭的指令)和特殊符號進行處理。或者說是掃描原始碼,對其進行初步的轉換,產生新的原始碼提供給編譯器。預處理過程先於編譯器對原始碼進行處理。
在C 語言中,並沒有任何內在的機制來完成如下一些功能:在編譯時包含其他原始檔、定義巨集、根據條件決定編譯時是否包含某些程式碼。要完成這些工作,就需要使用預處理程式。儘管在目前絕大多數編譯器都包含了預處理程式,但通常認為它們是獨立於編譯器的。預處理過程讀入原始碼,檢查包含預處理指令的語句和巨集定義,並 對原始碼進行響應的轉換。預處理過程還會刪除程式中的註釋和多餘的空白字元。
二、偽指令(或預處理指令)定義
預處理指令是以#號開頭的程式碼行。#號必須是該行除了任何空白字元外的第一個字元。#後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字元。整行語句構成了一條預處理指令,該指令將在編譯器進行編譯之前對原始碼做某些轉換。下面是部分預處理指令:

指令 用途
# 空指令,無任何效果
#include 包含一個原始碼檔案
#define 定義巨集
#undef 取消已定義的巨集
#if 如果給定條件為真,則編譯下面程式碼
#ifdef 如果巨集已經定義,則編譯下面程式碼
#ifndef 如果巨集沒有定義,則編譯下面程式碼
#elif 如果前面的#if給定條件不為真,當前條件為真,則編譯下面程式碼,其實就是else if的簡寫
#endif 結束一個#if……#else條件編譯塊
#error 停止編譯並顯示錯誤資訊

三、預處理指令主要包括以下四個方面:
1、巨集定義指令
巨集定義了一個代表特定內容的識別符號。預處理過程會把原始碼中出現的巨集識別符號替換成巨集定義時的值。巨集最常見的用法是定義代表某個值的全域性符號。巨集的第二種用 法是定義帶引數的巨集(巨集函式),這樣的巨集可以象函式一樣被呼叫,但它是在呼叫語句處展開巨集,並用呼叫時的實際引數來代替定義中的形式引數。

1.1 #define指令
1.1.1 #define預處理指令用來定義巨集。該指令最簡單的格式是:宣告一個識別符號,給出這個識別符號代表的程式碼(比如像圓周率這樣的數)。在後面的原始碼中,我們就可以使用定義的巨集取代要使用的程式碼,舉例如下:
//例1
#define MAX_NUM 10
int array[MAX_NUM];
for(i=0;i<MAX_NUM;i++)
在這個例子中,對於閱讀該程式的人來說,符號MAX_NUM就有特定的含義,它代表的值給出了陣列所能容納的最大元素數目。程式中可以多次使用這個值。作為一種約定,習慣上總是全部用大寫字母來定義巨集,這樣易於把程式的巨集識別符號和一般變數識別符號區別開來。如果想要改變陣列的大小,只需要更改巨集定義並重新編譯程式即可。

1.1.2 使用巨集的好處有兩點:

一是使用方便。如下:

//例2
#define PAI 3.1415926
PAI顯然比3.1415926寫著方便。
二是定義的巨集有了意義,可讀性強。如例1,MAX_NUM,望文生意便知是最大數量的意思,比單純使用10這個數字可讀性要強的多。

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

1.1.3 巨集表示的值可以是一個常量表示式,允許巨集巢狀(必須在前面已定義)。例如:

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

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

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

1.1.4 巨集還可以代表一個字串常量,例如:
#define VERSION "Version 1.0 Copyright(c) 2003"

1.2 帶引數的#define指令(巨集函式)
帶引數的巨集和函式呼叫看起來有些相似。看一個例子:

//例4
#define Cube(x) (x)*(x)*(x)

可以時任何數字表示式甚至函式呼叫來代替引數x。這裡再次提醒大家注意括號的使用。巨集展開後完全包含在一對括號中,而且引數也包含在括號中,這樣就保證了巨集和引數的完整性。看一個用法:
//例4用法
int num=8+2;
volume=Cube(num);
展開後為(8+2)*(8+2)*(8+2);
如果沒有那些括號就變為8+2*8+2*8+2了。
下面的用法是不安全的:
volume=Cube(num++);
如果Cube是一個函式,上面的寫法是可以理解的。但是,因為Cube是一個巨集,所以會產生副作用。這裡的書寫不是簡單的表示式,它們將產生意想不到的結果。它們展開後是這樣的:
volume=(num++)*(num++)*(num++);
很顯然,結果是10*11*12,而不是10*10*10;
那麼怎樣安全的使用Cube巨集呢?必須把可能產生副作用的操作移到巨集呼叫的外面進行:
int num=8+2;
volume=Cube(num);
num++;

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


1.3 #運算子
出現在巨集定義中的#運算子把跟在其後的引數轉換成一個字串。有時把這種用法的#稱為字串化運算子。例如:


//例5
#define PASTE(n) "adhfkj"#n
int main()
{
printf("%s\n",PASTE(15));
return 0;
}
//輸出adhfj15

巨集定義中的#運算子告訴預處理程式,把原始碼中任何傳遞給該巨集的引數轉換成一個字串。所以輸出應該是adhfkj15。


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


//例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;
}
//最後程式的輸出為:
123
aabbcc


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


//例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;
}

這樣我們就可以實現debug功能,每次要輸出除錯資訊前,只需要#ifdef DEBUG判斷一次。不需要了就在檔案開始定義#define DEBUG 0

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


//例8
#define TWO
int main()
{
#ifdef ONE
printf("1\n");
#elif defined TWO
printf("2\n");
#else
printf("3\n");
#endif
}
//輸出結果是2。

2.2 #ifdef和#ifndef

這二者主要用於防止重複包含。我們一般在.h標頭檔案前面加上這麼一段:

//標頭檔案防止重複包含
//funcA.h
#ifndef FUNCA_H
#define FUNCA_H
//標頭檔案內容
#end if
這樣,如果a.h包含了funcA.h,b.h包含了a.h、funcA.h,重複包含,會出現一些type redefination之類的錯誤。
#if defined等價於#ifdef; #if !defined等價於#ifndef

3、標頭檔案包含指令。
採用標頭檔案的目的主要是為了使某些定義可以供多個不同的C源程式使用。因為在需要用到這些定義的C源程式中,只需加上一條#include語句即可,而不必再在此檔案中將這些定義重複一遍。預編譯程式將把標頭檔案中的定義統統都加入到它所產生的輸出檔案中,以供編譯程式對之進行處理。
#include預處理指令的作用是在指令處展開被包含的檔案。包含可以是多重的,也就是說一個被包含的檔案中還可以包含其他檔案。標準C編譯器至少支援八重巢狀包含。預處理過程不檢查在轉換單元中是否已經包含了某個檔案並阻止對它的多次包含,這個的處理辦法上面已經給出。
在程式中包含標頭檔案有兩種格式:
#include <my.h>
#include "my.h"
第一種方法是用尖括號把標頭檔案括起來。這種格式告訴預處理程式在編譯器自帶的或外部庫的標頭檔案中搜尋被包含的標頭檔案。第二種方法是用雙引號把標頭檔案括起 來。這種格式告訴預處理程式在當前被編譯的應用程式的原始碼檔案中搜尋被包含的標頭檔案,如果找不到,再搜尋編譯器自帶的標頭檔案。
採用兩種不同包含格式的理由在於,編譯器是安裝在公共子目錄下的,而被編譯的應用程式是在它們自己的私有子目錄下的。一個應用程式既包含編譯器提供的公共 標頭檔案,也包含自定義的私有標頭檔案。採用兩種不同的包含格式使得編譯器能夠在很多標頭檔案中區別出一組公共的標頭檔案。
4、特殊符號。
預編譯程式可以識別一些特殊的符號。預編譯程式對於在源程式中出現的這些串將用合適的值進行替換。
4.1 __LINE__
注意,是雙下劃線,而不是單下劃線 。
__FILE__ 包含當前程式檔名的字串
__LINE__ 表示當前行號的整數
__DATE__ 包含當前日期的字串
__STDC__ 如果編譯器遵循ANSI C標準,它就是個非零值
__TIME__ 包含當前時間的字串

//例9
#include<stdio.h>
int main()
{
printf("Hello World!\n");
printf("%s\n",__FILE__);
printf("%d\n",__LINE__);
return 0;
}


4.2 #line等
#error指令將使編譯器顯示一條錯誤資訊,然後停止編譯。
#line指令改變_LINE_與_FILE_的內容,它們是在編譯程式中預先定義的識別符號。
#pragma指令沒有正式的定義。編譯器可以自定義其用途。典型的用法是禁止或允許某些煩人的警告資訊。

//例10,#line舉例
#line 100 //初始化行計數器
#include<stdio.h> //行號100
int main()
{
printf("Hello World!\n");
printf("%d",__LINE__);
return 0;
}
//輸出104


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


摘自:[url]http://www.cnblogs.com/rusty/archive/2011/03/27/1996806.html#commentform[/url]

相關文章