《Effective C++》学习笔记 (2nd.尽量以const/enum/inline替换#define)

在阅读本章之前,如果对const这个关键字还比较陌生的话,建议先参考一下本人关于const的学习笔记,链接:https://blog.csdn.net/qq_42601535/article/details/89675865

那么在正式开始之前,我们还需要知道两个概念:编译器和预处理器。
C和C++的程序编译一般分为四个步骤:

  • 预处理(preprocessing)
  • 编译(compilation)
  • 汇编(assembly)
  • 连接(linking)

预处理器和编译器正好对应了前两个步骤,编译器是将文本文件翻译成汇编语言程序,也就是把高级语言翻译为低级语言。而在C/C++中,预处理器的作用就只是最简单的进行文本替换、宏展开、删除注释这类的工作,它不会做任何的语法检查,不仅因为它不具备语法检查功能,也因为预处理命令不属于C/C++语句(定义宏的时候不需要加上分号这点就是很好的证明),通过预处理后,得到的仅仅是真正的源代码,其他的诸如语法检查之类的工作全都交给了编译器,当然,某些编程语言中的预处理器可能会更高级一点,但在这里我们就不做讨论了。

预处理器的如此的低能,叫我们怎么能放心地将完成代码这样的工作交给它?所以便有了“宁可以编译器替换预处理器”这样的观点,也正是《Effective C++》本章的主要内容:



2nd.尽量以const/enum/inline替换#define

#define不被视作语言的一部分,那正是它的问题所在。如果你做出了这样的事情:

#define PI 3.14159

PI这个记号名称大概从来不会被编译器看见,也或许在编译器开始处理源码之前就已经被预处理器移走了。这样的话它没办法进入记号表,于是当你某次因为使用PI而报错时,可能会带给你困惑,因为错误信息会提到3.14159而不是PI,如果PI被定义在一个非你所写的头文件里,你大概不会对3.14159有什么概念,呃……也许你会说我能猜出来这就是圆周率,那好,现在这个常量值被注释掉了,反而重新定义了一个NOT_PI,值为2.3333,一旦报错你还要对这个不明所以的数值瞎猜吗?这个问题也可能出现在记号调试器(symbolic debugger)中,原因相同:你所用的名称可能并未进入记号表(symbol table)。

总之,这种不明所以的error信息会对我们的debug造成困惑,解决之道是使用一个常量(const)替换这个宏(#define):

const double pi=3.14159;//大写名称通常用于宏,所以这里改变写法。

作为一个常量,pi肯定会被编译器看到,理所当然会进入记号表内。另外,对于浮点常量而言(比如这里的pi),使用常量生成的码比使用#define的码生成的码更少,因为预处理器盲目地将宏替换成所指定的值,导致目标码出现多份指定值,而常量绝对不会出现这样的情况。

当使用常量替换#define,有两种特殊的情况是必须要说的:

  • 定义常量指针
    常量定义式通常被放在头文件中,以便被不同的源码包含,因此有必要将指针(而不只是所指的对象)声明为const。例如:
const char* const name="my name is xxx";
  • class专属常量

    为了将常量的作用域限制于class内,你必须让它成为class的一个成员;而为确保此常量至多只有一份,你需要让它成为一个static成员:

class student
{
private:
static const int s_no=16001;
string name;
}

然而你所看到的是s_no的声明式而非定义式。通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static,且为整数类(ints,chars,bools),则特殊处理。只要不取它们的地址,你可以声明并使用它们而不需要定义式。但如果你取了某个class专属常量的地址,或者即使你没有这样做,而你的编译器却抽风似的硬是要看到一个定义式,你就必须要提供定义式如下:

const int student::s_no; 

//s_no的定义,下边解释为什么没给值

这个式子需要放进一个具体的实现文件而非头文件。由于class常量已在声明时获得初值(比如我们给了它16001),因此定义时可以不再设初值。

顺带一提,我们无法利用#define创建一个class 的专属常量,因为#defines并不重视作用域,一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味着#define不仅不能用来定义class的专属常量,也不能提供任何封装性,就像你从来没有看到过private #define 这样的东西。而const自然是可以被封装的,我们的s_no就是。

旧式编译器可能不支持这样的语法,它们不允许static成员在其声明式上获得初值。此外所谓的“in_class初值设定”也只允许对整数常量进行。如果你的编译器不支持的话,你可以将初值放在定义式内。

//位于头文件内
class student
{
private:
  static const double score;   //static class常量声明
  ...//省略了部分代码
}

//位于实现文件内
const double student::score = 96.5;   //static class常量定义

这几乎是你在任何时候唯一需要做的事情。当然,也有个唯一的例外:当你在class编译期间需要一个class常量值,比如:

class student
{
private:
static const int course = 5;
int scores[course];
}

编译器坚持在编译期间知道数组的大小,这个时候万一你的编译器抽风了,不允许“static整数class常量”完成 “in-class初值设定”,可以改用所谓的“the enum hack”。这个做法的理论基础是:
“一个属于枚举类型(enumerated type)的数值可以充当ints被使用”,于是代码可以变成如下内容:

class student
{
private:
enum {course=9};//令course成为9 的一个记号名称
int scores[course];//这样就没问题了
}

enum值得我们认识。第一个理由是enum的行为某些方面来说比较像#define而不像const,有时候你正需要这种。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法,如果你不想让别人获得一个指针(pointer)或者引用(reference)指向你的某个整数常量,enum可以帮你实现这个约束。此外,虽然优秀的编译器不会为整数型const对象设定额外的存储空间(除非你创建了一个pointer或reference指向该对象),不够优秀的编译器却可能如此,而你也许不想要这样的结果。enums和#define一样绝对不会导致非必要的内存分配。

第二个理由纯粹是为了实用。许多代码用了它,所以看到它的时候你得认识它。事实上,enum是模板元编程(template metaprogramming,还记得条款一中我们提到过这个吗?)的基础技术。

让我们把注意力重新转到预处理器上。

另一个常见的#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会有函数调用带来的额外开销。
下边这个例子是宏夹带着实参,调用函数f

define MAX(a,b) f((a)>(b)?(a):(b))

这类长相的宏有着太多缺点,光是想到就让人头疼。无论何时,当你写出这样的宏,你必须记住为宏中所有的实参带上小括号,否则某些人在表达式中调用这个宏的时候可能会遇到麻烦,事实上,即便你加上了括号,也能发生这样不可思议的事情:

int a=5,b=0;
MAX(++a,b);//a被累加二次
MAX(++a,b+10);//a被累加一次

在这里,调用f之前,a的递增次数竟然取决于它被拿来和谁比较?!

不过你不需要考虑这样无聊的事情,你可以用template inline函数来获得宏带来的效率以及一般函数的所有可预料行为和类型安全性。

template<typename T>
inline void max(const T &a,const T & b)
{
   f(a>b?a:b)
}

这个template产出一整群函数,每个函数都接受两个同类型对象,并以其中较大者调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算(求值)多次等等……此外,由于max是个真正的函数,它遵循作用域和访问规则。比如你可以写出一个class内的private inline函数,但宏无法完成此事。

有了const、enum和inline,我们对预处理器的需求降低了,但并非完全消除,毕竟#include仍然需要,而#ifdef/#ifndef也是不可或缺的重要角色。

  • 对于单纯常量,最好以const或enum替换#define
  • 对于形似函数的宏,最好改用inline函数替换#define
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值