C語言的本質(21)——預處理之三:其它預處理特性及總結

尹成發表於2014-07-17

 

C標準規定了幾個特殊的巨集,在不同的地方使用可以自動展開成不同的值,預編譯程式對於在源程式中出現的這些串將用合適的值進行替換。這些巨集有下面這些:

 __FILE__ 展開為當前原始檔的檔名,是一個字串

__LINE__ 展開為當前程式碼行的行號,是一個整數

__DATE__ 展開為包含當前日期的字串

__STDC__ 如果編譯器遵循ANSIC標準,它就是個非零值

__TIME__ 展開為包含當前時間的字串

注意:是雙下劃線,而不是單下劃線。


常用的有__FILE__和__LINE__這兩個巨集在原始碼中不同的位置使用會自動取不同的值,顯然不是用#define能定義得出來的,它們是編譯器內建的特殊的巨集。在列印除錯資訊時列印這兩個巨集可以給開發者非常有用的提示。

 

#include<stdio.h>
int main(void)
{
   printf("HelloWorld!\n");
   printf("%s\n",__FILE__);
   printf("%d\n",__LINE__);
   return 0;
}

 

下面我們自己實現斷言assert函式,以理解它的原理。

 

/* assert.h standard header */
#undef assert   /* remove existing definition */
 
#ifdef NDEBUG
         #defineassert(test) ((void)0)
#else                  /*NDEBUG not defined */
         void_Assert(char *);
         /*macros */
         #define_STR(x) _VAL(x)
         #define_VAL(x) #x
         #defineassert(test) ((test) ? (void)0 \
                   :_Assert(__FILE__ ":" _STR(__LINE__) " " #test))
#endif

C標準規定assert應該實現為巨集定義而不是一個真正的函式,並且assert(test)這個表示式的值應該是void型別的。首先用#undef assert確保取消前面對assert的定義,然後分兩種情況:如果定義了NDEBUG,那麼assert(test)直接定義成一個void型別的值,什麼也不做;如果沒有定義NDEBUG,則要判斷測試條件test是否成立,如果條件成立就什麼也不做,如果不成立則呼叫_Assert函式。假設在main.c檔案的第33行呼叫assert(is_sorted()),那麼__FILE__是字串"main.c",__LINE__是整數33,#test是字串"is_sorted()"。注意_STR(__LINE__)的展開過程:首先展開成_VAL(33),然後進一步展開成字串"33"。這樣,最後_Assert呼叫的形式是_Assert("main.c"":" "33" " " "is_sorted()"),傳給_Assert函式的字串是"main.c:33is_sorted()"。_Assert函式是我們自己定義的,在另一個原始檔中:

 

/* xassert.c _Assert function */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
 
void _Assert(char *mesg)
{                 /*print assertion message and abort */
         fputs(mesg,stderr);
         fputs("-- assertion failed\n", stderr);
         abort();
}

 

注意,在標頭檔案assert.h中自己定義的內部使用的識別符號都以_線開頭,例如_STR,_VAL,_Assert,因為我們在模擬C標準庫的實現,以_線開頭的識別符號通常由編譯器和C語言庫使用,在/usr/include下的標頭檔案中你可以看到大量_線開頭的識別符號。另外為什麼我們不直接在assert的巨集定義中呼叫fputs和abort呢?因為呼叫這兩個函式需要包含stdio.h和stdlib.h,C標準庫的標頭檔案應該是相互獨立的,一個程式只要包含assert.h就應該能使用assert,而不應該再依賴於別的標頭檔案。_Assert中的fputs向標準錯誤輸出列印錯誤資訊,abort異常終止當前程式。

 

現在測試一下我們的assert實現,把assert.h和xassert.c和測試程式碼main.c放在同一個目錄下。

 

/* main.c */
#include "assert.h"
 
int main(void)
{
         assert(2>3);
         return0;
}

注意#include "assert.h"要用"引號而不要用<>括號,以保證包含的是我們自己寫的assert.h而非C標準庫的標頭檔案。然後編譯執行:

 

$ gcc main.c xassert.c
$ ./a.out
main.c:6 2>3 -- assertion failed
Aborted

#error指令將使編譯器顯示一條錯誤資訊,然後停止編譯。

#line指令改變_LINE_與_FILE_的內容,它們是在編譯程式中預先定義的識別符號。

 

#line舉例:

#line 100 //初始化行計數器
#include<stdio.h> //行號100
int main(void)
{
    printf("HelloWorld!\n");
    printf("%d",__LINE__);
    return 0;
}


輸出104

 

#pragma指令沒有正式的定義。它預處理指示供編譯器實現一些非標準的特性,C標準沒有規定#pragma後面應該寫什麼以及起什麼作用,由編譯器自己規定。典型的用法是禁止或允許某些煩人的警告資訊。有的編譯器用#pragma定義一些特殊功能暫存器名,有的編譯器用#pragma定位連結地址。如果編譯器在程式碼中碰到不認識的#pragma指示則忽略它,例如gcc的#pragma指示都是#pragma GCC ...這種形式,用別的編譯器編譯則忽略這些指示。

 

 

預處理指令總結:

預處理指令是以#號開頭的程式碼行。#號必須是該行除了任何空白字元外的第一個字元。#後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字元。整行語句構成了一條預處理指令,該指令將在編譯器進行編譯之前對原始碼做某些轉換。

預處理功能是C語言特有的功能,它是在對源程式正式編譯前由預處理程式完成的。程式設計師在程式中用預處理命令來呼叫這些功能。

巨集定義可以帶有引數,巨集呼叫時是以實參代換形參。而不是“值傳送”。

為了避免巨集代換時發生錯誤,巨集定義中的字串應加括號,字串中出現的形式引數兩邊也應加括號。

檔案包含是預處理的一個重要功能,它可用來把多個原始檔連線成一個原始檔進行編譯,結果將生成一個目標檔案。

條件編譯允許只編譯源程式中滿足條件的程式段,使生成的目標程式較短,從而減少了記憶體的開銷並提高了程式的效率。

使用預處理功能便於程式的修改、閱讀、移植和除錯,也便於實現模組化程式設計。

相關文章