程式扎記: [C++ 文章收集] C++ 前置處理器

標籤

2012年2月10日 星期五

[C++ 文章收集] C++ 前置處理器

前言 : 
早期的C 編譯器並沒有常數或inl ine 函式。隨著C 的快速發展,人們很快發現,C 需要處理常數、巨集及含括(include)檔。解決的方式是建立一個前置處理器(preprocessor),在將程式送到C 編譯器之前先予以處理。前置處理器算是一種特殊的文字編輯器。它的語法和C 完全不同,它也不需要了解C 語言的架構。它只是一種原始的文字編輯器. 

#define 敘述 : 
#define 敘述可用來定義常數。例如,下面兩行功能類似 : 
  1. #define SIZE 20 // 陣列大小是20  
  2. const int SIZE = 20// 陣列大小是20  
實際上,#def ine SIZE 20 的動作,就是要求前置處理器將每個SIZE 變成20,這簡化了許多額外的處理程序. 前置處理器的前面都會加上“#”符號。C++ 是一種格式自由的語言,語言的元素可放在一行的任何位置,行的結束符號會視為空白。但前置處理器並不是自由格式,你必須將“#”符號放在每一行的前面。後面會談到,前置處理器並不了解 C++ 的語法. 
前置處理器是以行為單位,C++ 的敘述是用分號(;)來結束. 但前置處理器並不是用分號來結束,加入分號的話,會造成奇怪的結果. 前置處理器的指示可在行的尾端放入倒斜線(\)來連接下一行. 一般來說,你可以定義一個取代的巨集,例如 : 
  1. #define FOO bar  
前置處理器會將程式中的“F OO ”這個字用“bar”取代。在實務上,一般會用大寫英文字母當作巨集名稱。如此才容易分辨是一個變數(全部小寫)或是一個巨集(全部大寫). 一般define 敘述的用法如下 : 
#define Name Substitute-Text

Name 可以是任何合法的變數名稱,Substitute-Text 可以是任何內容,只要一行中可以放得下。Subs tit ute-Text 可包含空白字元、運算子和其它字元. 也可以使用下面的定義 : 
#define FOR_ALL for (i = 0; i < ARRAY_SIZE; ++i)

用法如下 : 
  1. /* 
  2. * 清除陣列的內容 
  3. */  
  4. FOR_ALL {  
  5. data[i] = 0;  
  6. }  
這種定義巨集的方式並不理想,反而使得程式的控制流程糾纏不清。以這個例子來說,如果程式設計師想明白迴圈的功能, 他必須先找到程式開始的位置,看看 
FOR_ALL 的定義. 若是濫用這種取代的做法來代替基本的程式架構,會造成反效果。例如,你可以定義 : 
  1. #define BEGIN {  
  2. #define END }  
  3. . . .  
  4. if (index == 0)  
  5. BEGIN  
  6. cout << "Starting\n";  
  7. END  
你根本不是在寫C++ 程式,而是C++ 與PA SCAL 的混合體! 前置處理器會導致難以預測的問題,因為它並沒有檢查C++ 的語法.例如,範例10-1 會在第11 行發生錯誤 : 
- 範例10-1. big/big.cc
  1. #define BIG_NUMBER 10 ** 10  
  2.   
  3. main()  
  4. {  
  5. // index for our calculations  
  6. int index;  
  7.   
  8. index = 0;  
  9.   
  10. // 下一行有語法錯誤  
  11. while (index < BIG_NUMBER) {  
  12.   index = index * 8;  
  13. }  
  14. return (0);  
  15. }  

問題是在第1 行的#def ine 敘述,但錯誤訊息會指出第11 行的位置。第1 行的定義會使前置處理器將第11 行展開為 : 
  1. while (index < 10 ** 10)  
因為** 是不合法的運算子,所以會導致語法錯誤. 接著下面程式的執行結果是47,而不是預計的答案144,怎麼搞的? 
- 範例10-2. f i rst /f i rst .cc
  1. #include   
  2. #define FIRST_PART 7  
  3. #define LAST_PART 5  
  4. #define ALL_PARTS FIRST_PART + LAST_PART  
  5. main() {  
  6.   cout << "The square of all the parts is " <<  
  7.   ALL_PARTS * ALL_PARTS << '\n';  
  8.   return (0);  
  9. }  

