这个条款改为“尽量把工作交给编译器而不是预编译器”更恰当,因为或许#define不被视为语言的一部分,这正是问题所在,如下:
#define DATA 1.6
编译器也许根本就接触不到这个DATA符号名 ,可能它在编译器对源代码进行编译之前就被预处理器替换掉了。So, 这一名字很可能不会列在符号表(symbletabal)中,如果你在代码中使用了这常量遇到一些错误,却往往很难找到问题的所在,因为出错信息也许会提到1.6而不提到DATA。如果DATA 是在某个你不知情的头文件中定义的,那么寻找 1.6的出处可以说是大海捞针,跟踪这一数值上将浪费很多时间。在符号调试器中也会出现同样的问题,原因是一样的:你在编程时使用的名字可能并没有加入符号表中。
解决的方法是:使用常量来代替宏定义
const double DATA=1.6; //宏名一般大写
显然,作为一个语言常量,DATA肯定会被编译器看到,当然就会进入记号表内。另外,对于浮点数来说,使用常量比#define会生成更小的目标代码。这是因为预处理会对目标代码中出现的所有宏DATA复制一份1.6,而使用常量的DATA永远只有一份。
当使用常量替换#defines时,有两种特殊情况。一、定义常量指针(constant pointers),因为常量定义通常被放在头文件内(方便不同源文件包含)。要将指针定义为const的,通常情况下也要把指针所指的内容定义为constant的,比如说,在一个头文件中定义一个 char* 的字符常量时,你需要写两次 const :
const char* const Name=”lsfreeing”;
在条款3中将全面介绍 const 的含义和用法,尤其是在其与指针混合使用时的一些细节问题。但是,在这里提醒你使用 string 对象要比使用其祖先“ char* ”好得多,知道这一点是很有意义的。上述的 Name 最好以这样的形式定义:
<span style="font-family:Microsoft YaHei;font-size:14px;"><span style="white-space:pre"> </span>const std::string Name("lsfreeing");</span>
第二个特殊情况关系到类内部的常量。为了将常量的作用域限制在一个类里,必须将这个常量作为类的成员;为了限制常量为一份,必须将其声明为 static 成员:
class lsfreeing
{
private:
static const int Number=5; //常量声明
int scores[Number]; //使用该常量
};
上面的 Number只是声明,而不是定义。通常情况下, C++ 要求你为所有要用到的所有东西做出定义,但是这里有一个例外:类内部的静态常量如果是整型(比如整数、字符型、布尔型)则不需要定义。只要不需要得到它们的地址,可以只声明而不提供定义。如果你需要得到类常量的地址;即使你不需要这一地址,而编译器坚持你必须为这个常量做出定义,这两种情况下我们应该用下面的形式提供其定义:
const int lsfreeing::Number; //Number的定义
//下面将讨论为什么没有赋值
这段代码应该放在一个实现文件中。这是因为类常量的初始值已经在其声明的时候给出了(比如说, Number在声明时就被初始化为 5 ),而在定义的时候不允许为其赋初值
要注意的是,不能用 #define 来创建一个类内部的静态常量,这是因为 #define 不关心作用域。一旦定义一个宏,在编译时它将影响到所有其它代码(除非某处使用 #undef 取消这个宏定义)。这意味着 #define 不能用来定义类内部的常量,同时也无法带来任何封装效果,就是我们所说的:“私有的” #define不存在。但const 数据成员可以得到封装,上面DATA就是一个例子。
早期的编译器不支持上面代码的语法,因为那时候在声明一个静态的类成员时为其赋初值是非法的。与此同时,只有整型数据才可以在类内部进行初始化,并且只有常量才能得到初始化。在这种情况下不能使用上面的语法,我们可以在定义的时候为其赋初值:
class lsfreeing{
private:
static const double DATA; //静态类声明
… //在头文件中进行
}
constdouble lsfreeing::DATA=1.35; //静态类常量的定义,位于实现文件内
上面几乎是我们要了解的全部内容。但也有例外:当你在编译一个类的时候,你可能需要这个类内部的一个常量值,比如说前述的lsfreeing::scores 数组的声明(编译器可能需要在编译时了解数组大小)。编译器在这时违背了为类内部的静态整型常量赋初值的规范,补救的方法是可以使用“ enum”。这一方法的理论基础是:枚举类型数据是 int 型的,所以 lsfreeing也可以这样定义:
class lsfreeing{
private:
enum{Number=5}; //使Number成为一个符号名,值为5
int scores[Number]; //可以正常运行
}
基于多个理由, enum hack值得我们了解。首先, enum hack的行为更像一个 #define 而不是 const ,在某些情况下这更符合的要求。比如说,取得一个 const 的地址是合法的,但是取 enum 的地址则是非法的,取#define 的地址同样非法。如果不想让其他人获得整形常量的指针或引用,使用枚举类型是一个好方法。(参见第 18 项)。优秀的编译器不会为“整数型const变量”分配额外的存储空间(除非创建一个指向该对象的指针或引用),但不够优秀的编译器可能如此。与 #define 类似, enum 不会带来不必要的内存开销。
了解 enum hack的第二个理由纯粹是实用主义。许多代码都用,所以你看到它时必须要认得。事实上, enum hack是模板元编程的一个基本技术。
再次回到预处理, #defined 的 另一个用法(这样做很不好,但这非常普遍)就是将宏定义得和函数一样,但不会带来函数调用的开销。下面例子中的宏定义使用 a 和 b 中 更大的参数调用了一个名为 f 的 函数:
<span style="white-space:pre"> </span>// 使用 a 和 b 中 更大的一个调用函数
#define MAX(a, b) f((a) > (b) ? (a) : (b))
这样的宏有很多缺点。当这样的宏,我们必须为宏内部所有的参数加上括号。否则,即使做调用会出现问题。即使为每个实参加了括号,也会发生一个问题,比如下面的代码:
inta=5,b=0;
MAX(++a,b); //a自增2次
MAX(++a,b+10); //a自增一次
在这里调用f之前,a的自增次数取决于它和谁比较。
幸运的是,我们可以使用内联函数的模板,此时你可以得到宏的高效,并且一切都是可预知和安全的:
template<typenameT>
inline void Max(const T& a, const T& b)
// 因为我们不知道 T 的类型是什么,因此我们通过引用传递 const 参数。参见第 20 项
{
f(a > b ? a : b);
}
这一模板创建了一类函数,其中每一个函数都会得到同一类型的两个对象,使用其中较大的一个来调用函数 f 。可以看到,在函数内部不需要为参数加括号,不需要担心参数会被多次操作。与此同时,由于Max 是一个真实的函数,它遵循作用域和访问规则,比如类可以拥有私有的内联函数。而在这些问题上宏无法实现
尽管有了const 、 enum 、 inline 这些新特征,预处理器(尤其是 #define )的作用就越来越小,但#include 仍是程序中的主角, #ifdef/#ifndef 在控制编译过程还有着举足轻重的地位。
需要记住的:
对于简单的常量,应该尽量使用 const 对象或枚举类型数据,避免使用 #define 。
对于类似程序的宏,尽量使用内联函数,避免使用 #define 。