Effective C++ Item 2:Prefer constS, enumS, and inlineS to #defineS

来源:互联网 发布:dns cache 域名劫持 编辑:程序博客网 时间:2024/05/16 00:27

这个条款叫“喜欢编译器胜过预处理器”更贴切。因为#define不被视为语言的一部分。当你这样写时,

 

 

符号名ASPECT_RATIO对于编译器来讲是不可见的;在源程序到达编译器之前,ASPECT_RATIO很可能被预处理器删除了。所以,ASPECT_RATIO符号名也许压根就没有进入符号表。这就会使你很困惑了,特别编译过程中涉及到这个常量的使用时,因为错误信息可能指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO在一个不是你写的头文件中定义时,你就不知道这个1.653到底是哪里冒出来的,而你还浪费时间跟踪这个变量。这个问题在符号调试程序(symbolic debugger)中也会出现。原因相同:你编程中用到的名字也许压根就不在符号表(symbol table)里面。

 

解决方案:用常量(constant)来替换宏(macro)

 

 

作为一个常量,AspectRatio对编译器是可见的,并且肯定是进入符号表的。另外对于浮点常量而言,使用常量(constant)比使用#define产生更少的代码量。因为预处理器(preprocessor)盲目地把宏名ASPECT_RATIO替换成1.653,这很可能导致了你的目标代码(object code)中含有1.653的若干个拷贝,但用常量AspectRatio就不同了,顶多会出现一份拷贝。(AspectRatio should never result in more than one copy)。

 

当用常量替换#define时,有二点值得注意。

1、定义常量指针(constant pointers)。因为典型的常量定义是在头文件中(许多源文件包含这些头文件),指针有必要被声明为const型(无论所指向的内容是否为const型)。例如,在头文件中定义一个常量char*字符串,你需要写const二次:

 

 

一般情况下,更推荐使用string对象,所以authorName通常这样定义:

 

 

2、类专属常量(class-specific constants)

为了限制一个常量的范围在一个类内,你需要将这个常量定义为类的一个成员(member),为了保证最多只有一份变量的拷贝,你需要将其定义为static成员:

 

 

上面你看到的是NumTurns的声明(declaration),不是定义(definition)。通常C++要求你对你使用的任何东西提供一个定义(definition),但如果该类专属常量(class-specific constants)是static且是integral type(如,integers, chars, bools)则无需特殊处理。只要你不使用它们的地址(address),你就可以不用定义而声明和使用它们(declare them and use them without providing a definition)。如果你确实需要使用一个类常量的地址,或者当你不使用地址时你的编译器坚持要你给一个定义,你必须另外提供这样的定义式:

 

 

请把这个放在一个实现文件(impl. file)而不是一个头文件(head file)。这是因为类常量的初值是在这个常量被声明的时候给出的(如,NumTurns在声明的时候被赋初始5),因此在定义时不需要再设初值(no initial value is permitted at the point of definition)。

 

顺带说一下,注意我们无法用#define来创建一个类专属常量(class-specific constant),因为#define没有作用域(scope)的概念。一旦定义了一个宏,它就在其后的编译过程中有效(除非在某处被#undef)。这意味着#defines不仅不能用来定义类专属常量(class-specific constant),也不能提供封装性(encapsulation),如就没有private #define这种东西。而当然const数据成员(data member)是可以被封装的,就像NumTurns一样。

 

      旧式的编译器也许不支持上述语法,因为不允许在一个静态类成员(static class member)声明的时候(at its point of declaration)提供一个初始值。并且,“in-class”initialization只允许对integral types进行且仅仅对于常量。如果你的编译器不允许上述语法,你可以把初值放在定义式中:

 

 

这几乎是你在任何时候都需要做的事情。唯一的一个例外是:

当你在类编译期间需要一个类常量值,例如在上述的GamePlayer::scores的数组声明中(编译器坚持必须知道在编译期间知道数组大小)。这时万一你的编译器(错误地)不允许“static integral types 类常量”完成"in-class 初值设定",可改用所谓的"enum hack"的补偿做法。于是GamePlayer可以定义如下:

 

 

1、enum hack的行为某方面像#define而不像const,有时候这正是你想要的。

如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或reference指向你的某个integral constant,enum可以帮助你实现这个约束。此外,虽然优秀的编译器不会为"const objects of integral types"设定额外的存储空间(除非你想创建一个pointer或reference指向该对象),不够优秀的编译器却可能会如此,而这可能不是你想要的。Enums和#defines一样绝对不会导致非必要的内存分配。(unnecessary memory allocation)。

2、很多代码用enum hack,所以有必要了解一下。

 

      再回头看看预处理器。别一个常见的#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会导致函数调用(function call)带来的额外开销(overhead)。下面这个宏夹带着宏实参,调用函数f:

 

 

宏有太多的缺点,光想一想都令人头痛!

 

每次你写宏,你都需要记得在宏体里面把每个参数用括号给括起来。如果你没有这么做的话,当有人用一个表达式来调用宏时,就会出现问题:

 

 

在这里,a自增几次取决于它与什么数相比较!

 

不过幸运的是,你不需要忍受这些无聊的琐事。你可以获得得宏带来的效率以及一般函数的可预料行为和类型安全(type safety)----只要你写一个template inline函数。

 

 

这个template产生一系列的函数。每个函数接受两个同类型的对象,并且以其中较大者调用f。再也没有必要为每个参数打上括号了。也不用去担心参数到底被运算了几次等等。更进一步,因为callWithMax是一个实实在在的函数。它遵守作用域和访问规则(it obeys scope and access rules)。如,你绝对可以写出一个类私有内联函数(private inline function)。而通常用宏(macros)是不可能完成的。

 

     有了constS,enumS和inlineS,我们对于预处理器的需求降低了,但并非完全消除。#include依然是必要的,而#ifdef/#ifndef也继续扮演着控制编译的重要角色。目前还不到预处理器全面引退的时候,但对于你,可以给它放一个长假啦!

 

请记住

1、对于单纯常量(simple constants),最好以const对象或enums替换#defines。

2、对于形似函数的宏(macros),最好改用inline函数替换#defines。

 

 

 

原创粉丝点击