C++預處理詳解

 

    本文在參考ISO/IEC 14882:2003和cppreference.com的C++ Preprocessor的基礎上,對C++預處理作一個全面的總結講解。若是沒有特殊說明,所列內容均依據C++98標準,而非特定平臺相關(如VC++)的,C++11新增的特性會專門指出。html

 

1. 簡介linux

    一般咱們說C++的Build(這裏沒用「編譯」是怕混淆)可分爲4個步驟:預處理、編譯、彙編、連接。預處理就是本文要詳細說的宏替換、頭文件包含等;編譯是指對預處理後的代碼進行語法和語義分析,最終獲得彙編代碼或接近彙編的其餘中間代碼;彙編是指將上一步獲得的彙編或中間代碼轉換爲目標機器的二進制指令,通常是每一個源文件生成一個二進制文件(VS是.obj,GCC是.o);連接是對上一步獲得的多個二進制文件「連接」成可執行文件或庫文件等。ios

    這裏說的「預處理」其實並不很嚴格,在C++標準中對C++的translation分爲9個階段(Phases of translation),其中第4個階段是Preprocessor,而咱們說的一般的「預處理」實際上是指全部這4個階段,下面列出這4個階段(說的不詳細,詳見參考文獻):express

  1. 字符映射(Trigraph replacement:將系統相關的字符映射到C++標準定義的相應字符,但語義不變,如對不一樣操做系統上的不一樣的換行符統一換成規定字符(設爲newline);
  2. 續行符處理(Line splicing:對於「\」緊跟newline的,刪去「\」和newline(咱們在#define等中用的續行在Preprocessor以前就處理了),該過程只進行1遍(若是是「\\」後有兩個換行只會刪去一個「\」);
  3. 字串分割(Tokenization:源代碼做爲一個串被分爲以下串(Token)的鏈接:註釋、whitespace、preprocessing tokens(標示符等這時都是preprocessing tokens,由於此時不知道誰是標示符,通過下一步以後,真正的預處理符會被處理);
  4. 執行Preprocessor:對#include指令作遞歸進行該1-4步,此步驟時候源代碼中再也不含有任何預處理語句(#開頭的哪些)。

    須要強調的是,預處理是在編譯前已經完成的,也就是說編譯時的輸入文件裏已經不含有任何預處理語句了, 這包括,條件編譯的測試不經過部分被刪去宏被替換頭文件被插入等。編程

    另外,預處理是以 translation unit 爲單位進行的,一個 translation unit 就是一個源文件連同由#include包含(或間接包含)的全部文本文件的全體(參見C++標準)。通常的,編譯器對一個 translation unit 生成一個二進制文件(VS是.obj,GCC是.o)。windows

    有了這些知識以後,本文後面對第4步的Preprocessor作詳細介紹。架構

 

2. 通常格式及概覽iphone

    Preprocessor指令通常格式以下:ide

    # preprocessing_instruction [arguments] newline函數

    其中preprocessing_instruction是如下之一:define, undef, include, if, ifdef, ifndef, else, elif, endif, line, error, pragma;arguments是可選的參數,如#include後面的文件名;Preprocessor佔一行,可用「\」緊跟newline續行,但續行不是Preprocessor的專利,且續行在Preprocessor前處理。

    Preprocessor指令有如下幾種:

  1. Null,一個 # 後跟 newline ,不產生任何影響,相似於空語句;
  2. 條件編譯,由 #if, #ifdef, #ifndef, #else, #elif, #endif 定義;
  3. 源文件包含,由 #include 定義;
  4. 宏替換,由 #define, #undef, #, ## 定義;
  5. 重定義行號和文件名,由 #line 定義;
  6. 錯誤信息,由 #error 定義;
  7. 編譯器預留指令,由 #pragma 定義。

    要指出的是,除了以上所列的Preprocessor指令外,其餘指令是不被C++標準支持的,儘管有些編譯器實現了本身的預處理指令。很據「可移植性比效率更重要」的原則,應該儘可能僅適用C++標準的Preprocessor。

    下一節將對以上每一個進行詳細說明,除了 Null 預處理指令。

 

3. 詳細解釋

條件編譯                                                                                                                                                   

條件編譯由 #if, #ifdef, #ifndef 開始,後跟 0-n 個 #elif ,後跟 0-1 個 #else ,後跟 #endif 。#if, #ifdef, #ifndef, #elif 後面接expression,條件編譯的控制邏輯同 if-else if-else 條件語句(每一個沒配對的 else 和上面最近的沒配對 if 配對這條也相似),只不過它是條件的對代碼進行編譯而不是執行。#if, #elif 的expression爲常量表達式,expression非0時測試爲真,expression還能夠含有 defined(Token) 測試,即Token爲宏定義時爲真。#ifdef Token 等價於 #if defined(Token ),#ifndef Token 等價於 #if !defined(Token )。請看例子(摘自cppreference.com):

#include <iostream>
#define ABCD 2
int main()
{
#ifdef ABCD
    std::cout << "1: yes\n";
#else
    std::cout << "1: no\n";
#endif #ifndef ABCD
    std::cout << "2: no1\n";
#elif ABCD == 2
    std::cout << "2: yes\n";
#else
    std::cout << "2: no2\n";
#endif

#if !defined(DCBA) && (ABCD < 2*4-3)
    std::cout << "3: yes\n";
#endif
    std::cin.get();
    return 0;
}

條件編譯被大量用於依賴於系統又須要跨平臺的代碼,這些代碼通常會經過檢測某些宏定義來識別操做系統、處理器架構、編譯器,進而條件編譯不一樣代碼,以和系統兼容。但話又說回來,C++標準的最大價值就是讓全部版本的C++實現都一致,從這個層面上將,除非調用系統功能,不然不該該對系統作出任何假設,除了假設它支持C++標準之外。

源文件包含                                                                                                                                                

文件包含指示將某個文件的內容插入到該#include處,這裏「某個文件」將被遞歸預處理(1-4步,見第1節)。文件包含的3種格式爲:#include<filename>(1)、#include"filename"(2)、#include pp-tokens(3),其中第1種方式在標準包含目錄查找filename(通常C++標準庫頭文件在此),第二種方式先查找被處理源文件所在目錄,若是沒找到再找標準包含目錄,第3中方式的pp-tokens須是定義爲<filename>或"filename"的宏,不然結果未知。注意filename能夠是任何文本文件,而沒必要是.h、.hpp等後綴文件,例如能夠是.c或.cpp文本文件(因此標題是「源文件包含」而非「頭文件包含」)。例子:

// file: b.cpp
#ifndef _B_CPP_
#define _B_CPP_

int b = 999;

#endif // #ifndef _B_CPP_
// file: a.cpp
#include <iostream>  // 在標準包含目錄查找
#include "b.cpp"     // 在該源文件所在目錄查找,找不到再到標準包含目錄查找
#define CMATH <cmath> #include CMATH
int main()
{
    std::cout << b << '\n';
    std::cout << std::log10(10.0) << '\n';
    std::cin.get();
    return 0;
}

注意上面例子,將a.cpp和b.cpp放在同一文件夾,只編譯a.cpp。

宏替換                                                                                                                                                       

#define 定義宏替換,#define 以後的宏都將被替換爲宏的定義,直到用 #undef 解除該宏的定義。宏定義分爲不帶參數的常量宏(Object-like macros)和帶參數的函數宏(Function-like macros)。其格式以下:

  • #define identifier replacement-list                             (1)
  • #define identifier( parameters ) replacement-list         (2)
  • #define identifier( parameters, ... ) replacement-list    (3) (since C++11)
  • #define identifier( ... ) replacement-list                      (4) (since C++11)
  • #undef identifier                                                     (5)

對於有參數的函數宏,在replacement-list中,「#」置於identifier面前表示將identifier變成字符串字面值,「##」鏈接,下面的例子來自cppreference.com

#include <iostream>

//make function factory and use it
#define FUNCTION(name, a) int fun_##name() { return a;}

FUNCTION(abcd, 12);
FUNCTION(fff, 2);
FUNCTION(kkk, 23);

#undef FUNCTION
#define FUNCTION 34
#define OUTPUT(a) std::cout << #a << '\n'

int main()
{
    std::cout << "abcd: " << fun_abcd() << '\n';
    std::cout << "fff: " << fun_fff() << '\n';
    std::cout << "kkk: " << fun_kkk() << '\n';
    std::cout << FUNCTION << '\n';
    OUTPUT(million);               //note the lack of quotes
    std::cin.get();
    return 0;
}

可變參數宏是C++11新增部分(來自C99),使用時用__VA_ARGS__指代參數「...」,一個摘自C++標準2011的例子以下(標準舉的例子就是不同啊):

#define debug(...) fprintf(stderr, __VA_ARGS__)
#define showlist(...) puts(#__VA_ARGS__)
#define report(test, ...) ((test) ? puts(#test) : printf(__VA_ARGS__))
debug("Flag");
debug("X = %d\n", x);
showlist(The first, second, and third items.);
report(x>y, "x is %d but y is %d", x, y);

這段代碼在預處理後產生以下代碼:

fprintf(stderr, "Flag");
fprintf(stderr, "X = %d\n", x);
puts("The first, second, and third items.");
((x>y) ? puts("x>y") : printf("x is %d but y is %d", x, y));

在上面條件編譯就講到,有時用 #ifdef macro_NAME 來識別一些信息,C++標準指定了一些預約義宏,列在下表中(C++11新增宏已標出):

Predefined macros

Meaning

Remark

__cplusplus

在C++98中定義爲199711L,C++11中定義爲201103L

 

__LINE__

指示所在的源代碼行數(從1開始),十進制常數

 

__FILE__

指示源文件名,字符串字面值

 

__DATE__

處理時的日期,字符串字面值,格式「Mmm dd yyyy」

 

__TIME__

處理時的時刻,字符串字面值,格式「hh:mm:ss」

 

__STDC__

指示是否符合Standard C,可能不被定義

wikipedia條目

__STDC_HOSTED__

如果Hosted Implementation,定義爲1,不然爲0

C++11

__STDC_MB_MIGHT_NEQ_WC__

見ISO/IEC 14882:2011

C++11

__STDC_VERSION__

見ISO/IEC 14882:2011

C++11

__STDC_ISO_10646__

見ISO/IEC 14882:2011

C++11

__STDCPP_STRICT_POINTER_SAFETY__

見ISO/IEC 14882:2011

C++11

__STDCPP_THREADS__

見ISO/IEC 14882:2011

C++11

其中上面5個宏必定會被定義,下面從__STDC__開始的宏不必定被定義,這些預約義宏不能被 #undef。使用這些宏的一個例子以下(連續字符串字面值會被自動相連,「ab」「cde」 等價於 「abcde」):

 1 #include <iostream>
 2 int main()
 3 {
 4 #define PRINT(arg) std::cout << #arg": " << arg << '\n'
 5     PRINT(__cplusplus);
 6     PRINT(__LINE__);
 7     PRINT(__FILE__);
 8     PRINT(__DATE__);
 9     PRINT(__TIME__);
10 #ifdef __STDC__
11     PRINT(__STDC__);
12 #endif
13     std::cin.get();
14     return 0;
15 }

這些宏常常用於輸出調試信息。預約義宏通常以「__」做爲前綴,因此用戶自定義宏應該避開「__」開頭。

應當指出的是,現代的C++程序設計原則不推薦適用宏定義常量或函數宏,應該儘可能少的使用 #define ,若是可能,用 const 變量或 inline 函數代替。

重定義行號和文件名                                                                                                                                  

從 #line number ["filename"] 的下一行源代碼開始, __LINE__ 被重定義爲從 number 開始,__FILE__ 被重定義"filename"(可選),一個例子以下:

 1 #include <iostream>
 2 int main()
 3 {
 4 #define PRINT(arg) std::cout << #arg": " << arg << '\n'
 5 #line 999 "WO"
 6 
 7     PRINT(__LINE__);
 8     PRINT(__FILE__);
 9     std::cin.get();
10     return 0;
11 }

錯誤信息                                                                                                                                                    

#error [message] 指示編譯器報告錯誤,通常用於系統相關代碼,例如檢測操做系統類型,用條件編譯裏 #error 報告錯誤。例子以下:

int main()
{
#error "w"
    return 0;
#error
}

第2個 #error 可能不被執行,由於編譯器可能在遇到一個 #error "w" 時就報錯中止了。

編譯器預留指令                                                                                                                                         

#pragma 預處理指令是C++標準給特定C++實現預留的標準,因此,在不一樣的編譯器上 #pragma 的參數及意義可能不一樣,例如 VC++2010 提供 #pragma once 來指示源文件只被處理一遍。OpenMP做爲一個共享內存並行編程模型,使用 #pragma omp 指導語句,詳見:OpenMP共享內存並行編程詳解

VC++的 #pragma 指令參見MSDN相關條目

GCC的 #pragma 指令參見GCC文檔相關條目

 

4. 預處理的典型應用

    預處理的常見使用有:

  1. Include guard,見wikipedia條目,該技術用來保證頭文件僅被同一文件包含一次(準確地說,頭文件內容在一個 translation unit 中僅出現一次),以防止違反C++的「一次定義」原則;
  2. 用 #ifdef 和特殊宏識別操做系統、處理器架構、編譯器,條件編譯,進而實現針對特定平臺的功能,多用於可移植性代碼;
  3. 定義函數宏,以簡化代碼,或是方便修改某些配置;
  4. 用 #pragma 設定和實現相關的配置(見上一節最後給出的連接)。

    sourceforge.net上有一個項目,是關於用宏檢測操做系統處理器架構編譯器(請點連接或見參考文獻)。下面是一個例子(來自這裏):

#ifdef _WIN64
   //define something for Windows (64-bit)
#elif _WIN32
   //define something for Windows (32-bit)
#elif __APPLE__
    #include "TargetConditionals.h"
    #if TARGET_OS_IPHONE && TARGET_IPHONE_SIMULATOR
        // define something for simulator   
    #elif TARGET_OS_IPHONE
        // define something for iphone  
    #else
        #define TARGET_OS_OSX 1
        // define something for OSX
    #endif
#elif __linux
    // linux
#elif __unix // all unices not caught above
    // Unix
#elif __posix
    // POSIX
#endif

 

參考文獻

http://en.wikipedia.org/wiki/C_preprocessor

ISO/IEC 14882:2003 2.1[lex.phases] 和 16[cpp]

ISO/IEC 14882:2011 16.3[cpp.replace] 和 16.8[cpp.predefined]

cppreference.com 關於 C++ Preprocessor

Microsoft MSDN 關於 C++ Preprocessor

GCC在線文檔 關於 C++ preprocessor

http://sourceforge.net/p/predef/wiki/Home/

相關文章
相關標籤/搜索