洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目2] 多用const、enum、inline,少用#define

这一条目似乎叫做“多用编译器,少用预处理器”更恰当,因为#define的内容不应该属于语言自身的范畴。这是#define的众多问题之一,请看下面的代码:

#define ASPECT_RATIO 1.653

编译器也许根本就接触不到ASPECT_RATIO这个符号名,它也许在编译器对源代码进行编译以前就被预处理器替换掉了。于是,ASPECT_RATIO这一名字很可能不会被加入符号表。如果代码中使用了该常量,此时编译器产生的错误将会令人费解,因为出错信息只会涉及1.653,而完全不会涉及ASPECT_RATIO。如果ASPECT_RATIO是在某个其他人编写的头文件中定义的,那么对于它的来源你便一无所知,你将在跟踪这一数值上浪费很多时间。在符号调试器中同样的问题也会出现,原因和上述问题是一致的:你在编程时使用的名字可能不在符号表中。

解决的办法是:使用常量来代替宏定义:

const double AspectRatio = 1.653; // 宏的名字通常使用大写字母,

                                   // 于是常量使用这样的名字以示区别

作为语言层面的常量,AspectRatio会确保被编译器所看到,并且肯定会进入符号表中。另外,对于浮点数的情况(比如上述示例),使用常量较#define而言会生成更小的目标代码。这是由于预处理器会对目标代码中出现的所有宏ASPECT_RATIO复制出一份1.653,然而使用常量AspectRatio时拷贝永远不会多于一份。

在使用常量代替#define时有两个特殊情况值得注意。第一是定义指针常量的情形。因为常量一般都定义在头文件中(许多不同的代码文件会包含这些头文件),要将指针本身定义为const的,通常情况下也要将指针所指的内容定义为const的,这一点很重要。比如说,在头文件中定义一个基于char*的字符串常量时,你需要写两次const

const char * const authorName = "Scott Meyers";

const的含义和用法的完整讨论,尤其是涉及指针的情形,请参见条目3。但是,在这里有必要提醒你一下,使用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)与此同时,优秀的编译器不会为整型const对象分配内存,但是粗心大意的编译器也许会这么做,你也一定不会愿意为这类对象分配内存的。与#define类似,enum永远不会带来不必要的内存开销。

了解enum戏法的第二个用处纯粹是实用主义的。许多代码都在这样做,所以你看到它时必须要认得。事实上,enum戏法是模板元编程的一个基本技术。(参见条目48

回到预处理器的问题,#defined命令的另一个用法(这样做很不好,但这非常普遍)就是将宏定义得和函数一样,却没有函数调用的开销。下面例子中的宏定义使用ab中更大的参数调用了一个名为f的函数:

// 使用ab中更大的一个用于调用函数f

#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++为你提供了constenuminline这些新特征,预处理器(尤其是#define)的作用就越来越小了,但是这并不是说可以完全抛弃它。#include 仍是程序中的主角,#ifdef/#ifndef在控制编译过程还有着举足轻重的地位。说预处理器该退休了”还为时过早,但是你还是要经常给它放放长假。

时刻牢记

对于简单的常量,应多用const对象或枚举类型数据,少用#define

对于类似程序的宏,应多用内联函数,少用#define

posted on 2007-04-04 21:58 ★ROY★ 阅读(2191) 评论(0)  编辑 收藏 引用 所属分类: Effective C++


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理