个人主页:Lei宝啊
愿所有美好如期而遇
书中说这个条款或许改为“宁可以编译器替换预处理器”比较好,这句话在我看来原因是这样的:
如果我们有这样一个宏(假设写这个宏的人比较粗心):#define Add(x, y) x + y
我们本意是想得到x+y的值,但是如果我们在这样一个表达式中:int ret = 3 * Add(4 + 5); 我们预期的结果应该是27,但是实际上我们得到的结果是17,这就是由于在预处理阶段进行了宏替换,表达式就成了这样:ret = 3 * 4 + 5;
又或者是这样:#define NUM 1.653,如果我们使用这个常量但是获得一个编译错误信息时,错误信息可能会提到1.653而不是NUM,因为在预处理阶段就已经进行了宏替换,所以在编译阶段已经没有NUM了。如果这个宏甚至不是我们写的,那么如果报错1.653,那我们一定对这个值没有概念,不知道他是哪里来的,所以我们不推荐使用宏,而是使用const,enum,inline等,因为他们都是在编译阶段的,在编译后会进入符号表。所以上面的宏我们可以替换成这样:const double NUM = 1.653;
当我们使用常量替换宏,有两种特殊情况需要我们注意:
第一就是定义常量指针,由于常量定义通常被放在头文件内,因此有必要将指针和他所指的值都声明为const,假设我们定义一个常量的字符串,我们使用指针指向,我们通常这样写:
const char* const authorname = "Scot Meyers";
并且书中提到,string对象通常比char*更加合适,因为我们是可以使用char*来构造string对象,并且string对象使用起来更加方便,所以他推荐这样定义上面的authorname变量:
const std::string authorname("Scot Meyers");这样authorname这个变量也就同样不能对这个字符串进行修改了。
第二个需要注意的就是class的专属常量。为了将常量的作用域限制在class内,所以必须让其成为class内的一个成员:而常量我们没有必要让每个对象都拥有一份,所以最好是让所有对象都能够共享他,所以我们让他成为一个static成员,就像这样:
class GamePlayer
{
private:
static const int Num = 5;
int scores[Num];
}
这里博主要提醒一下,静态成员变量只能在类外进行初始化,而上面这个初始化是一个特殊的例子,仅仅只有const int这样的静态成员变量可以在类内这样进行声明。
并且我们书中提到,通常C++要求我们对所使用的任何一个东西提供定义式,但如果他是个class专属常量并且是static,并且还是整型,那么只要不取他们的地址,我们就可以使用他们并且无需提供他们的定义式,就像这样:
class Gameplayer
{
public:
static const int Num = 5;
int scores[Num];
};
int main()
{
cout << Gameplayer::Num << endl;
return 0;
}
但是如果我们需要取他的地址,就需要提供定义式(书中这样写的,但是博主经过测试,发现即使不提供定义式,似乎也是可以的,但是我们仍然还是按照书中的去写代码,不要依赖于编译器的各种骚操作):
并且我们这里要说,如果这样的一个常量在声明时获得初值,那么定义时不可以再给初值!
顺带一提,我们无法利用#define创建一个class专属常量,因为#define并不注重作用域,也就是说,一但宏被定义,那么他在其后的编译过程中都是有效的,除非在某处被#undef。
也就是说,没有private : #define这样的东西,他不能够用来定义class专属常量,也就没有任何封装性。
书中提到,如果你的编译器不支持静态整型常量在类内给初值,那么就将初值放在定义式。
唯一例外的是,如果有成员变量,也就是我们上面的scores数组需要这个常量值,那么在编译期间,这个常量值就必须让编译器知道,也就是说,这个常量需要在类内给一个初值,在定义式给初值是不可以的:
这也是唯一例外。
如果你的编译器不允许在类内给初值,那么可以使用enum这种补偿做法。这种做法的理论基础是:“一个枚举类型的数值可以被充作ints使用”,所以Gameplayer可以定义成这样:
书中说到enum值得我们去认识,那么我们就去认识一下,第一点,enum的行为的某方面比较像#define而不是const,他举了这样一个例子:如果我们想要取一个const的地址是合法的,而取enum的地址就不合法,同样的,取#define的地址通常也不合法。
如果我们不想让别人获得一个指针或引用指向我们的某个整型常量,enum就可以帮助我们实现这个约束,因为取他的地址是不合法的。
同时书中提到空间上的问题,说到优秀的编译器不会为整数型const对象设定另外的空间(除非我们创建一个指针或引用指向这个对象),但不够优秀的编译器可能不会这么做,而这可能不是我们想要的结果。enum就和#define一样绝不会导致非必要的内存分配,这里其实博主理解的也不是很清楚,所以也就不多解释。
书中说到认识enum的第二个理由纯粹是为了使用主义。许多代码用了他,所以我们见到他时必须认识他,事实上,enum是模板编程的基础(事实上,博主不清楚这点,解释不清)
现在我们继续谈预处理器的问题。也就是说,我们使用宏的时候,需要加小括号,否则就会像我们开始Add那样得出我们不想要的结果,而这些小括号往往令人抓狂。
首先有一个这样的函数:
int f(int a)
{
return a;
}
我们使用宏,有这样一个例子:
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
书中使用这样的函数来进行替代:
template<class T>
inline int callWIthMax(const T& a, const T& b)
{
return f(a > b ? a : b);
}
我们对他们进行举例,看结果:
我们可以看见宏中的++a,a是否要++竟然取决于比较的先后顺序!所幸我们不需要为这种无聊的方式浪费时间,所以就有了上面替代的函数。(其实只要使用函数,这些值都将是确定的,而不是像宏那样,烦且存在许多不确定性)。并且这样的函数可以成为在类内的private inline函数,一般而言宏无法完成此时。
有了const, enum, inline,我们对于预处理器的需求(特别是#define)降低了,但是#include仍然是必需品,并且#ifdef/#ifndef对于控制编译也是很重要的,博主这里对于#if 和 #endif也是常用,博主常常这样使用:
#include <iostream>
using namespace std;
#if 1
int main()
{
//using...
return 0;
}
#endif
#if 0
int main()
{
//...
return 0;
}
#endif
其实相当于一个变相的注释了,而且想释放使用时将0改成1即可。
本篇重点,请记住:
- 对于单纯常量,最好使用const对象或enum替换#define
- 对于形似函数的宏,我们最好改用inline函数替换#define