第16章 C前處理器和C庫 16.6 其他指令

NO IMAGE

程式設計師可能需要為不同的工作環境準備不同的C程式和C庫包。程式碼型別的選擇會根據環境的不同而各異。前處理器提供一些指令來幫助程式設計師編寫出這樣的程式碼:改變一些#define巨集的值後,這些程式碼就可以被從一個系統移植到另一個系統。#undef指令取消前面的#define定義。#if、#ifdef、#ifndef、#else、#elif和#endif指令可用於選擇什麼情況下編譯哪些程式碼。#line指令用於重置行和檔案資訊,#error指令用於給出錯誤訊息,#pragma指令用於向編譯器發出指示。

16.6.1  #undef指令

#undef指令取消定義一個給定的#define。也就是說,假設有如下定義:

#define LIMIT 400

則指令:

#undef LIMIT

會取消該定義。現在就可以重新定義LIMIT,以使它有一個新的值。即使開始沒有定義LIMIT,取消LIMIT的定義也是合法的。如果想使用一個特定的名字,但又不能確定前面是否已經使用了該名字,為安全起見,就可以取消該名字的定義。

16.6.2  已定義:C前處理器的觀點

關於識別符號的構成,前處理器和C遵循相同規則:識別符號只能包含大寫字母 、小寫字母 、數字和下劃線。

這裡的已定義表示由前處理器定義。

如果識別符號是該檔案前面的#define指令建立的巨集名,並且沒有#undef指令關閉該識別符號,則識別符號是已定義的如果識別符號不是巨集,而是(例如)一個具有檔案作用域的C變數,那麼前處理器把識別符號當作未定義的。

已定義的巨集可以為類物件巨集(包括空巨集)或類函式巨集:

#define LIMIT 1000    //LIMIT是已定義的
#define GOOD          //GOOD是已定義的
#define A(X) ((-(X))*(X))  //A是已定義的
int q;                //q不是一個巨集,因此是未定義的
#undef GOOD           //GOOD是未定義的

注意,#define巨集的作用域從檔案中的定義點開始,直到用#undef取消巨集為止,或直到檔案尾為止(由二者中先滿足條件的那個結束巨集的作用域)。還應注意,如果用標頭檔案引入巨集,那麼#define在檔案中的位置依賴於#include指令的位置。

16.6.3  條件編譯

可使用已經提到的指令設定條件編譯。也就是說,可以使用這些指令告訴編譯器:根據編譯時的條件接受或忽略資訊(程式碼)塊。

一、#ifdef、#else和#endif指令

一個簡短的示例可以闡明條件編譯。考慮下面的內容:

#ifdef MAVIS
#include "horse.h"    //如果已經使用#define 定義了MAVIS,則執行這裡的指令
#define STABLES 5
#else 
#include "cow.h"      //如果沒有用#define定義MAVIS,則執行這裡的指令
#define STABLES 15
#endif

這裡採用了較新的實現和ANSI 標準支援的縮排格式。如果使用舊編譯器,必須要使所有指令,或者到少使用#指令符號左對齊:

#ifdef MAVIS
#    include "horse.h"    //如果已經使用#define 定義了MAVIS,則執行這裡的指令
#    define STABLES 5
#else 
#    include "cow.h"      //如果沒有用#define定義MAVIS,則執行這裡的指令
#    define STABLES 15
#endif

#ifdef #else格式非常類似於C中的if else。主要差異為前處理器不能識別標記程式碼塊的花括號({ })因此使用#else(如果需要)和#endif(必須存在)來標記指令塊。這些條件結構可以巢狀。

也可以使用這些指令標識C語句塊,如程式清單16.9所示。

/*ifdef.c  --使用條件編譯*/
#include <stdio.h>
#define JUST_CHECKING
#define LIMIT 4
int main(void)
{
int i;
int total = 0;
for (i=1;i<=LIMIT;I  )
{
total  = 2*i*i   1;
#ifdef JUST_CHECKING
printf("i=%d,running total = %d\n",i,total);
#endif
}
printf("Grand total = %d\n",total);
return 0;
}

編譯並執行該程式,產生下列輸出:

i=1,running total = 3
i=2,running total = 12
i=3,running total = 31
i=4,running total = 64
Grand total = 64

