分享
 
 
 

翻译:Effective C++, 3rd Edition, Item 2: 用 consts, enums 和 inlines 取代 #defines

王朝c/c++·作者佚名  2006-01-31
窄屏简体版  字體: |||超大  

Item 2: 用consts, enums和inlines取代#defines

这个条款改名为“用编译器取代预处理器”也许更好一些,因为 #define 根本就没有被看作是语言本身的一部分。这是它很多问题中的一个。当你像下面这样做:

#define ASPECT_RATIO 1.653

编译器也许根本就没有看见这个符号名 ASPECT_RATIO,在编译器得到源代码之前,这个名字就已经被预处理器消除了。结果,名字 ASPECT_RATIO 可能就没有被加入符号表。如果在编译的时候,发现一个常量使用的错误,你可能会陷入混乱之中,因为错误信息中很可能用 1.653 取代了 ASPECT_RATIO。如果,ASPECT_RATIO 不是你写的,而是在头文件中定义的 ,你可能会对 1.653 的出处毫无头绪,你还会为了跟踪它而浪费时间。在符号调试器中也会遇到同样的问题,因为这个名字可能并没有被加入符号表。

解决方案是用常量来取代宏:

const double AspectRatio = 1.653; // uppercase names are usually

// for macros, hence the name change

作为一个常量,AspectRatio 被编译器明确识别并确实加入符号表。另外,对于浮点常量(比如本例)来说,使用常量比使用 #define 能产生更小的代码。这是因为预处理器盲目地使用宏名字 ASPECT_RATIO 置换 1.653,导致你的目标代码中存在多个 1.653 的副本,如果使用常量 AspectRatio,就绝不会产生多于一个的副本。

用常量代替 #defines 时,有两个特殊情况值得提出。首先是关于常量指针的定义。因为常量通常被定义在头文件中(这样它们就可以被包含在多个源文件中),除了指针指向的目标是常量外,指针本身被声明为常量更加重要。例如,在头文件中定义一个基于字符串的常量 char* 时,你必须写两次 const:

const char * const authorName = "Scott Meyers";

对于 const(特别是与指针相结合)的意义和使用的完整讨论,请参考 Item 3。无论如何在此值的一提的是,string 对象通常比基于它的祖先的 char* 更可取,所以,更好的 authorName 的定义方式如下:

const std::string authorName("Scott Meyers");

第二个特殊情况涉及到类属(类内部专用的)常量。为了将一个常量的作用范围限制在一个类内,你必须将它作为一个类的成员,而且为了确保它最多只有一个副本,你还必须把它声明为一个 static 成员。

class GamePlayer {

private:

static const int NumTurns = 5; // constant declaration

int scores[NumTurns]; // use of constant

...

};

你只从上面看到了 NumTurns 的声明,而不是定义。通常,C++ 要求你为你使用的任何东西的提供一个定义,但是一个静态的(static)整型族(例如:整型值、字符值和布尔值)的类属常量是一个例外。只要你不去取得它的地址,你可以只声明并使用它,而不提供它的定义。如果你要取得一个类属常量的地址,或者你使用的编译器在你没有取得地址时也不正确地要求定义的话,你可以提供如下这样一个独立的定义:

const int GamePlayer::NumTurns; // definition of NumTurns; see

// below for why no value is given

你应该把它放在一个实现文件而非头文件中。因为类属常量的初值在声明时已经提供(例如:NumTurns 在定义时被初始化为 5),因此在定义处允许没有初值。

注意,顺便提一下,没有办法使用 #define 来创建一个类属常量,因为 #define 不考虑作用范围。一旦一个宏被定义,它将大范围影响你的代码(除非在后面某处存在 #undefed)。这就意味着,#defines 不仅不能用于类属常量,而且不能提供任何类型的封装,也就是说,没有类似“私有” #define 的东西。当然,常量数据成员是能够被封装的,NumTurns 就是如此。

某些老的编译器可能不接受上面的语法,因为它习惯于将一个静态类成员在声明时就获得初值视为非法。此外,class-in 初始化仅仅对于整型族和常量才被允许。如果上述语法不能使用,你可以将初值放在定义处:

