C/C++中的預處理

本文主要講述C/C++中預處理命令相關的內容。主要參考資料在後面給出。

我們可以在C源程序中插入傳給編譯程序的各種指令(宏),這些指令被稱爲預處理器指令,它們擴充了程序設計的環境。

在將一個C源程序轉換爲可執行程序的過程中, 編譯預處理是最初的步驟. 這一步驟是由預處理器(preprocessor)來完成的. 在源程序被編譯器處理之前, 預處理器首先對源程序中的"宏(macro)"進行處理.

C 初學者可能對預處理器沒什麼概念, 這是情有可原的: 一般的C編譯器都將預處理, 彙編, 編譯, 連接過程集成到一起了,編譯預處理往往在後臺運行。 在有的C編譯器中, 這些過程統統由一個單獨的程序來完成, 在編譯的不同階段分別實現這些不同的功能,我們可以指定特定的命令選項來執行這些指定功能。有的C編譯器則使用獨立的程序來完成這些步驟. 可單獨調用這些程序來完成特定步驟,比如在 gcc 中, 進行編譯預處理的程序被稱爲 CPP , 它的可執行文件名爲 cpp

編譯預處理命令的語法與C語言的語法是完全獨立的,比如: 你可以將一個宏擴展爲與C語法格格不入的內容, 但該內容與後面的語句結合在一個若能生成合法的C語句, 也是可以正確編譯的。

預處理命令簡介

預處理命令由#(hash字符)開頭, 它獨佔一行, #之前只能是空白符. 以#開頭的語句就是預處理命令, 不以#開頭的語句爲C中的代碼行。常用的預處理命令如下:

#define         定義一個預處理宏
#undef          取消宏的定義

#if             編譯預處理中的條件命令, 相當於C語法中的if語句
#ifdef          判斷某個宏是否被定義, 若已定義, 執行隨後的語句
#ifndef         與#ifdef相反, 判斷某個宏是否未被定義
#elif           若#if, #ifdef, #ifndef或前面的#elif條件不滿足, 則執行#elif之後的語句, 相當於C語法中的else-if
#else           與#if, #ifdef, #ifndef對應, 若這些條件不滿足, 則執行#else之後的語句, 相當於C語法中的else
#endif          #if, #ifdef, #ifndef這些條件命令的結束標誌.
defined         與#if, #elif配合使用, 判斷某個宏是否被定義

#include        包含文件命令
#include_next   與#include相似, 但它有着特殊的用途

#line           標誌該語句所在的行號
#               將宏參數替代爲以參數值爲內容的字符竄常量
##              將兩個相鄰的標記(token)連接爲一個單獨的標記
#pragma         說明編譯器信息

#warning        顯示編譯警告信息
#error          顯示編譯錯誤信息

預處理的文法

預處理並不分析整個源代碼文件, 它只是將源代碼分割成一些標記(token), 識別語句中哪些是C語句, 哪些是預處理語句。

預處理器能夠識別C標記, 文件名, 空白符, 文件結尾標誌。預處理語句格式: #command name(...) token(s)

這裏,

  • command 預處理命令的名稱。

    它之前以 # 開頭, #之後緊隨預處理命令, 標準C允許 # 兩邊可以有空白符, 但比較老的編譯器可能不允許這樣. 若某行中只包含 # (以及空白符), 那麼在標準C中該行被理解爲空白. 整個預處理語句之後只能有空白符或者註釋, 不能有其它內容。

  • name 代表宏名稱,它可帶參數。

    參數可以是可變參數列表(C99).

  • token(s) 宏體,將替換宏名稱的語句。

    語句中可以利用 \ 來換行.

e.g.

# define ONE 1 /* ONE == 1 */

等價於:

#define ONE 1

#define err(flag, msg) if(flag) \
   printf(msg)

等價於:

#define err(flag, msg) if(flag) printf(msg)

預處理命令詳述

1、聲明定義

#define

#define 命令定義一個宏,定義一個標識符和一個串(也就是字符集),在源程序中發現該標識符時,都用該串替換之。這種標識符稱爲宏名字,相應的替換稱爲宏代換。一般形式如下:

#define MACRO_NAME(args) tokens(opt)

這種語句不用分號結尾。宏名字和串之間可以有多個空白符,但串開始後只能以新行終止。之後出現的 MACRO_NAME 將被替代爲所定義的標記( tokens ). 宏可帶參數, 而後面的標記也是可選的。

對象宏

不帶參數的宏被稱爲"對象宏(objectlike macro)"。

#define 經常用來定義常量, 此時的宏名稱一般爲大寫的字符串。 這樣利於修改這些常量。
e.g.

#define MAX 100
int a[MAX];

