本文源自:http://www.shnenglu.com/tiandejian/archive/2007/04/04/ECPP_02.html
第2項: 盡量使用 const 、 enum 、 inline ,避免使用 #define
這一項似乎叫做“盡量把工作交給編譯器而不是預編譯器”更恰當,因為 #define 的內容不應該屬于語言自身的范疇。這是 #define 的眾多問題之一,請看下面的代碼:
#define ASPECT_RATIO 1.653
編譯器也許根本就接觸不到這個符號名 ASPECT_RATIO ,它在編譯器對源代碼進行編譯以前也許就被預處理器替換掉了。于是, ASPECT_RATIO 這一名字很可能不會列在符號表中。如果你在代碼中使用了這常量,也許就會遇到一些很不容易察覺的錯誤,而往往你又很難找到問題所在,因為出錯信息只會涉及 1.653 ,而對 ASPECT_RATIO 則只字不提。如果 ASPECT_RATIO 是在某個你不知情的頭文件中定義的,那么尋找 1.653 的出處對于你來說就是大海撈針了,你將在跟蹤這一數值上浪費很多時間。在符號調試器中同樣的問題也會出現,原因和上述問題是一致的:你在編程時使用的名字可能沒有加入符號表中。
解決的辦法是:使用常量來代替宏定義:
const double AspectRatio = 1.653; // 宏的名字通常使用大寫字母,
// 于是常量名這樣定義
作為語言層面的常量, AspectRatio 最終會被編譯器所看到,并且會確保進入符號表中。另外,對于浮點數而言,使用常量較 #define 而言會生成更小的目標代碼。這是由于預處理器會對目標代碼中出現的所有宏 ASPECT_RATIO 復制出一份 1.653 ,然而使用常量 AspectRatio 時永遠不會多于一份。
在使用常量代替 #define 時有兩個特殊情況值得注意。第一個是要把指針定義為常量。因為常量定義一般都放在頭文件中(許多不同的源碼文件會包含這些頭文件),要將指針定義為 const 的,這一點很重要,通常情況下也要將指針所指的內容定義為 const 。比如說,在一個頭文件中定義一個 char* 的字符常量時,你需要寫兩次 const :
const char * const authorName = "Scott Meyers";
在 第 3 項 中我將向您全面介紹 const 的含義和用法,尤其是在其與指針混合使用時的一些細節問題。但是,在這里提醒你使用 string 對象要比使用其祖先“ char* ”好得多,知道這一點是很有意義的。上述的 autherName 最好以這樣的形式定義:
const std::string authorName("Scott Meyers");
第二個特殊情況關系到類內部的常量。為了將常量的作用域限制在一個類里,你必須將這個常量作為類的成員;為了限制常量份數不超過一份,你必須將其聲明為 static 成員:
class GamePlayer {
private:
static const int NumTurns = 5; // 常量聲明
int scores[NumTurns]; // 該常量的用法
...
};
上面你所看到的是 NumTurns 的聲明,而不是定義。通常情況下, C++ 要求你為所有要用到的所有東西做出定義,但是這里有一個例外:類內部的靜態常量如果是整型(比如整數、字符型、布爾型)則不需要定義。只要你不需要得到它們的地址,你可以只聲明它們而不提供定義。如果你需要得到類常量的地址;或者即使你不需要這一地址,而你的編譯器錯誤地堅持你必須為這個常量做出定義,這兩種情況下你應該以下面的形式提供其定義:
const int GamePlayer::NumTurns; // NumTurns 的定義,
// 下邊會告訴你為什么不為其賦值。
你應該把這段代碼放在一個實現文件中。這是因為類常量的初始值已經在其聲明的時候給出了(比如說, NumTurns 在聲明時就被初始化為 5 ),而在定義的時候不允許為其賦初值。
順便要注意一下,你不可能使用 #define 來創建一個類內部的靜態常量,這是因為 #define 不關心域的問題。一旦定義了一個宏,在編譯時它將影響到所有其它代碼(除非你在某處使用 #undef 取消了這個宏的定義)。這不僅意味著 #define 不能用來定義類內部的常量,同時還說明它無法給你帶來任何封裝效果,也就是說,“私有的” #define 這類東西是不存在的。然而 const 數據成員可以得到封裝, NumTurns 就是一個例子。
早期的編譯器可能不會接受上面代碼的語法,這是因為那時候在聲明一個靜態的類成員時為其賦初值是非法的。與此同時,只有整型數據才可以在類內部進行初始化,并且只有常量才能得到初始化。在這種情況下不能使用上述的語法,你可以在定義的時候為其賦初值:
class CostEstimate {
private:
static const double FudgeFactor; // 靜態類常量的聲明
... // 應在頭文件中進行
};
const double // 靜態類常量的定義
CostEstimate::FudgeFactor = 1.35; // 應在實現文件中進行
上面幾乎是你所要了解的全部內容了。但是在某時刻還有可能會發生一個小的意外:當你在編譯一個類的時候,你可能需要這個類內部的一個常量的值,比如說前述的 GamePlayer::scores 數組的聲明(編譯器可能會堅持在編譯時了解數組的大小)。編譯器在這時違背了為類內部的靜態的整型常量賦初值的規范,那么有什么辦法補救呢?你可以使用“ enum 黑客手段”(這是愛稱,不帶有蔑視色彩)。這一技術利用了這一事實:枚舉類型數據都是 int 型的,所以 GamePlayer 也可以這樣定義:
class GamePlayer {
private:
enum { NumTurns = 5 }; // “ enum 黑客”
// 使 NumTurns 成為一個符號名,其值為 5
int scores[NumTurns]; // 可以正常工作
...
};
從許多角度講,了解 enum 黑客手段是很有好處的。首先, enum 黑客的行為更像一個 #define 而不是 const ,在某些情況下這更符合你的要求。比如說,你可以合法地取得一個 const 的地址,但是取 enum 的地址則是非法的,而去取 #define 的地址同樣不合法。如果不想讓其他人得到你的整形常量的指針或引用,那么使用枚舉類型便是強制實施這一約束的一個很好的方法。(參見第 18 項,那里介紹了使用編碼手段強制實現設計約束的更多信息。)粗心大意的編譯器也許會為這類對象分配多余的內存,但你也一定不會情愿。與 #define 類似, enum 不會帶來不必要的內存開銷。
了解 enum 黑客的第二個用處純粹是實用主義的。許多代碼都在這樣做,所以你看到它時必須要認得。事實上, enum 黑客是模板元編程的一個基本技術。(參見第 48 項)
回到預處理器的問題, #defined 的 另一個用法(這樣做很不好,但這非常普遍)就是將宏定義得和函數一樣,但不會帶來函數調用的開銷。下面例子中的宏定義使用 a 和 b 中 更大的參數調用了一個名為 f 的 函數:
// 使用 a 和 b 中 更大的一個調用函數
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
這樣的宏會帶來數不清的缺點,想起來就讓人頭疼。
無論什么時候,只要你寫下了這樣的宏,你必須為宏內部所有的參數加上括號。否則,其他人在某些語句中調用這個宏的時候總會遇到麻煩。即使你做了正確的定義,古怪的事情也會發生:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a 自加兩次
CALL_WITH_MAX(++a, b+10); // a 自加一次
在這里,調用 f 以前 a 自加的次數竟取決于它和誰進行比較!
幸運的是,你不需要把精力放在這些毫無意義的事情上。你可以使用內聯函數的模板,此時你可以得到宏的高效,并且一切都是可預知的,類型安全的:
template<typename T>
inline void callWithMax(const T& a, const T& b)
// 因為我們不知道 T 的類型是什么,因此我們通過引用傳遞 const 參數。參見第 20 項
{
f(a > b ? a : b);
}
這一模板創建了一族函數,其中每一個函數都會得到同一類型的兩個對象,使用其中較大的一個來調用函數 f 。可以看到,在函數內部不需要為參數加括號,不需要擔心參數會被多次操作。與此同時,由于 callWithMax 是一個真實的函數,它遵循作用域和訪問權的規則,比如類可以擁有私有的內聯函數。然而宏在這些問題上就望塵莫及了。
C++ 為你提供了 const 、 enum 、 inline 這些新特征,預處理器(尤其是 #define )的作用就越來越小了,但是這并不是說可以完全拋棄它。 #include 仍是程序中的主角, #ifdef/#ifndef 在控制編譯過程還有著舉足輕重的地位。說“預處理器該退休了”還為時過早,但是你還是要經常給它放放長假。
需要記住的
l 對于簡單的常量,應該盡量使用 const 對象或枚舉類型數據,避免使用 #define 。
l 對于類似程序的宏,盡量使用內聯函數,避免使用 #define 。