Effective-C++阅读解析条款(条款二:尽量以const,enum,inline替换#define)

个人主页: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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Lei宝啊

觉得博主写的有用就鼓励一下吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值