这个条款或许改为“宁可以编译器替换预处理器“比较好,因为或许#define
不被视为语言的一部分。那正是它的问题所在,当你做出这样的事情:
#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见:也许在编译器开始处理源码
之前它就被预处理器移走了。于是记号名称ASPECT_RATIO有可能没进入记号表
内。于是当你运用此常量但获得一个编译错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653
而不是ASPETC_RATIO。如果ASPETC_RATIO被定义在一个非你所写的头文件中
你肯定对1.653以及它来自何处毫无概念,于是你将因为追踪它而浪费时间。这个问题
也可能出现在记号式调试器中原因相同:你所使用的名称可能并未进入记号表。
解决之道是以一个常量替换上述的宏(#define)
const double AspectRatio = 1.653;
作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。此外
对浮点常量而言,使用常量可能比使用#define导致较小量的码,因为预处理器
“盲目的将宏名称ASPECT_RATIO替换为1.653”可能导致目标代码出现多份1.653
,若改用常量AspectRatio绝不会出现相同情况。
当我们以常量替换#define,有两种特殊情况。
(1)定义常量指针。由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此
有必要将指针(而不只是指针所指之物)声明为const.例如上述的authorName
往往定义成这样更好些:
const std::string authorName(“Scott Meyers”);
第二个值得注意的是class专属常量。为了将常量的作用域限制于class内,你必须
让它成为一个成员,而为确保此常量至多只有一份实体,你必须要让它成为一个
static成员
class GamePlayer{
private:
static const int NumTurns = 5;//常量声明式
int scores[NumTurns];//使用该常量
...
}`
然而当你看到的是NumTurns的声明式而非定义式。通常c++要求你对所使用的
任何东西提供一个定义式,但如果它是个class专属常量又是static且为
整数类型,则需要特殊处理。只要不取它们的地址,你可以声明并使用它们而
无需提供定义式。但如果你取某个class专属常量的地址,或纵使你不取其地址
而你的编译器却坚持要看到一个定义式,你就必须另外提供定义式如下:
const int GamePlayer::NumTurns ;//NumTurns 的定义
//下面告诉你为什么没有给予数值
请把这个式子放进一个实现文件而非头文件。由于class常量已在声明时获得
初值,因此定义时不可以再设初值。
顺带一提,请注意,我们无法利用#define创建一个class专属常量,因为#defines
并不重视作用域。一旦宏被定义,它就在其后的编译过程中有效。这意味着
#defines
不仅不能用来定义class专属常量,也不能提供任何封装特性,也就是说没有所谓private #define这样的东西。而当然const成员是可以被封装的,NumTurns
就是。旧式编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初值,此外所谓的“in-class初值设定”也只允许对整数常量进行,如果你的编译器
不支持上述语法,你可以将初值放在定义式。
class CosEstimate
{
private:
static const double FudgeFactor;//static class 常量声明式
...//位于头文件内
const double//static class常量定义
CostEstimate::FudgeFactor = 1.35;//位于实现文件内
};
这几乎是你在任何时候唯一需要做的事情。唯一例外的是当你在class编译期间
需要一个class常量值,例如在上述的GamePlayer::scores的数组声明式中
。这时候万一你的编译器不允许“static 整数型 class常量”完成“in class
初值设定”,可改用所谓的“the enum hack”补偿做法。其理论基础是:“一个属于
枚举类型的数值可权充ints被使用”,于是GamePlayer可定义如下:
class GamePlayer
{
private:
enum{NumTurns = 5;
int scores[NumTurns];
...
}
}
基于某个理由enum hack值得我们认识,第一,enum hack的行为某方面说比较像
#define而不像const,有时候这正是你想要的。
例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。此外虽然优秀的编译器不会为“整数型const对象”设定另外的存储空间(除非你创建一个pointer或reference指向该对象),不够优秀的编译器却可能如此,而这可能是你不想要的,Enums和#defines一样绝不会导致非必要的内存分配
认识enum hack的第二个理由纯粹是为了实用主义。许多代码用了它,所以看到它时
你必须认识它。事实上,enum hack是模板元编程的基础技术。
把焦点拉回预处理器。另一个常见的#define误用情况是以它实现宏,宏看起来像函数,但不会
招致函数调用带来的额外开销,下面这个宏夹带着宏实参,调用函数f:
//以a和b的较大值调用f
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
这般长相的宏有着太多缺点,光是想到它们就让人痛苦不堪
无论何时当你写出这种宏,你必须记住为宏中的所有实参加上小括号,看看下面这个不可思议的事情
int a = 5,b=0;
CALL_WITH_MAX(++a,b);
CALL_WITH_MAX(++a,b+10);
在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”!幸运的是你不需要对这种无聊事情提供温床,你可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性–只要你写出template inline函数形式、范型形式、无编程形式的语言。这些能力和弹性使得
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
f(a>b?a:b);
}
这个template产出一整群函数,每个函数都接受两个同型对象,并以其中较大
者调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算(求值)多次……等等。此外由于callWithMax是个真正的函数,它遵守作用域和
访问规则。例如你绝对可以写出一个“class 内的private inline函数”。一般而言宏
无法完成此时。
有了consts、enums和inlines,我们对预处理器(特别是#define)得需求降低
了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演
控制编译的重要角色。目前还不到预处理器全面引退的时候,但你应该明确的
给予它更长更频繁的假期。
请记住:对于单纯常量,最好以const对象或enums替换#defines
对于形似函数的宏,最好改用inline函数替换#defines