简介
本条款中介绍了替代预处理指令 #define
的几种做法。
- 对于常量,最好以
const
对象或enum
来替换#define
- 对于形似函数的宏 (macros),最好改用 inline 函数替换
#define
例子
这个条款还可以有另一种说法:“尽量使用编译器操作代替预处理器操作”,使用 #define
的问题在于:#define
本身不被视为语言的一部分,参考以下例子:
当我们使用:
#define ASPECT_RATIO 1.653
由于在预处理阶段,所有 ASPECT_RATIO
都被替换成了 1.653
。所以对于编译器而言,ASPECT_RATIO
相当于从没出现过,也不会保存在其记号表 (symbol table)里。这对于我们在 debug 过程中,有可能会带来很多困难。当我们因为使用了常量而编译错误时,编译错误信息有可能会出现 1.653
但不会出现 ASPECT_RATIO
;而如果这个常量是定义在某个不是我们写的头文件(第三方库)中,追踪错误信息会很困难。原因就是因为记号表里没有 ASPECT_RATIO
。
而针对上述情况,解决的方法是用一个常量来代替宏:
const double AspectRatio = 1.653; // 常量一般不使用全大写
作为常量,AspectRatio
会进入编译器的记号表中。另外当我们使用常量的类型很长时(如本例的浮点数常量),在多个地方使用宏(ASPECT_RATIO
)会导致目标代码中重复出现多次该常量(本例是 1.653
),因此提高了空间消耗。
具体实施方法
接下来是是使用常量替换 #define
的一些具体做法,这里主要提以下两种情况:
定义常量指针
由于常量定义通常会放在头文件中,所以当我们定义常量指针时,除了定义指针对象为 const
以外,还需要将指针本身也定义成 const
,以防止别人使用时修改指针本身使其不再指向原对象。例如我们要定义一个常量字符串(以 char*
的形式时),需要定义成:
const char* const authorName = "Scott Meyers";
关于 const
的使用会在之后的条款中讨论。当然,针对字符串而言,使用 std::string
比 char*
更加方便,所以上述定义也可以改写为:
const std::string authorName("Scott Meyers");
定义 class 专属常量
使用 const static 常量
首先要说明一点,由于宏定义 #define
并不关于作用域 (scope)。只要我们使用了某个宏定义,在之后编译中只要没出现(#undef
)所以出现了宏定义的地方都会有效。因此 #define
不能够用来定义 class 专属常量,也不能提供任何封装性。而使用 const
常量是可以的。
在使用 const
来定义 class 专属常量时,需要满足两点:
- 为了将其作用域限制在 class 中,需要让其成为该 class 的一个成员变量。
- 为了这个常量针对所有实例都只保存一份实体,需要将其定义成一个 static 成员
如下所示:
class GamePlayer {
private:
static const int NumTurns = 5; // 常量声明式
int scores[NumTurns]; // 使用常量
};
这里要注意一下,上述中 NumTurns
的语句只是一条声明式而非定义式。意味着编译器并没有对其分配空间,因此没有地址。理论上不能使用,但是假如它满足:
- 属于 class 专属变量
- 并且是 static 的 (注意:C++11 之后的标准已经支持 non-static 变量在声明式中获得初值,参考:In-class member initializers)
- 而且是整数类型(integral type,如
int
,char
,bool
)。
满足以上条件的情况下,只要我们在不对其取地址的情况下,只需要声明就可以使用它。如果我们需要获取该变量 (NumTurns
)的地址(或者某些编译器坚持需要一个定义式)时,我们可以在实现文件中提供定义式如下:
const int GamePlayer::NumTurns; // NumTurns 的定义式,注意由于声明式已提供初值,这里不能再赋值
由于声明式中,NumTurns
已经获得初值,所以定义式中不需要(由于 const 的特性所以也不允许)再次赋值。
注意,这个做法并不是通用的。部分旧式编译器不支持 static 变量在声明式中获得初值;此外,这种 in-class 的初值设定也对整数变量进行。在这种情况下,我们可以将初值放在定义式中:
// 头文件中
class CostEstimate {
private:
static const double FudgeFactor; // static class 常量声明
};
// 实现文件中
const double CostEstimate::FudgeFactor = 1.35; // static class 常量定义
使用 enum hack
但是在考虑如下情况,我们必须要在声明式中获得初值(如上例中数组 scores
必须要在编译期间就知道数组长度),但是编译器(错误地)不支持 in class initializer 特性。(注:在现在的情况下(2020年),几乎没有编译器不支持这一特性了。) 这种情况下,考虑枚举类型(enum
)可以作为 int
使用,我们可以利用一种 “enum hack” 的技巧来实现,如下所示:
class GamePlayer {
private:
enum { NumTurns = 5 }; // "enum hack" - 将 NumTurns 作为 5 的一个记号名称
int scores[NumTurns]; // 编译成功
}
enum hack 有以下特点使得我们必须对其有一定认识:
-
enum hack 的行为某种程度上更贴近
#define
而非const
,有时候我们会跟想要这类特性。如:- 我们不能获取
enum
的地址。当我们不想让别人通过 pointer 或 reference 来指向你的某个整数变量时,可以使用; - 此外,虽然大部分编译器不会为整数型
const
对象设定额外的存储空间;但部分编译器可能会,这样就造成了额外的空间浪费,在这一点上使用enum
和#define
都可以避免出现这种情况
- 我们不能获取
-
从功利主义的思想来看,很多代码都会使用 enum,所以我们也必须要去学会如何正确使用它。
宏
除了上述使用 #define
来定义常量情况下,还有一种常见的做法是使用 #define
来实现宏(macros)。宏看上去像函数,但又不会有调用函数(function call)时带来的额外开销。下面的例子中宏夹带红参数并调用函数 f
:
// 以 a 和 b 的较大值调用 f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
这种做法看似没有问题,实际上有很多缺点,使用起来相当不方便。如下:
- 首先是你必须手动地为所有实参(上例中的
a
和b
)添加上小括号,否则在不同情况下(含表达式时)调用宏的时候会出现问题 - 即便所有实参已经加上小括号了,还是有可能会出现问题。下面的例子中,
a
累加的次数取决于a
和b
的大小关系。
#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 累加两次
而当我们想避免出现上述问题,获得一般函数中所有可预料行为已经类型安全,并且想获得宏带来的效率时,我们可以使用模板内联函数(template inline function),如下所示:
template<typename T>
inline void callWithMax(const T& a, const T& b) { // 由于我们不知道 T 是什么,所以采用 pass by reference to const
f(a > b ? a : b);
}
上例中的 template 为不同类型对象生成一系列函数。每个函数接受两个同型对象,并以较大值调用函数 f
。这个函数除了避免了上述宏中出现的所有问题(不需要手动加小括号,也不用担心表达式会调用多次),并且由于它是一个真正的函数(相对于宏而言),所以它遵守作用域和访问规则。这意味着我们可以写出一个 class 内的 private inline 函数,而宏无法实现这种需求。
总结
利用 const
,enum
和 inline
的情况下,我们对预处理器(尤指 #define
)的需求可以大大降低。当然针对 #include
,#ifdef/#ifndef
这类指令我们还是会需要使用。总的来说,本条款可以归纳如下:
- 对于常量,最好以
const
对象或enum
来替换#define
- 对于形似函数的宏 (macros),最好改用 inline 函数替换
#define
完整可运行代码地址:Effective-Cpp-Reading-Note