我在整里C语言预处理指令的时候曾具体说明了#define的用法:浅谈C语言预处理指令而今天我们来看看为什么又最好不要用#define。
我们写这样一行代码:
#define NUM 1.653
NUM可能不会被编译器看见;也许再变一起开始处理源码之前它就被预处理器移走了。于是记号名NUM可能未进入记号表内。于是当你运用此常量但获得一个错误信息时,就会不知所措,因为这个错误信息也许会提到100而不是NUM。当NUM被定义在一个不是你自己写的头文件中的时候,肯定会对其所指代的东西感到陌生,于是你开始找寻它的定义,浪费了时间。这个问题可能出现在记号式调试器中,原因相同,你可能使用的名称并未进入到记号表中。
于是我们就可以用下面的代码去代替它:
const double NUM = 1.653; //大写名称通常用于宏
为什么说问题解决了呢?它是一个语言常量,因此NUM肯定会被编译器看到。尤其是对浮点常量,使用常量会比使用#define导致更少的代码量,因为预处理器盲目地将宏NUM替换成1.653可能会导致目标码出现多份1.653。
用常量代替#define有两种情况值得注意:
一、定义常量指针:常量通常放在头文件中,方便移植,因此有必要将指针(并非指针所指之物)声明为const。例如想在头文件内定义一个常量的char*-based字符串,必须这么写:
const char* const authorName = "Scott Meyers";
但值得注意的是,string对象通常会比其前辈char*-based更合适。所以上面的这样写会比较好:
const std::string authorName("Scott Meyers");
二、class专属常量。这种情况不可以用#define的方式去定义,因为#define是不重视作用域这种东西的,一旦被定义,在之后就全部有效,除非碰到#undef。而我们想定义一个class专属变量时只需要将变量定义在类中即可,充分利用它的作用域。而为了确保此常量至多只有一个实体,为此我们最好将它定义为一个static成员:
class GamePlayer
{
private:
static const int NumTurns = 5; //声明常量
int scores[NumTurns]; //使用常量
};
上面的类中定义了一个成员的NumTurns ,然后用它来定义了一个数组。注意,此处的NumTurns 是一个声明式而非定义式。通常C++要求你所使用的所有东西都要有定义式,但如果它是一个class专属常量又是static且为整数类,你可以声明并使用他们而无需提供定义式。但如果你取某个class专属常量的地址或你的编译器坚持要看到一个定义是,你就必须额外提供一个定义式:
const int GamerPlayer::NumTurns; //NumTurns 的定义式
由于在class中声明的手已经给它富国值,这里自然在定义的时候就不用赋值了。
旧式编译器不允许static成员在其声明式上获得初值,此外所谓的"in-class处置设定"也只允许对整数常量进行。不支持也没关系,我们可以这么写:
class CostEstimate
{
private:
static const double FudgeFactor; //static class常量声明
...
};
const double CostEstimate::FudgeFactor = 1.35; //常量定义
其实这么做还会存在一个潜在的问题:第一次定义的那个类GamePlayer里面的数组大小是用的我们自己声明的常量。如果编译器不支持在类中声明变量的同时给变量赋值,那么就会报错,因为编译器坚持必须在编译器器件知道数组的大小。这个时候就要用所谓的“the enum hack”补偿法。其理论依据是:一个属于枚举类型的数值可以被当作int值来使用,于是可以修改如下:
class CostEstimate
{
enum { NumTurns = 5 }; //令NumTurn成为5的一个记号名称
int scores[NumTurns];
...
}
其实enum hack更像是#define而不是#define,但有时候我们就是想这么干!例如取一个const的地址是合法的,但取一个enum的地址就不合法,同样的,我们也不可以取#define的地址。如果你不想别人获得一个指针指向某个整数常量,enum可以帮助你实现这个要求。另外,一些编译器会为“整数型const对象”设定另外的存储空间。但是enum和#define一样是不会导致非必要内存分配的。
还有一点很重要的是,我之前在#define的博客里说#define的函数某种程度上来说是很好用的。那是因为C语言中没有Template模板,用#define可以实现类似泛型编程。但是在C++中,我们大可不必这么做,我们强大的C++支持泛型编程。用#define需要注意的地方我也在那篇博客中提到过了。那么我们来看看为什么在C++不推荐用宏
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b)) //f()是一个函数,这个宏选择f()的参数
这个定义看着就不是特别舒服,括号太多了,接下来来调用:
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加两次
CALL_WITH_MAX(++a, b+10); //a被累加一次
第一次调用,由于a递增后比较大,传入函数中,又递增一次;第二次调用,a递增后比不过b,于是就传入b,不会递增。说简单点,a的递增次数取决它被拿来和谁比较。
而我们好好利用C++的Template模板:
template
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a : b);
}
这样我们照样完成了泛型编程,我们不需要要管传入的数据类型,并且内联函数保证其不会有调用函数那多出来的时间。此外,由于callWithMax是一个真正的函数,其有自己的作用域,所以可以写一个class内的private inline函数。
所以,我们发现,预处理器确实有很多不足,而我们也找到了方法去代替它,但我们不肯能完全取代它,#include是必需品,#ifdef系列可以控制程序编译。