本文源自:http://www.cppblog.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 。