如果省略JUST_CHECKING的定義(或置於註釋中,或用#undef取消它的定義)並重新編譯程式,那麼將會只顯示最後一行。可以使用這種方法輔助除錯程式 。定義JUST_CHECKING併合理使用#ifdef,使編譯器包含那些用於列印輔助除錯的中間值的程式程式碼。程式正常工作後,可以刪除定義並重新編譯。如果以後還需要使用這些資訊,可以重新插入定義,從而避免再次輸入額外的列印語句。

另外一種應用是:使用#indef選擇使用不同C實現的大的程式碼塊。

二、 #ifndef指令

類似於#indef指令,#ifndef指令可以與#else 、#endif指令一起使用。#ifndef判斷後面的識別符號是否為未定義的#ifndef的反義詞#ifdef。#ifndef通常用來描述此前未定義的常量。

/*arrays.h*/
#ifndef SIZE
#    define SIZE 100
#endif

一般地,當某個檔案包含幾個標頭檔案,而且每個標頭檔案都可能定義了相同的巨集時, 使用#ifndef可以防止對該巨集的重複定義。此時 ,第一個標頭檔案中的定義變成有效定義,而其他標頭檔案中的定義則被忽略。

下面是另外一個應用。假設把下面這行程式碼:

#include “arrays.h”

放在一個檔案頭部,那麼SIZE定義為100,但如果把:

#define SIZE 10

#include “arrays.h”

放在檔案頭部,那麼SIZE定義為10.在處理arrays.h中的行時,SIZE是已定義的,因此將跳過#define SIZE 100.有時候可能會這麼做,例如,可以用較小的陣列來測試程式。當程式令人滿意後,可以去除#define SIZE 10語句並重新編譯。這樣就不必擔心修改標頭檔案陣列本身了。

#ifndef指令通常用於防止多次包含同一檔案。也就是說,標頭檔案可以採用類似下面幾行的設定:

/*things.h*/

#ifndef THINGS_H_

    #define THINGS_H_

    /*標頭檔案的其餘部分*/

#endif

假設多次包含了該檔案。當前處理器第一次遇到該檔案時,THINGS_H_是未定義的,因此程式接著定義THINGS_H_,並處理檔案其餘部分。前處理器下次遇到該檔案時,THINGS_H_已經定義,因此,前處理器跳過該檔案的其餘部分。

為什麼會多次包含同一檔案呢?最常見的原因是許多包含檔案自身包含了其他檔案,因此可能顯式的包含其他檔案已經包含的檔案。為什麼這會成為問題呢?因為標頭檔案中的有些語句在一個檔案中只能出現一次(如結構型別的宣告)。標準C標頭檔案使用#ifndef技術來避免多次包含。有一個問題是如何確保您使用的識別符號在其他任何地方都沒有使定義過。通常編譯器提供商採用下述方法解決這個問題:用檔名作識別符號,並在檔名中使用大寫字母、用下劃線代替檔名中的句點字元、用下劃線(可能使用兩條下劃線)作字首和字尾。例如,檢查標頭檔案stdio.h,可以發現許多類似這樣的語句:

#ifndef _STDIO_H
#define _STDIO_H
//檔案內容
#endif

您也可以這樣做,但是,因為C標準保留使用下劃線作字首,所以您應避免這種用法。程式清單16.10使用#ifndef為程式清單16.6中的標頭檔案提供了多次包含保護。

程式清單16.10  names_st.h標頭檔案

//names_st.h  --帶有多次包含保護的修訂版本
#ifndef NAMES_H_
#define NAMES_H_
//常量
#define SLEN 32
//結構宣告
struct names_st
{
char first[SLEN];
char last[SLEN];
};
//型別定義
typedef struct names_st names;
//函式原型
void get_names(names *);
void show_names(const names *);
#endif

可以用程式清單16.11中的程式對該標頭檔案進行測試。使用程式清單16.10所示的標頭檔案時,程式正常工作。但是從程式清單16.10中刪除了#ifndef保護後,程式不能通過編譯。

//doubincl.c  --兩次包含同一檔案
#include <stdio.h>
#include "names_st.h"
#include "names_st.h"  //不小心兩次包含同一標頭檔案
int main(void)
{
names winner = {"Less","Ismoor"};
printf("The winner is %s %s.\n",winner.first,winner.last);
return 0;
}

三、#if和#elif指令

#if指令更像常規的C中的if;#if後跟常量整數表示式。如果表示式為非零值,則表示式為真。在該表示式中可以使用C的關係運算子和邏輯運算子。

#if SYS == 1
#include "ibm.h"
#endif

可以使用#elif(有些早期的實現不支援#elif)指令擴充套件if-else序列。例如,可以這樣使用:

#if SYS == 1
#include "ibmpc.h"
#elif SYS == 2
#include "vax.h"
#elif SYS == 3
#include "mac.h"
#else 
#include "general.h"
#endif 

許多新的實現提供另外一種方法來判斷一個名字是否已經定義。不需要使用:

#ifdef VAX

而是採用下面的形式:

#if defined(VAX)

這裡的defined( )是一個前處理器運算子。如果defined的引數已經使用#define定義過,那麼defined( )返回1;否則返回0。這種方法的 優點在於它可以和#elif一起使用 。下面用它重寫前面的示例:

#if defined(IBMPC)
#include "ibmpc.h"
#elif defined(VAX)
#include "vax.h"
#elif defined(MAC)
#include "mac.h"
#else 
#include "general.h"
#endif 

如果把這幾行用於VAX機上,那麼應在檔案前面的某處用下面這行指令定義過VAX:

#define VAX

條件編譯的一個用途是可以使程式更易於移植。通過在檔案開頭部分改變幾個關鍵的定義,就可以為不同系統設定不同的值幷包含不同檔案。

16.6.4  預定義巨集

表16.3列舉了C 標準指定的一些預定義巨集。

巨集意    義
_ _DATE_ _進行預處理的日期(“Mmm dd yyyy”形式的字串文字)
_ _FILE_ _代表當前原始碼檔名的字串文字
_ _LINE_ _代表當前原始碼檔案中的行號的整數常量
_ _STDC_ _設定為1時,表示該實現遵循C標準
_ _STDC_HOSTED_ _為本機環境設定為1,否則設為0
_ _STDC_VERSION_ _

為C99時設定為199901L

_ _TIME_ _原始檔編譯時間,格式為”hh: mm: ss”

C99標準提供一個名為_ _func_ _的預定義識別符號。_ _func_ _展開為一個代表函式名(該函式包含該識別符號)的字串該識別符號具有函式作用域,而巨集本質上具有檔案作用域。因而_ _func_ _是C語言的預定義識別符號,而非預定義巨集。

程式清單16.12  顯示了幾個使用這些預定義識別符號的示例。注意有些識別符號是C99新增的,C99這前的編譯器可能不接受它們。

程式清單  16.12  predef.c程式

//predef.c  --預定義識別符號
#include <stdio.h>
void why_me( );
int main()
{
printf("The file is %s.\n",_ _FILE_ _);
printf("The date is %s.\n",_ _DATE_ _);
printf("The time is %s.\n",_ _TIME_ _);
printf("The version is %ld.\n",_ _STDC_VERSION_ _);
printf("This is line %d.\n",_ _LINE_ _);
printf("The function is %s\n",_ _func_ _);
why_me( );
return 0;
}
void why_me()
{
printf("This function is %s.\n",_ _func_ _);
printf("This is line %d.\n",_ _LINE_ _);
}
下面是一個執行示例:
The file is predef.c.
The date is Jul 19 2004.
The time is 10:00:30.
The version is 199901.
This is line 11.
The function is main
The function is why_me
This is line 21.

16.6.5  #line和#error

#line指令用於重置由_ _LINE_ _和_ _FILE_ _巨集報告的行號和檔名。可這樣使用#line:

#line 1000            //把當前行號重置為1000

#line 10 “cool.c”  //把行號重置為10,檔名重置為cool.c

#error指令使前處理器發出一條錯誤訊息,該訊息包含指令中的文字。可能的話,編譯過程應該中斷。

可以這樣使用#error:

#if _ _ STDC_VERSION_ _ !=199901L

    #error Not C99

#endif

16.6.6  #pragma

在現代的編譯器中,可用命令列引數或IDE選單修改編譯器的某些設定。也可用#pragma將編譯器指令置於原始碼中。

例如,在開發C99時,用C9X代表C99。編譯器可以使用下面的編譯指示來啟用對C9X的支援:

#pragma C9X on

C99還提供了_Pragma前處理器運算子。_Pragma可將字串轉換成常規的編譯指令。例如:

_Pragma(“nonstandardtreatmenttypeB on”)

等價於下面的指令:

#pragma nonstandardtreatmenttypeB on

因為該運算子沒有使用#符號,所以可以將它作為巨集展開的一部分

#define PRAGMA(X) _Pragma(#X)

#define LIMRG(X) PRAGMA(STDC CX_LIMITED_RANG X)

接著可以使用類似下面的程式碼:

LIMRG(ON)

順便提一下,雖然下面的定義看上去可以正常執行,但實際上並非如此:

#define LIMRG(X) _Pragma(STDC CX_LIMITED_RAND #X)

問題在於上面的程式碼依賴於字串連線功能,但是,直到預處理過程完成後編譯器才連線字串。

_Pragma運算子完成字串析構(destringizing)工作,也就是說,將字串中的轉義序列轉換成它所代表的字元。因而:

_Pragma(“use_bool \”true \”false”)

變成:

#Pragma use_bool “true “false