class CostEstimate {

private:

static const double FudgeFactor; // declaration of static class

... // constant; goes in header file

};

const double // definition of static class

CostEstimate::FudgeFactor = 1.35; // constant; goes in impl. file

这就是你所要做的全部。仅有的例外是当在类的编译期需要类属常量的值的情况,例如前面在声明数组 GamePlayer::scores 时(编译器必须在编译期知道数组的大小)。如果编译器(不正确地)禁止这种 in-class 规范(关于静态整型族类属常量的初值的使用方法),一个可接受的替代方案被亲切地(并非轻蔑地)昵称为 the enum hack。这项技术在以下事实上获得优势:一个枚举型的值可以用在一个需要整型的地方,所以 GamePlayer 可以被如下定义:

class GamePlayer {

private:

enum { NumTurns = 5 }; // "the enum hack" — makes

// NumTurns a symbolic name for 5

int scores[NumTurns]; // fine

...

};

The enum hack 有几个值得被人所知的原因。首先,the enum hack 的行为在几个方面上更像一个 #define 而不是 const,而有时这正是你所需要的。例如:可以合法地取得一个 const 的地址,但不能合法地取得一个枚举的地址,这正像同样不能合法地取得一个 #define 的地址。如果你不希望人们得到你的整型常量的地址或引用,枚举就是强制约束这一点的好方法。(关于更多的通过编码的方法强制执行设计约束的方法,参见 Item 18。)同样,使用好的编译器不会为整型族的 const 对象分配多余的内存(除非你创建了这个对象的指针或引用),即使拖泥带水的编译器乐意,你也决不会乐意为这样的对象分配多余的内存。像 #defines 和 enums 就绝不会导致这种不必要的内存分配。

需要知道 the enum hack 的第二个理由是纯粹实用主义的,大量的代码在使用它,所以当你看到它时,你要认识它。实际上,the enum hack 是模板元编程的一项基本技术(参见 Item 48)。

回到预处理器上来,#define 指令的另一个普遍用法是实现看来像函数,但不会导致一个函数调用开销的宏。以下是一个用较大的宏参数调用函数 f 的宏:

// call f with the maximum of a and b

#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

这样的宏有数不清的缺点,想起来就让人头疼。

无论何时,你写这样一个宏,都必须记住为宏体中所有的参数加上括号。否则,当其他人在表达式中调用了你的宏,你将陷入麻烦。但是,即使你确实了解它,你还是会看到意料之外的事情发生:

int a = 5, b = 0;

CALL_WITH_MAX(++a, b); // a is incremented twice

CALL_WITH_MAX(++a, b+10); // a is incremented once

这里,调用 f 之前 a 递增的次数取决于它和什么进行比较。

幸运的是,你并不需要和这样不知所云的东西打交道。你可以通过一个 inline 模板函数来获得宏的效率,可完全预测的行为和正规函数的类型安全(参见 Item 30):

template<typename T> // because we don't

inline void callWithMax(const T& a, const T& b)// know what T is, we

{ // pass by reference-to-

f(a > b ? a : b); // const — see Item 20

}

这个模板产生一组函数,每一个获得两个相同类型的对象并使用其中较大的一个调用 f。这样就不需要为函数体内部的参数加上括号,也不需要担心多余的参数解析次数,等等。此外,因为 callWithMax 是一个真正的函数,它遵循作用范围和访问规则。例如,谈论一个类的私有的 inline 函数会获得正确的理解,但是用宏就无法做到这一点。

为了得到 consts,enums 和 inlines 的有效性,你需要尽量减少预处理器(特别是 #define)的使用,但也不能完全消除。#include 依然是基本要素,而 #ifdef/#ifndef 继续扮演着重要的角色。现在还不是让预处理器完全退休的时间,但你应该给它漫长而频繁的假期。

Things to Remember

· 对于简单常量,用 const 对象或 enums 取代 #defines。

· 对于类似函数的宏,用 inline 函数取代 #defines。

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有