事實上上面的代碼經過前置處理後換變成下面代碼 : 
  1. main() {  
  2.   cout << "The square of all the parts is " <<  
  3.   7 + 5 * 7 + 5 << '\n';   // 而不是預期的 12 * 12  
  4.   return (0);  
  5. }  
# define 與const : 
在cons t 之前,#def i ne 是定義常數的唯一方式,因此,早期的程式碼都是使用#define。然而,const 比 #def ine 更理想。首先,C++ 會立即檢查const 敘述的語 
法 ; 而#define 指示一直到使用巨集時才會檢查。const 是採用C++ 語法,#define 則有自己的語法;最後,const 適用C++ 變數的有效範圍法則,而用#define 所定義的常數會延伸到整個程式. 在大多數情況下,const 比#define 理想。底下有兩種個方式來定義同樣的常數 : 
  1. #define MAX 10 // 使用前置處理器定義一個值  
  2. // 這很容易出問題  
  3. const int MAX = 10// 定義一個C++ 的常數整數  
  4. // ( 比較安全)  
#define 只能定義簡單的常數. cons t 可定義幾乎每一種C++ 常數的型態,包含結構類別. 例如 : 
  1. struct box {  
  2. int width, height; // 方塊的大小,單位為點  
  3. };  
  4. const box pink_box(1.04.5); // 輸入粉紅色方塊的大小  
另外 #define 在條件式編譯和其它特定應用上十分有用. 

條件式編譯 : 
程式設計師往往必須面對跨平台的問題,就是讓程式在各種機器上執行。理論上,C++ 的可攜性很高;但許多機器具有特殊的架構, 例如 UNIX、MSDOS 及Windows 編譯器,表面上十分類似,但仍有差異存在. 前置處理器允許你透過條件式編譯(condi tional compilat ion),賦予你設計上的彈性. 假設你要在測試時將除錯用的程式碼放進來,然後在正式發行時予以刪除,就可以使用#ifdef-#endif 的方式 : 
  1. #ifdef DEBUG  
  2.     cout << "In compute_hash, value " << value << " hash " << hash << "\n";  
  3. #endif /* DEBUG */  
程式開頭如果包含 : 
  1. #define DEBUG /* Turn debugging on */  
就會包含cout 這一行. 若程式含有下面的指示 : 
  1. #undef DEBUG /* Turn debugging off */  
就會跳過cout. 嚴格來說,#undef DEBUG 不是必要的. 若是沒有#define DEBUG 敘述,DEBUG 就是未定義的狀態。#undef DEBUG 可以明確指出 : DEBUG 是用來進行條件式編譯,目前是屬於關閉狀態. 如果不定義該符號,#ifndef 指示會編譯這段程式碼. 另外 #else 表示條件式的反向. 例如 : 
  1. #ifdef DEBUG  
  2.     cout << "Test version. Debugging is on\n";  
  3. #else /* DEBUG */  
  4.     cout << "Production version\n";  
  5. #endif /* DEBUG */  
編譯器的設定選項-Dsymbol 允許你定義符號,例如下面的命令 : 
CC -DDEBUG -g -o prog prog.cc

雖然程式中沒有#define DEBUG,但是編譯prog.c 程式會包含#ifdef DEBUG 到 #endif /* DEBUG */ 之間的程式. 一般形式的選項是-Dsymbol 或 -Dsymbol=value. 例如,下面會設定MAX 為10 : 
CC -DMAX=10 -o prog prog.c

大部份的C++ 編譯程式會自動定義一些與系統相關的符號. 像是Turbo C++ 定義 __TURBOC__,MS-DOS 則定義__MSDOS__ . ANSI 標準的C 編譯器定義了符號__STDC__. C++ 編譯器定義了__cplusplus . 大多數的UNIX 編譯器都定義了系統的名稱(如Sun、VAX、celer i ty 等等),但很少有文件提到這一點。unix 這個符 
號,在所有的UNIX 機器上都有定義. 

