現代C++中的預處理宏
--徐東來
摘要:在C++從C繼承的遺產中,預處理宏是其中的一部分。在現代C++的發展過程中,預處理宏是否還有意義?本文將討論之。
關鍵字:預處理 宏 #define #pragma
C++中有那么多靈活的特性,例如重載、類型安全的模板、const關鍵字等等,為什么程序員還要寫“#define”這樣的預處理指令?
典型的一個例子,大家都知道“const int a=100;”就比“#define a 100”要好,因為const提供類型安全、避免了預處理的意外修改等。
然而,還是有一些理由讓我們去使用#define。
一、使用預處理宏
1) 守護頭文件
為了防止頭文件被多次包含,這是一種常用技巧。
#ifndef MYPROG_X_H
#define MYPROG_X_H
// … 頭文件x.h的其余部分
#endif
2) 使用預處理特性
在調試代碼中,插入行號或編譯時間這類信息通常很有用,可以使用預定義的標準宏,例如__FILE__、__LINE__、__DATE__和__TIME__。
3) 編譯時期選擇代碼
A. 調試代碼
選擇性的輸出一些調試信息:
void f()
{
#ifdef _DEBUG
cerr<<”調試信息”<<endl;
#endif
// .. f()的其他部分
}
通常我們也可以用條件判斷來代替:
void f()
{
if(_DEBUG)
{
cerr<<”調試信息”<<endl;
}
// .. f()的其他部分
}
B. 特定平臺代碼
同一函數同一功能在不同的編譯平臺上可能有不同的表現形式,我們可以通過定義宏來區分不同的平臺。
C. 不同的數據表示方式
<<深入淺出MFC>>這本書對MFC框架中宏的使用解析的很透徹,也讓我們領略到宏的強大功能。可以參看DECLARE_MESSAGE_MAP(),
BEGIN_MESSAGE_MAP, END_MESSAGE_MAP的實現。
4) #pragma的使用,例如用#pragma禁止掉無傷大雅的警告,用于可移植性的條件編譯中。例如,
包含winsock2 lib文件:
#pragma comment(lib,”ws2_32”)
用如下預處理宏,可以使結構按1字結對齊:
#pragma pack(push)
#pragma pack(1)
// … 結構定義
#pragma pack(pop)
禁止掉某些警告信息:
#pragma warning( push )
#pragma warning( disable : 4705 )
#pragma warning( disable : 4706 )
#pragma warning( error : 164 )// 把164號警告作為錯誤報出
// Some code
#pragma warning( pop )
二、宏的常見陷阱
下面示范如何寫一個簡單的預處理宏max();這個宏有兩個參數,比較并返回其中較大的一個值。在寫這樣一個宏時,容易犯哪些錯誤?有四大易犯錯誤。
1) 不要忘記為參數加上括號
// 例1:括號陷阱一:參數
//
#define max(a, b) a < b ? b : a
例如:
max(i += 2, j)
展開后:
i += 2 < j ? j : i += 2
考慮運算符優先級和語言規則,實際上是:
i += ((2 < j) ? j : i += 2)
這種錯誤可能需要長時間的調試才可以發現。
2) 不要忘記為整個展開式加上括號
// 例2:括號陷阱二:展開式
//
#define max(a, b) (a) < (b) ? (b) : (a)
例如:
m = max(j, k) + 42;
展開后為:
m = (j) < (k) ? (j) : (k) + 42;
考慮運算符優先級和語言規則,實際上是:
m = ((j) < (k)) ? (j) : ((k) + 42);
如果j >= k, m被賦值k+42,正確;如果j < k, m被賦值j,是錯誤的。如果給展開式加上括號,就解決了這個問題。
3) 當心多參數運算
// 例3:多參數運算
//
#define max(a, b) ((a) < (b) ? (b) : (a))
max(++j, k);
如果++j的結果大于k,j會遞增兩次,這可能不是程序員想要的:
((++j) < (k) ? (k) : (++j))
類似的:
max(f(), pi)
展開后:
((f()) < (pi) ? (pi) : (f()))
如果f()的結果大于等于pi,f()會執行兩次,這絕對缺乏效率,而且可能是錯誤的。
4) 名字沖突
宏只是執行文本替換,而不管文本在哪兒,這意味著只要使用宏,就要小心對這些宏命名。具體來說,這個max宏最大的問題是,極有可能會和標準的max()函數模板沖突:
// 例4:名字沖突
//
#define max(a,b) ((a) < (b) ? (b) : (a))
#include <algorithm> // 沖突!
在<algorithm>中,有如下:
template<typename T> const T&
max(const T& a, const T& b);
宏將它替換為如下,將無法編譯:
template<typename T> const T&
((const T& a) < (const T& b) ? (const T& b) : (const T& a));
所以,我們盡量避免命名的沖突,想出一個不平常的,難以拼寫的名字,這樣才能最大可能地避免與其他名字空間沖突。
宏的其他缺陷:
5) 宏不能遞歸
容易理解。
6) 宏沒有地址
你可能得到任何自由函數或成員函數的指針,但不可能得到一個宏的指針,因為宏沒有地址。宏之所以沒有地址,原因很顯然===宏不是代碼,宏不會以自身的形勢存在,因為它是一種被美化了的文本替換規則。
7) 宏有礙調試
在編譯器看到代碼之前,宏就會修改相應的代碼,因而,他會嚴重改變變量名稱和其他名稱;此外,在調試階段,無法跟蹤到宏的內部。