程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++預處理詳解

C++預處理詳解

編輯:C++入門知識

  本文在參考ISO/IEC 14882:2003和cppreference.com的C++ Preprocessor的基礎上,對C++預處理做一個全面的總結講解。如果沒有特殊說明,所列內容均依據C++98標准,而非特定平台相關(如VC++)的,C++11新增的特性會專門指出。       1. 簡介       通常我們說C++的Build(這裡沒用“編譯”是怕混淆)可分為4個步驟:預處理、編譯、匯編、鏈接。預處理就是本文要詳細說的宏替換、頭文件包含等;編譯是指對預處理後的代碼進行語法和語義分析,最終得到匯編代碼或接近匯編的其他中間代碼;匯編是指將上一步得到的匯編或中間代碼轉換為目標機器的二進制指令,一般是每個源文件生成一個二進制文件(VS是.obj,GCC是.o);鏈接是對上一步得到的多個二進制文件“鏈接”成可執行文件或庫文件等。       這裡說的“預處理”其實並不很嚴格,在C++標准中對C++的translation分為9個階段(Phases of translation),其中第4個階段是Preprocessor,而我們說的通常的“預處理”其實是指所有這4個階段,下面列出這4個階段(說的不詳細,詳見參考文獻):   字符映射(Trigraph replacement):將系統相關的字符映射到C++標准定義的相應字符,但語義不變,如對不同操作系統上的不同的換行符統一換成規定字符(設為newline); 續行符處理(Line splicing):對於“\”緊跟newline的,刪去“\”和newline(我們在#define等中用的續行在Preprocessor之前就處理了),該過程只進行1遍(如果是“\\”後有兩個換行只會刪去一個“\”); 字串分割(Tokenization):源代碼作為一個串被分為如下串(Token)的連接:注釋、whitespace、preprocessing tokens(標示符等這時都是preprocessing tokens,因為此時不知道誰是標示符,經過下一步之後,真正的預處理符會被處理); 執行Preprocessor:對#include指令做遞歸進行該1-4步,此步驟時候源代碼中不再含有任何預處理語句(#開頭的哪些)。     需要強調的是,預處理是在編譯前已經完成的,也就是說編譯時的輸入文件裡已經不含有任何預處理語句了, 這包括,條件編譯的測試不通過部分被刪去、宏被替換、頭文件被插入等。       有了這些知識之後,本文後面對第4步的Preprocessor做詳細介紹。       2. 一般格式及概覽       Preprocessor指令一般格式如下:       # 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指令有以下幾種:   Null,一個 # 後跟 newline ,不產生任何影響,類似於空語句; 條件編譯,由 #if, #ifdef, #ifndef, #else, #elif, #endif 定義; 源文件包含,由 #include 定義; 宏替換,由 #define, #undef, #, ## 定義; 重定義行號和文件名,由 #line 定義; 錯誤信息,由 #error 定義; 編譯器預留指令,由 #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. 預處理的典型應用       預處理的常見使用有:   Include guard,見wikipedia條目,該技術用來保證頭文件僅被包含一次,以防止違反C++的“一次定義”原則; 用 #ifdef 和特殊宏識別操作系統、處理器架構、編譯器,條件編譯,進而實現針對特定平台的功能,多用於可移植性代碼; 定義函數宏,以簡化代碼,或是方便修改某些配置; 用 #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 復制代碼

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved