条款2:不要滥用宏
文章目录
- 优先选择编译器而不是预编译器
- 两种特殊情况
- 使用宏替代函数调用
- 总结
优先选择编译器而不是预编译器
假设我们预定义了一个宏#define ASPECT_RATIO 1.653,当我们的程序在这个地方出现错误的时候。可能会出现以下的问题:
- 符号名称ASPECT_RATIO可能永远不会被编译器看到。它可能在源代码进入编译器之前被预处理器删除。(如果发生错误,编译器的给出的错误信息可能是1.653,而不是ASPECT_RATIO。设想一下,如果这部分不是你自己写的,而来自某个头文件呢?)
- 目标代码中,可能出现多个1.653的副本。
也可能由于这个宏是你提供给别人的库导致如果在这个地方出现问题的时候,别人可能找不到这个问题导致程序错误。
这个时候我们有更好的替代方案,定义一个常量:const double AspectRatio = 1.653;
两种特殊情况
1、定义常量指针
由于常量定义式通常被放在头文件内(以便被不同的源码包含),因此有必要将声明为const。例如若要在头文件内定义一个常量的字符串,你必须写const两次:const char *authorName = “Scott Meyers”;
2、类的常量
要把常量的作用域限制为类,就必须把它设为成员;要确保最多有一个常量的副本,就必须把它设为静态成员:
class GamePlayer {
private:
//#define NUMTURNS 5 //宏无法将作用域现在在类的内部
static const int NumTurns = 5; // constant 声明
int scores[NumTurns]; // 使用constant
//...
public:
int numTurns() {
return NumTurns;
}
};
const int GamePlayer::NumTurns; // NumTurns的定义;
1、通常,C++要求为所使用的任何东西提供定义,但类专用的静态整数类型(例如integer、char、bool)常量是个例外。只要不获取它们的地址,就可以在不提供定义的情况下声明并使用它们。
2、因为类常量的初始值是在声明时指定的,所以在定义时不允许赋初始值。
关于类内定义常量,我们还可以使用枚举量:
class GamePlayer {
private:
enum { NumTurns = 5 };
int scores[NumTurns]; // fine,而且枚举更像#define,不可以取地址
//...
};
使用宏替代函数调用
#define指令的另一个常见(错误)用法是使用它来实现看起来像函数的宏,但不会引起函数调用的开销。下面是一个宏,它使用较大的参数调用某个函数f:
int f(int num) { return num; }
// 使用较大的参数,调用函数f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
像这样的宏有很多缺点。必须记住将宏主体中的所有参数都用括号括起来。否则,当有人用表达式调用宏时,可能会遇到麻烦。但即使你很小心,还是可能会发生奇怪的事情:
int a = 5, b = 0;
std::cout << CALL_WITH_MAX(++a, b) << std::endl; // a 递增了2次
std::cout << CALL_WITH_MAX(++a, b + 10) << std::endl; // a 递增了1次
运行结果:我们可以发现由于宏是直接替代的方式,所以实际上a被++了两次,在返回的时候也被++一次,所以第一次结果为7,第二次返回的是b+10,所以第二次的结果为8,两次返回的结果不一致。
替代方案:通过使用内联函数模板,可以获得宏的所有效率,以及普通函数的所有可预测行为和类型安全
template<typename T>
inline T callWithMax(const T& a, const T& b)
{
return f(a > b ? a : b);
}
总结
考虑到const、枚举和内联的可用性,对预处理器(特别是#define)的需求减少了,但并没有完全消除。#include仍然是必不可少的,#ifdef/#ifndef在控制编译方面继续发挥重要作用。
- 对于简单常量,首选const对象或枚举,而不是#define。
- 对于类似函数的宏,优先选择内联函数。