C語言的本質(20)——預處理之二:條件預處理和包含標頭檔案

尹成發表於2014-07-17

 

我們可以通過定義不同的巨集來決定編譯程式對哪些程式碼進行處理。條件編譯指令將決定那些程式碼被編譯,而哪些是不被編譯的。可以根據表示式的值或者某個特定的巨集是否被定義來確定編譯條件。

條件編譯可分為三種情況,按照不同的條件去編譯不同的程式部分,因而產生不同的目標檔案,這對於程式的移植和除錯都非常有用。

 

1、

 

#ifdef  識別符號
     程式段1
#else
     程式段2
#endif

功能:如果識別符號已經被#define定義過,則對程式段1進行編譯,否則對程式段2進行編譯。如果沒有程式斷2,#else可以沒喲。

 

2、

 

#ifndef 識別符號
         程式段1
#else
         程式段2
#endif

它的功能是跟上面恰恰相反的,也就是說如果識別符號沒有被#define定義過,則對程式段1進行編譯,否則對程式段2進行編譯。

 

3、

 

#if  常量表示式
        程式段1;
#else
        程式段2;
#endif

功能:根據常量表示式的真假去判斷執行哪個程式段,如果為真則執行程式段1,否則執行程式段2。

 

條件預處理可以用來除錯程式,比如:

 

#define DEBUG     //此時#ifdefDEBUG為真
//#define DEBUG 0    //此時為假
int main(void)
{
#ifdef DEBUG
         printf("Debugging\n");
#else
         printf("Notdebugging\n");
#endif
         printf("Running\n");
         return0;
}

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

 

 

條件預處理指示常用於原始碼的配置管理,例如:

 

#if MACHINE == arm
   int x;
#elif MACHINE == x64
   long x;
#else   /* all others */
   #error UNKNOWN TARGET MACHINE
#endif

假設這段程式是為多種平臺編寫的,在arm平臺上需要定義x為int型,在x64平臺上需要定義x為long型,對其它平臺暫不提供支援,就可以用條件預處理指示來寫。如果在預處理這段程式碼之前,MACHINE被定義為arm,則包含intx;這段程式碼;否則如果MACHINE被定義為x64,則包含long x;這段程式碼;否則(MACHINE沒有定義,或者定義為其它值),包含#error UNKNOWN TARGET MACHINE這段程式碼,編譯器遇到這個預處理指示就報錯退出,錯誤資訊就是UNKNOWN TARGET MACHINE。

 如果要為x64平臺編譯這段程式碼,有幾種可選的辦法:

 1、手動編輯程式碼,在前面添一行#defineMACHINE x64。這樣做的缺點是難以管理,如果這個專案中有很多原始檔都需要定義MACHINE,每次要為x64平臺編譯就得把這些定義全部改成x64,每次要為arm平臺編譯就得把這些定義全部改成arm。

 2、在所有需要配置的原始檔開頭包含一個標頭檔案,在標頭檔案中定義#define MACHINE x64,這樣只需要改一個標頭檔案就可以影響所有包含它的原始檔。通常這個標頭檔案由配置工具生成,比如在Linux核心原始碼的目錄下執行make menuconfig命令可以出來一個配置選單,在其中配置的選項會自動轉換成標頭檔案include/linux/autoconf.h中的巨集定義。

 舉一個具體的例子,在核心配置選單中用Enter鍵和方向鍵進入Device Drivers ---> Network device support,然後用空格鍵選中Networkdevice support(選單項左邊的[ ]括號內會出現一個*號),然後儲存退出,會生成一個名為.config的隱藏檔案,其內容類似於:

 ......
#
# Network device support
#
CONFIG_NETDEVICES=y
# CONFIG_DUMMY is not set
# CONFIG_BONDING is not set
# CONFIG_EQUALIZER is not set
# CONFIG_TUN is not set
......


然後執行make命令編譯核心,這時根據.config檔案生成標頭檔案include/linux/autoconf.h,其內容類似於:

 

......
/*
 *Network device support
 */
#define CONFIG_NETDEVICES 1
#undef CONFIG_DUMMY
#undef CONFIG_BONDING
#undef CONFIG_EQUALIZER
#undef CONFIG_TUN
......

上面的程式碼用#undef確保取消一些巨集的定義,如果先前沒有定義過CONFIG_DUMMY,用#undef CONFIG_DUMMY取消它的定義沒有任何作用,也不算錯。

 