#include 檔 : 
#include 指示允許程式使用另一個程式的原始碼. 例如,程式中使用了這個指示 : 
它會要求前置處理器取出ios tream.h 這個檔案,並且放入目前的程式中. 可被其他檔案包含的檔案,稱為標頭檔(header file),大部份的#include 指示出現在程式頂端. 在UNIX 中,這些檔案位於/usr/include. 
標準包含檔(incl ude f i le)是用來定義程式庫中函式的資料結構和巨集. 例如,cout 是一個標準類別,可將資料印到到標準輸出裝置. cout 所用ostream 的類別定義和相關程序,都定義在iost ream.h. 另外你可用雙引號括住檔案名稱,來指定含括檔 : 
  1. #include "defs.h"  
檔名(“defs.h”)可為任何合法的檔案名稱,可以是一個簡單的檔案“defs.h”;相對的路徑“. . / . . /dat a.h”;或絕對路徑“/ root /include/const .h”.(在DOS/Windows環境中,必須使用倒斜線(\)而不是斜線(/)來做為目錄的分隔符號.)包含檔可為巢狀方式,但這可能很容易會造成問題。假設你在const .h 檔案中定義多個常數,若是檔案dat a.h 及io.h 兩者都含括const.h,然後你的程式中加入了 : 
  1. #include "data.h"  
  2. #include "io.h"  
由於前置處理器設定cons t.h 中的定義兩次,所以會發生錯誤。定義常數兩次並非嚴重錯誤(fatal error);然而定義一個資料結構或是union 兩次,則會導致嚴重錯誤,必須極力避免. 解決的方法之一是檢查const .h,看看是否它已經被包含進來,並且沒有重覆定義的符號. 請看下面這段程式碼 : 
  1. #ifndef _CONST_H_INCLUDED_  
  2. /* 定義常數*/  
  3. #define _CONST_H_INCLUDED_  
  4. #endif /* _CONST_H_INCLUDED_ */  
當const .h 被包含進來,它定義了_CONST_H_IN CL UDED_. 若是這個符號已經被定義了,#ifndef 的條件式會隱藏其他的定義,以避免問題發生! 

參數化的巨集 : 
目前為止,討論的只有簡單的#def ine 和巨集. 巨集也可以接受參數,下面的巨集是計算一個數字的平方 : 
  1. #define SQR(x) ((x) * (x)) // 數值的平方計算  
巨集會用引數的字串來取代x,SQR(5) 會展開成為((5)*(5)) .最好在巨集的參數旁邊加上括號,範例10-7 說明了不用這個方式所產生的問題 : 
- 範例10-7 : sqr/sqr.cc
  1. #include   
  2. #define SQR(x) (x * x)  
  3. main()  
  4. {  
  5.     int counter; // 迴圈的計數器  
  6.     for (counter = 0; counter < 5; ++counter) {  
  7.         cout << "x " << counter+1 <<  
  8.         " x squared " << SQR(counter+1) << '\n';  
  9.     }  
  10.     return (0);  
  11. }  

上面程式的輸出結果是什麼?(請在你的機器上測試)為什麼會是這個結果? 

# 運算子 : 
# 運算子用在參數化的巨集中,可以將引數變成字串. 例如 : 
  1. #define STR(data) #data  
  2. STR(hello)  
會產生 : 
"hello"

參數化的巨集與inline 函式 : 
大部份的情形下,最好使用inl ine 函式而不要用參數化的巨集. 要避免參數化巨集所造成的問題,最好是透過inline 函式. 例如,SQR 巨集可同時處理f loat 及int 兩種資料型態,但你必須寫兩個inline 函式,才能達到同樣的功能 : 
  1. #define SQR(x) ((x) * (x)) // 參數化的巨集  
  2. // 可以處理,但是會有問題  
  3. // 相同作用的inline 函式  
  4. inline int sqr(const int x) {  
  5.     return (x * x);  
  6. }  
高階功能 : 
本書沒有包含C++ 前置處理器指示的完整列表. 它包括#if 某些進階的特性,可以處理條件式的編譯 ; 以及#pragma 指示,將特定編譯器的命令放進檔案裡. 關於這些功能的詳細說明,請參考C++ 的技術文件. 

補充說明 : 
[C++ 小學堂] pragma comment 的使用 
[C++ 小學堂] 關鍵字 inline 介紹 
[C++ 文章收集] C++中 #define的用法

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!