#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

這裏, #define __FILE_H__ 中的宏就不帶任何參數, 也不擴展爲任何標記. 這經常用於包含頭文件.

要調用宏, 只需在代碼中指定宏名稱, 相應宏將被替代爲它被定義的內容。例如:我們使用 LEFT 代表1,用 RIGHT 代表0,我們使用兩個 #define 指令:

#define LEFT 1
#define RIGHT 0

每當在源程序中遇到 LEFTRIGHT 時,編譯程序都用1或0替換。宏代換就是用相關的串替代標識符。因此,如果希望定義一條標準錯誤信息時,可以如下定義:

#define ERROR_MS “Standard error on input \n”

如果一個串長於一行,可在行尾用反斜線”\”續行,如下:

#define LONG_STRING “This is a very very long \
String that is used as an example”

函數宏

帶參數的宏也被稱爲"函數宏". 利用宏可以提高代碼的運行效率: 子程序的調用需要壓棧出棧, 這一過程如果過於頻繁會耗費掉大量的CPU運算資源. 所以一些代碼量小但運行頻繁的代碼如果採用帶參數宏來實現會提高代碼的運行效率.

  1. 函數宏的參數是固定的情況

    函數宏的定義採用這樣的方式: #define name( args ) tokens

    其中的 argstokens 都是可選的. 它和對象宏定義上的區別在於對象宏名稱之後不帶括號.

    注意: 定義時name 之後的左括號 ( 必須緊跟 name , 之間不能有空格, 否則這就定義了一個對象宏, 它將被替換爲以 ( 開始的字符串;但在 用函數宏時 , name( 之間卻可以有空格.

    例如:

    #define mul(x,y) ((x)*(y))
    

    注意, 函數宏之後的參數要用括號括起來, 看看這個例子:

    #define mul(x,y) x*y
    

    mul(1, 2+2); 將被擴展爲: 1*2 + 2

    同樣, 整個標記串也應該用括號引用起來:

    #define mul(x,y) (x)*(y)
    

    sizeof mul(1,2.0) 將被擴展爲 sizeof 1 * 2.0

    調用函數宏時候, 傳遞給它的參數可以是函數的返回值, 也可以是任何有意義的語句:
    e.g.

    mul (f(a,b), g(c,d));
    

    e.g.

    #define insert(stmt) stmt
    

    這裏:

    • insert ( a=1; b=2;) 相當於在代碼中加入 a=1; b=2 .
    • insert ( a=1, b=2;) 就有問題了: 預處理器會提示出錯: 函數宏的參數個數不匹配. 預處理器把","視爲參數間的分隔符.
    • insert ((a=1, b=2;)) 可解決上述問題.
    1. 在定義和調用函數宏時候, 要注意一些問題

      1. 使用 do-while(0) 解決結尾 ; 問題

        我們經常用 {} 來引用函數宏被定義的內容, 這就要注意調用這個函數宏時的 ; 問題.
        example_3.7:

        #define swap(x,y) { unsigned long _temp=x; x=y; y=_tmp}
        

        如果這樣調用它: swap(1,2); 將被擴展爲: { unsigned long _temp=1; 1=2; 2=_tmp};

        明顯後面的;是多餘的, 我們應該這樣調用: swap(1,2)

        雖然這樣的調用是正確的, 但它和C語法相悖, 可採用下面的方法來處理被 {} 括起來的內容:

        #define swap(x,y) \
           do { unsigned long _temp=x; x=y; y=_tmp} while (0)
        

        swap(1,2); 將被替換爲: do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);

        在Linux內核源代碼中對這種 do-while(0) 語句有這廣泛的應用.

      2. 無法被 do-while(0) 實現的宏

        有的函數宏是無法用 do-while(0) 來實現的, 所以在調用時不能帶上 ;, 最好在調用後添加註釋說明。
        eg_3.8:

        #define incr(v, low, high) \
           for ((v) = (low),; (v) <= (high); (v)++)
        

        只能以這樣的形式被調用: incr(a, 1, 10) /* increase a form 1 to 10 */

  2. 函數宏中的參數包括可變參數列表的情況

    C99標準中新增了可變參數列表的內容。 不光是函數, 函數宏中也可以使用可變參數列表。

    #define name(args, ...) tokens
    #define name(...) tokens
    

    ... 代表可變參數列表, 如果它不是僅有的參數, 那麼它只能出現在參數列表的最後. 調用這樣的函數宏時, 傳遞給它的參數個數要不少於參數列表中參數的個數(多餘的參數被丟棄)。

    通過 __VA_ARGS__ 來替換函數宏中的可變參數列表. 注意 __VA_ARGS__ 只能用於函數宏中參數中包含有 ... 的情況.

    e.g.

    #ifdef DEBUG
    #define my_printf(...) fprintf(stderr, __VA_ARGS__)
    #else
    #define my_printf(...) printf(__VA_ARGS__)
    #endif
    

    tokens 中的 __VA_ARGS__ 被替換爲函數宏定義中的 ... 可變參數列表。

注意在使用#define時候的一些常見錯誤

#define MAX = 100
#define MAX 100;

=, ; 的使用要值得注意,再就是調用函數宏是要注意, 不要多給出 ;

注意: 函數宏對參數類型是不敏感的, 你不必考慮將何種數據類型傳遞給宏。 那麼, 如何構建對參數類型敏感的宏呢? 參考本章的第九部分, 關於 ## 的介紹。

關於定義宏的另外一些問題

  1. (1)重複定義宏

    宏可以被多次定義, 前提是這些定義必須是相同的. 這裏的"相同"要求 先後定義中空白符出現的位置相同, 但具體的空白符類型或數量可不同, 比如原先的空格可替換爲多個其他類型的空白符: 可爲tab, 註釋…

    e.g.

    #define NULL 0
    #define NULL /* null pointer */     0
    

    上面的重定義是相同的, 但下面的重定義不同:

    #define fun(x) x+1
    #define fun(x) x + 1 
    //或: #define fun(y) y+1
    

    如果多次定義時, 再次定義的宏內容是不同的, gcc 會給出 NAME redefined 警告信息。

    應該避免重新定義函數宏, 不管是在預處理命令中還是C語句中, 最好對某個對象只有單一的定義. 在 gcc 中, 若宏出現了重定義, gcc 會給出警告。

  2. (2) 命令行擴展定義

    gcc 中, 可在命令行中指定對象宏的定義:

    e.g.

    $gcc -Wall -DMAX=100 -o tmp tmp.c
    

    相當於在 tmp.c 中添加 #define MAX 100

    那麼, 如果原先 tmp.c 中含有MAX宏的定義, 那麼再在 gcc 調用命令中使用 -DMAX , 會出現什麼情況呢?

    • -DMAX=1, 則正確編譯.
    • -DMAX 的值被指定爲不爲1的值, 那麼 gcc 會給出 MAX 宏被重定義的警告, MAX 的值仍爲1.

    注意: 若在調用 gcc 的命令行中不顯示地給出對象宏的值, 那麼 gcc 賦予該宏默認值(1), 如: -DVAL == -DVAL=1

  3. (3) 宏定義作用域

    #define 所定義的宏的作用域
    宏在定義之後才生效, 若宏定義被 #undef 取消, 則 #undef 之後該宏無效. 並且字符串中的宏不會被識別

    e.g.

    #define ONE 1
    sum = ONE + TWO    /* sum = 1 + TWO */
    #define TWO 2
    sum = ONE + TWO    /* sum = 1 + 2    */
    #undef ONE
    sum = ONE + TWO    /* sum = ONE + 2 */
    char c[] = "TWO"   /* c[] = "TWO", NOT "2"! */
    
  4. (4) 遞歸嵌套定義

    宏的替換可以是遞歸的, 所以可以嵌套定義宏。也就是說,定義一個宏名字之後,可以在其他宏定義中使用。

    e.g.

    # define ONE NUMBER_1
    # define NUMBER_1 1
    int a = ONE /* a = 1 */
    
    #define ONE 1
    #define TWO ONE+ONE
    #define THREE ONE+TWO
    

#undef

#undef 用來取消宏定義, 它與 #define 對立,經常用來刪除前面定義的宏名字。也就是說,它“不定義”宏。一般形式爲:

#undef macro-name

如夠被取消的宏實際上沒有被 #define 所定義, 針對它的 #undef 並不會產生錯誤,當一個宏定義被取消後, 可以再度定義它。

#, ##

預處理操作符 ### 主要作用是允許預處理程序對付某些特殊情況,多數程序中並不需要,它們可以在 #define 中使用。

由於經常用於對字符串的預處理操作, 所以他們也經常用於 printf, puts 之類的字符串顯示函數中。例如:

e.g.

#define TEST(a,b) printf( #a "<" #b "=%d\n", (a)<(b));

# 命令

操作符 # 通常稱爲字符串化的操作符,它把其後的串變成用雙引號包圍的串。用於在宏擴展之後將 tokens 轉換爲以 tokens 爲內容的字符串常量。例如:

#include <stdio.h>
#define mkstr(s) #s
int main(void)
{
 printf(mkstr(I like C));
 Return 0;
}

預處理程序把以下的語句: printf(mkstr(I like C)); 變成 printf(“I like C”);

注意: # 只針對緊隨其後的 token 有效!

## 命令

操作符 ## 把兩個標記拼在一起,形成一個新標記。用於將它前後的兩個 token 組合在一起轉換成以這兩個 token 爲內容的字符串常量。

#include <stdio.h>
#define concat(a,a) a##b
int main(void)
{
 int xy = 10;
 printf(“%d”,concat(x,y));
 Return 0;
}

預處理程序把以下語句: printf(“%d”,concat(x,y)); 變成 printf(“%d”,xy);注意: ## 前後必須要有 token

2、條件編譯

若干編譯指令允許程序員有選擇的編譯程序源代碼的不同部分,這種過程稱爲條件編譯。

#if, #elif, #else, #endif

#if, #elif, #else, #endif 允許程序員根據常數表達式的結果有條件的包圍部分代碼。一般的表達形式是:

#if 常量表達式1
   語句...
#elif 常量表達式2
   語句...
#elif 常量表達式3
   語句...
...
#else
   語句...
#endif

它們後面所判斷的宏只能是對象宏. 如果 name 爲名的宏未定義, 或者該宏是函數宏. 那麼在 gcc 中使用 -Wundef 選項會顯示宏未定義的警告信息。

#if

表示“如果”, 其一般形式是:

#if constant-expression
 Statement sequence
#endif

#if 後的常數表達式爲真,則 #if#endif 中間的代碼被編譯,否則忽略該代碼段。 #endif 標記 #if 塊的結束。

#elif 指令

表示“否則,如果”,爲多重編譯選擇建立一條 if-else-if (如果-否則-如果鏈)。一般形式如下:

#if expression
 Statement sequence
#elif expression1
 Statement sequence
#elif expression2
 Statement sequence
#elif expression
 Statement sequence
#endif

如果 #if 表達式爲真,該代碼塊被編譯,不測試其他 #elif 表達式。否則,序列中的下一塊被測試,如果成功則編譯之。

#else 指令

作用與C語言的 else 相似, #if 指令失敗時它可以作爲備選指令。例如:

#include <stdio.h>
#define MAX 100

int main(void)
{
 #if MAX>99
 printf(“Compiled for array greater than 99.\n”);
 #else
 printf(“Complied for small array.\n”);
 #endif
 return 0;
}

注意, #else 既是標記 #if 塊的結束,也標記 #else 塊的開始。因爲每個 #if 只能寫一個 #endif 匹配。

與代碼中的 if/else 類似

#if#else 分別相當於C語句中的 if , else 。它們根據常量表達式的值來判別是否執行後面的語句。 #elif 相當於C中的 else-if 。使用這些條件編譯命令可以方便地實現對源代碼內容的控制。 else 之後不帶常量表達式, 但若包含了常量表達式, gcc 只是給出警告信息。使用它們可以提升代碼的可移植性——針對不同的平臺使用執行不同的語句. 也經常用於大段代碼註釋。

e.g.

#if 0
{
   一大段代碼;
}
#endif

常量表達式可以是包含宏, 算術運算, 邏輯運算等等的合法C常量表達式, 如果常量表達式爲一個未定義的宏, 那麼它的值被視爲0,

#if MACRO_NON_DEFINED == #if 0

在判斷某個宏是否被定義時, 應當避免使用#if, 因爲該宏的值可能就是被定義爲0。 而應當使用下面介紹的 #ifdef#ifndef

#ifdef, #ifndef, defined

#ifdef, #ifndef, defined 用來測試某個宏是否被定義。

#if, #elif, #else 不同, #indef, #ifndef, defined 測試的宏可以是對象宏, 也可以是函數宏。在 gcc 中使用 -Wundef 選項不會顯示宏未定義的警告信息。

#ifdef 指令

一般形式如下:

#ifdef macro-name
 Statement sequence
#endif

如果 macro-name 原先已經被一個 #define 語句定義,則編譯其中的代碼塊。

#ifndef 指令

一般形式如下:

#ifndef macro-name
 Statement sequence
#endif

如果 macro-name 當前未被 #define 語句定義,則編譯其中的代碼塊。

#ifndef 經常用於避免頭文件的重複引用,例如:

#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif

defined 指令

#ifdef 之外,還有另外一種確定是否定義宏名字的方法,即可以將 #if 指令與 defined 指令一起使用。 defined 操作符的一般形式如下:

defined macro-name

macro-name 當前被定義,則表達式爲真返回1,否則爲假返回0。

defined#if, #elif, #else 結合使用來判斷宏是否被定義, 乍一看好像它顯得多餘, 因爲已經有了 #ifdef#ifndef ,其實使用 defined 的一個原因是,它允許被做爲 #ifdef#ifndef , #elif 的判斷條件(如:由 #elif 語句確定宏名字存在),因此顯得更爲靈活。

  1. #ifdef 等價的判斷

    例如,確定宏 MY 是否定義,可以使用下列兩種預處理命令之一:

    #if defined MY
    

    #ifdef MY
    
  2. #ifndef 等價的判斷

    也可以在 defined 之前加上感嘆號”!”來反轉相應的條件。例如,只有在DEBUG未定義的情況下才編譯。

    #if !defined DEBUG
     printf(“Final Version!\n”);
    #endif
    

    #ifndef DEBUG
     printf(“Final Version!\n”);
    #endif
    
  3. defined 做爲條件語句的判斷

    比如在一條判斷語句中聲明多個判別條件,如:

    #if defined(VAX) && defined(UNIX) && !defined(DEBUG)
    

    注:可以沒有括號。

結合 #else / #elif 指令使用

#ifdef / #ifndef 可與 #else / #elif 結合使用,如下:

#inlucde <stdio.h>
#define T 10
int main(void)
{
 #ifdef t
 printf(“Hi T\n”);
 #else
 printf(“Hi anyone\n”);
 #endif
 #ifndef M
 printf(“M Not Defined\n”);
 #endif
 Return 0;
}

3、文件包含

#include 指令

用於文件包含. 含義是要求編譯程序讀入另一個源文件。在 #include 命令所在的行不能含有除註釋和空白符之外的其他任何內容。一般形式:

#include "headfile"
#include <headfile>
#include 預處理標記

前面兩種形式大家都很熟悉,被讀入文件的名字必須用雙引號(“”)或一對尖括號(<>)包圍,例如:

#include “stdio.h”
#include <stdio.h>

表示都使C編譯程序讀入並編譯頭文件以用於I/O系統庫函數。

#include 預處理標記 形式中, 預處理標記會被預處理器進行替換, 替換的結果必須符合前兩種形式中的某一種。

包含文件中可以包含其他 #include 指令,稱爲嵌套包含。允許的最大嵌套深度隨編譯器而變。

被包含文件名用雙引號或尖括號包圍決定了對指定文件的搜索方式。當文件名被尖括號包圍時,搜索按編譯程序(如 gcc)作者的定義進行,一般用於搜索某些專門放置包含文件的特殊目錄。當文件名被雙引號包圍時,搜索按編譯程序實時的規定進行,一般搜索當前目錄。如未發現,再按尖括號包圍時的辦法重新搜索一次。通常,絕大多數程序員使用尖括號包圍標準的頭文件,雙引號用於包圍與當前程序相關的文件名。

實際上, 真正被添加的頭文件並不一定就是 #include 中所指定的文件。 #include "headfile"= 包含的頭文件當然是同一個文件, 但 =#include <headfile> 包包含的"系統頭文件"可能是另外的文件. 但這不值得被注意. 感興趣的話可以查看宏擴展後到底引入了哪些系統頭文件。

關於 #include "headfile"和#include <headfile> 的區別以及如何在 gcc 中包含頭文件的詳細信息, 具體需要參考GCC相關的文檔。

#include_next 指令

相對於 #include, 我們對 #include_next 不太熟悉。 #include_next 僅用於特殊的場合. 它被用於頭文件中(#include 既可用於頭文件中, 又可用於 .c 文件中)來包含其他的頭文件. 而且包含頭文件的路徑比較特殊: 從當前頭文件所在目錄之後的目錄來搜索頭文件。比如:

若頭文件的搜索路徑依次爲 =A,B,C,D,E=, 而 =#include_next= 所在的當前頭文件位於 =B= 目錄, 
那麼 =#include_next= 使得預處理器從 =C,D,E= 目錄來搜索 =#include_next= 所指定的頭文件。

可參考預處理命令 cpp 的手冊進一步瞭解 #include_next

4、預定義宏

標準C中定義了一些對象宏, 這些宏的名稱以 __ 開頭和結尾, 並且都是大寫字符. 這些預定義宏可以被 #undef, 也可以被重定義。

下面列出一些標準C中常見的預定義對象宏(其中也包含gcc自己定義的一些預定義宏:

__LINE__         當前語句所在的行號, 以10進制整數標註.
__FILE__         當前源文件的文件名, 以字符串常量標註.
__DATE__          程序被編譯的日期, 以"Mmm dd yyyy"格式的字符串標註.
__TIME__           程序被編譯的時間, 以"hh:mm:ss"格式的字符串標註, 該時間由asctime返回.

__STDC__           如果當前編譯器符合ISO標準, 那麼該宏的值爲1
__STDC_VERSION__   如果當前編譯器符合C89, 那麼它被定義爲199409L, 如果符合C99, 那麼被定義爲199901L.
                   我用gcc, 如果不指定-std=c99, 其他情況都給出__STDC_VERSION__未定義的錯誤信息, 咋回事呢?
__STDC_HOSTED__     如果當前系統是"本地系統(hosted)", 那麼它被定義爲1. 本地系統表示當前系統擁有完整的標準C庫.

另外, gcc 定義的預定義宏:

__OPTMIZE__         如果編譯過程中使用了優化, 那麼該宏被定義爲1.
__OPTMIZE_SIZE__   同上, 但僅在優化是針對代碼大小而非速度時才被定義爲1.
__VERSION__         顯示所用gcc的版本號.

具體可參考 GCC the complete reference 。要想看到 gcc 所定義的所有預定義宏, 可以運行:

$cpp -dM /dev/null

5、擴展控制

#line

#line 指令用來修改 __LINE____FILE__ 的內容。前面介紹了, __LINE__和__FILE__ 都是編譯程序中預定義的標識符。標識符 __LINE__ 的內容是當前被編譯代碼行的行號,=__FILE__= 的內容是當前被編譯源文件的文件名。 #line 的一般形式是:

#line number “filename”

其中, number 是正整數並變成 __LINE__ 的新值;可選的 filename 是合法文件標識符並變成 __FILE__ 的新值。 #line 主要用於調試和特殊應用。常見的使用:

printf("line: %d, file: %s\n", __LINE__, __FILE__);
#line 100 "haha"
printf("line: %d, file: %s\n", __LINE__, __FILE__);
printf("line: %d, file: %s\n", __LINE__, __FILE__);

以上代碼,輸出顯示:

line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha

一個完整的例子, main.cpp 文件內容如下:

 1 #include<iostream>
 2 using std::cout;
 3 using std::cin;
 4 using std::endl;
 5 #line 2 "myfile.h"
 6 int main(int argc, char *argv[])
 7 {
 8     cout<<__LINE__<<":"<<__FILE__<<endl;
 9     return 0;
10 }

上述代碼編譯運行輸出如下:

[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make main
g++     main.cpp   -o main
[quietheart@lv-k pre_test]$ ls
main  main.cpp
[quietheart@lv-k pre_test]$ ./main 
4:myfile.h

由上面輸出可以看出,

  • __LINE__ 的內容變成了相對 #line 宏的內容,即 #line 下面的第一行就是 #line 指定的2;
  • 同時 __FILE__ 變成了 #line 指定的 myfile.h

#pragma, _Pragma

#pragma 編譯器用來添加新的預處理功能或者顯示一些編譯信息. 是編譯程序實現時定義的指令,它允許由此向編譯程序傳入各種指令。例如:

一個編譯程序可能具有支持跟蹤程序執行的選項,此時可以用 =#pragma= 語句選擇該功能。

編譯程序忽略其不支持的 #pragma 選項, #pragma 提高C源程序對編譯程序的可移植性。

#pragma 的格式是各編譯器特定的, gcc 的如下:

#pragma GCC name token(s)

#pragma 之後有兩個部分: GCC 和特定的 pragma name

下面分別介紹 gcc 中常用的。

(1) #pragma GCC dependency

dependency 測試當前文件(既該語句所在的程序代碼)與指定文件(既 #pragma 語句最後列出的文件)的時間戳。 如果指定文件比當前文件新, 則給出警告信息。

e.g. 在 demo.c 中給出這樣一句:

#pragma GCC dependency "temp-file"

然後在 demo.c 所在的目錄新建一個更新的文件:

$touch temp-file

編譯:

$gcc demo.c

會給出這樣的警告信息: warning: current file is older than temp-file

如果當前文件比指定的文件新, 則不給出任何警告信息。還可以在在 #pragma 中給添加自定義的警告信息。

e.g.

#pragma GCC dependency "temp-file" "demo.c needs to be updated!"

可能會有如下警告

1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file

注意: 後面新增的警告信息要用""引用起來, 否則 gcc 將給出警告信息。

(2) #pragma GCC poison token(s)

若源代碼中出現了 #pragma 中給出的 token(s) , 則編譯時顯示警告信息. 它一般用於在調用你不想使用的函數時候給出出錯信息。

e.g.

#pragma GCC poison scanf
scanf("%d", &a);

會輸出如下:

warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"

注意, 如果調用了 poison 中給出的標記, 那麼編譯器會給出的是出錯信息. 關於第一條警告, 我還不知道怎麼避免, 用""將 token(s) 引用起來也不行.

(3) #pragma GCC system_header

#pragma GCC system_header 直到文件結束之間的代碼會被編譯器視爲系統頭文件之中的代碼. 系統頭文件中的代碼往往不能完全遵循C標準, 所以頭文件之中的警告信息往往不顯示. (除非用 #warning 顯式指明)。

(這條 #pragma 語句還沒發現用什麼大的用處)

_Pragma 實現將 #pragma 用於宏擴展

由於 #pragma 不能用於宏擴展, 所以gcc還提供了 _Pragma :

e.g.

#define PRAGMA_DEP #pragma GCC dependency "temp-file"

由於預處理之時進行一次宏擴展, 採用上面的方法會在編譯時引發錯誤, 要將 #pragma 語句定義成一個宏擴展, 應該使用下面的 _Pragma 語句:

#define PRAGMA_DEP _Pragma("GCC dependency \"temp-file\"")

注意, () 中包含的""引用之前引該加上\轉義字符.

#warning, #error

#warning, #error 分別用於在編譯時顯示警告和錯誤信息, 格式如下:

#warning tokens
#error tokens

注意, #error#warning 後的 token 要用""引用起來!在 gcc 中,

  • 如果給出了 warning , 編譯繼續進行,
  • 但若給出了 error , 則編譯停止,
  • 若在命令行中指定了 -Werror ( gcc 手冊上意思是將所有 warning 變爲 error ), 即使只有警告信息, 也不編譯.

#warning 指令

e.g.

#warning "some warning"

error 指令

#error 指令強制編譯程序停止編譯,它主要用於程序調試。 #error 指令的一般形式是:

#error error-message

注意,宏串 error-message 不用雙引號包圍。遇到 #error 指令時,錯誤信息被顯示,可能同時還顯示編譯程序作者預先定義的其他內容。例如 main.cpp 文件內容如下:

 1 #include<iostream>
 2 using std::cout;
 3 using std::cin;
 4 using std::endl;
 5 #ifdef MMM
 6 #error myerror //如果執行了這個就會阻止編譯通過
 7 #endif
 8 int main(int argc, char *argv[])
 9 {
10     cout<<"hello world!"<<endl;
11     return 0;
12 }

編譯過程如下:

[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make CXXFLAGS+=-DMMM pre_error
g++ -DMMM    pre_error.cpp   -o pre_error
pre_error.cpp:6:2: error: #error myerror
 #error myerror //如果執行了這個就會阻止編譯通過
  ^~~~~
<內置>: recipe for target 'pre_error' failed
make: *** [pre_error] Error 1
[quietheart@lv-k pre_test]$ ls
main.cpp
[quietheart@lv-k pre_test]$ make main
g++     main.cpp   -o main
[quietheart@lv-k pre_test]$ ls
main  main.cpp
[quietheart@lv-k pre_test]$ ./main 
hello world!

C語言常用宏定義

這裏給出C語言中一些常用的宏定義。

01: 防止一個頭文件被重複包含

#ifndef COMDEF_H
#define COMDEF_H
//頭文件內容
#endif

02: 重新定義一些類型,防止由於各種平臺和編譯器的不同,而產生的類型字節數差異,方便移植。

typedef  unsigned char      boolean;     /* Boolean value type. */
typedef  unsigned long int  uint32;      /* Unsigned 32 bit value */
typedef  unsigned short     uint16;      /* Unsigned 16 bit value */
typedef  unsigned char      uint8;       /* Unsigned 8  bit value */
typedef  signed long int    int32;       /* Signed 32 bit value */
typedef  signed short       int16;       /* Signed 16 bit value */
typedef  signed char        int8;        /* Signed 8  bit value */

//下面的不建議使用
typedef  unsigned char     byte;         /* Unsigned 8  bit value type. */
typedef  unsigned short    word;         /* Unsinged 16 bit value type. */
typedef  unsigned long     dword;        /* Unsigned 32 bit value type. */
typedef  unsigned char     uint1;        /* Unsigned 8  bit value type. */
typedef  unsigned short    uint2;        /* Unsigned 16 bit value type. */
typedef  unsigned long     uint4;        /* Unsigned 32 bit value type. */
typedef  signed char       int1;         /* Signed 8  bit value type. */
typedef  signed short      int2;         /* Signed 16 bit value type. */
typedef  long int          int4;         /* Signed 32 bit value type. */
typedef  signed long       sint31;       /* Signed 32 bit value */
typedef  signed short      sint15;       /* Signed 16 bit value */
typedef  signed char       sint7;        /* Signed 8  bit value */

03: 得到指定地址上的一個字節或字

#define  MEM_B(x) (*((byte *)(x)))
#define  MEM_W(x) (*((word *)(x)))

04: 求最大值和最小值

#define  MAX(x,y) (((x)>(y)) ? (x) : (y))
#define  MIN(x,y) (((x) < (y)) ? (x) : (y))

05: 得到一個field在結構體(struct)中的偏移量

#define FPOS(type,field) ((dword)&((type *)0)->field)

06: 得到一個結構體中field所佔用的字節數

#define FSIZ(type,field) sizeof(((type *)0)->field)

07: 按照LSB格式把兩個字節轉化爲一個Word

#define FLIPW(ray) ((((word)(ray)[0]) * 256) + (ray)[1])

08: 按照LSB格式把一個Word轉化爲兩個字節

#define FLOPW(ray,val) (ray)[0] = ((val)/256); (ray)[1] = ((val) & 0xFF)

09: 得到一個變量的地址(word寬度)

#define B_PTR(var)  ((byte *) (void *) &(var))
#define W_PTR(var)  ((word *) (void *) &(var))

10: 得到一個字的高位和低位字節

#define WORD_LO(xxx)  ((byte) ((word)(xxx) & 255))
#define WORD_HI(xxx)  ((byte) ((word)(xxx) >> 8))

11: 返回一個比X大的最接近的8的倍數

#define RND8(x) ((((x) + 7)/8) * 8)

12: 將一個字母轉換爲大寫

#define UPCASE(c) (((c)>='a' && (c) <= 'z') ? ((c) - 0x20) : (c))

13: 判斷字符是不是10進值的數字

#define  DECCHK(c) ((c)>='0' && (c)<='9')

14: 判斷字符是不是16進值的數字

#define HEXCHK(c) (((c) >= '0' && (c)<='9') ((c)>='A' && (c)<= 'F') \
((c)>='a' && (c)<='f'))

15: 防止溢出的一個方法

#define INC_SAT(val) (val=((val)+1>(val)) ? (val)+1 : (val))

16: 返回數組元素的個數

#define ARR_SIZE(a)  (sizeof((a))/sizeof((a[0])))

17: 返回一個無符號數n尾的值MOD_BY_POWER_OF_TWO(X,n)=X%(2^n)

#define MOD_BY_POWER_OF_TWO( val, mod_by ) ((dword)(val) & (dword)((mod_by)-1))

18: 對於IO空間映射在存儲空間的結構,輸入輸出處理

#define inp(port) (*((volatile byte *)(port)))
#define inpw(port) (*((volatile word *)(port)))
#define inpdw(port) (*((volatile dword *)(port)))
#define outp(port,val) (*((volatile byte *)(port))=((byte)(val)))
#define outpw(port, val) (*((volatile word *)(port))=((word)(val)))
#define outpdw(port, val) (*((volatile dword *)(port))=((dword)(val)))

19: 使用一些宏跟蹤調試

ANSI標準說明了五個預定義的宏名。它們是:

__LINE__
__FILE__
__DATE__
__TIME__
__STDC__

C++中還定義了 __cplusplus

如果編譯器不是標準的,則可能僅支持以上宏名中的幾個,或根本不支持。記住編譯程序也許還提供其它預定義的宏名。

__LINE__ 及 __FILE__ 宏指示,#line指令可以改變它的值,簡單的講,編譯時,它們包含程序的當前行數和文件名。
__DATE__ 宏指令含有形式爲月/日/年的串,表示源文件被翻譯到代碼時的日期。
__TIME__ 宏指令包含程序編譯的時間。時間用字符串表示,其形式爲: 分:秒
__STDC__ 宏指令的意義是編譯時定義的。一般來講,如果__STDC__已經定義,編譯器將僅接受不包含任何非標準擴展的標準C/C++代碼。如果實現是標準的,則宏__STDC__含有十進制常量1。如果它含有任何其它數,則實現是非標準的。
__cplusplus 與標準c++一致的編譯器把它定義爲一個包含至少6爲的數值。與標準c++不一致的編譯器將使用具有5位或更少的數值。

可以定義宏,例如:當定義了 _DEBUG ,輸出數據信息和所在文件所在行

#ifdef _DEBUG
#define DEBUGMSG(msg,date) printf(msg);printf(“%d%d%d”,date,_LINE_,_FILE_)
#else
#define DEBUGMSG(msg,date)
#endif

20:宏定義防止錯誤使用小括號包含。

例如:有問題的定義:

#define DUMP_WRITE(addr,nr) {memcpy(bufp,addr,nr); bufp += nr;}

應該使用的定義:

#difne DO(a,b) do{a+b;a++;}while(0)

例如:

if(addr)
    DUMP_WRITE(addr,nr);
else
    do_somethong_else();

宏展開以後變成這樣:

if(addr)
    {memcpy(bufp,addr,nr); bufp += nr;};
else
    do_something_else();

其它

主要參考:

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