include/linux/autoconf.h被另一個標頭檔案include/linux/config.h所包含,通常核心程式碼包含後一個標頭檔案,例如net/core/sock.c:

 

......
#include <linux/config.h>
......
int sock_setsockopt(struct socket *sock,int level, int optname,
                    char __user *optval, intoptlen)
{
......
#ifdef CONFIG_NETDEVICES
                case SO_BINDTODEVICE:
                {
                            ......
                }
#endif
......

再比如drivers/isdn/i4l/isdn_common.c:

 

......
#include <linux/config.h>
......
static int
isdn_ioctl(struct inode *inode, struct file*file, uint cmd, ulong arg)
{
......
#ifdef CONFIG_NETDEVICES
case IIOCNETGPN:
/* Get peerphone number of a connected
* isdn networkinterface */
if (arg) {
if (copy_from_user(&phone, argp, sizeof(phone)))
return -EFAULT;
return isdn_net_getpeer(&phone, argp);
} else
return -EINVAL;
#endif
......
#ifdef CONFIG_NETDEVICES
case IIOCNETAIF:
......
#endif /* CONFIG_NETDEVICES */
......
 

這樣,在配置選單中所做的配置通過條件預處理最終決定了哪些程式碼被編譯到核心中。#ifdef或#if可以巢狀使用,但預處理指示通常都頂頭寫不縮排,為了區分巢狀的層次,可以像上面的程式碼中最後一行那樣,在#endif處用註釋寫清楚它結束的是哪個#if或#ifdef。

3、要定義一個巨集不一定在程式碼中用#define定義,比如我們可以用gcc的-D選項定義一個巨集NDEBUG。對於上面的例子,我們需要給MACHINE定義一個值,可以寫成類似這樣的命令:gcc -c -DMACHINE=x64 main.c。這種辦法需要給每個編譯命令都加上適當的選項,和第2種方法相比似乎也很麻煩,第2種方法在標頭檔案中只寫一次巨集定義就可以在很多原始檔中生效,第3種方法能不能做到“只寫一次到處生效”呢?等以後學習了Makefile就有辦法了。

最後通過下面的例子說一下#if後面的表示式:

#define VERSION  2
#if defined x || y || VERSION < 3

首先處理defined運算子,defined運算子一般用作表示式中的一部分,如果單獨使用,#if defined x相當於#ifdef x,而#if !defined x相當於#ifndef x。在這個例子中,如果x這個巨集有定義,則把defined x替換為1,否則替換為0,因此變成#if 0 || y || VERSION < 3。

然後把有定義的巨集展開,變成#if 0 || y || 2 < 3。

把沒有定義的巨集替換成0,變成#if 0 || 0 || 2 < 3,注意,即使前面定義了一個變數名是y,在這一步也還是替換成0,因為#if的表示式必須在編譯時求值,其中包含的名字只能是巨集定義。

把得到的表示式0 || 0 || 2 < 3像C表示式一樣求值,求值的結果是#if 1,因此條件成立。

 

檔案包含

檔案包含是C預處理程式的另一個重要功能。檔案包含命令列的一般形式為:

   #include "檔名"

我們曾經多次用此命令包含過庫函式的標頭檔案。例如:

#include "stdio.h"
#include "math.h"


檔案包含命令的功能是把指定的檔案插入該命令列位置取代該命令列,從而把指定的檔案和當前的源程式檔案連成一個原始檔

 在程式設計中,檔案包含是很有用的。一個大的程式可以分為多個模組,由多個程式設計師分別程式設計。有些公用的符號常量或巨集定義等可單獨組成一個檔案,在其它檔案的開頭用包含命令包含該檔案即可使用。這樣,可避免在每個檔案開頭都去書寫那些公用量,從而節省時間,並減少出錯。

 對檔案包含命令還要說明以下幾點:

包含命令中的檔名可以用雙引號括起來,也可以用尖括號括起來。例如以下寫法都是允許的:

#include "stdio.h"
#include <math.h>

但是這兩種形式是有區別的:

使用尖括號表示在包含檔案目錄中去查詢(包含目錄是由使用者在設定環境時設定的),而不在原始檔目錄去查詢;

使用雙引號則表示首先在當前的原始檔目錄中查詢,若未找到才到包含目錄中去查詢。使用者程式設計時可根據自己檔案所在的目錄來選擇某一種命令形式。

一個include命令只能指定一個被包含檔案,若有多個檔案要包含,則需用多個include命令。

檔案包含允許巢狀,即在一個被包含的檔案中又可以包含另一個檔案。

 

 

相關文章