Fork me on GitHub

读书笔记 effective c++ Item 2 尽量使用const,枚举(enums),内联(inlines),不要使用宏定义(define)

这个条目叫做,尽量使用编译器而不要使用预处理器更好。#define并没有当作语言本身的一部分。

例如下面的例子:

1 #define ASPECT_RATIO 1.653

符号名称永远不会被编译器看到。它可能在源码到达编译器之前被预处理器移除。ASPECT_RATIO 最终不会进入符号表,如果因为这个常量的使用而导致编译错误,会使你非常迷惑,因为错误信息会指向1.653而不是ASPECT_RATIO。如果ASPECT_RATIO被定义在一个不是你自己写的头文件中,你会不知道1.,653来自哪里。,

解决方法是将宏替换成常量:

1 Const double AspectRatio = 1.653

作为一个语言常量,AspectRatio能够被编译器看到,编译器也肯定能进入到AspectRatio的符号表中。此外,对于浮点常量来说,使用常量比使用宏定义会产生更少的代码。因为预处理器会盲目的将宏定义名称ASPECT_RATIO替换成1.653,这会造成在目标码中1.653的多份拷贝,而常量的使用最多产生一份拷贝。

当用常量替换宏定义的时候,有两种特殊情况值得提一下:

第一种是定义常量指针,因为常量定义会被放到头文件中,很多文件会包含这个头文件,将指针声明成常量,同时将指针指向的内容也声明成常量。为了在头文件中定义一个基于char*的字符串,必须写const两次:

1 Const char* const authorname = “Scott”

在这里有必要提醒一下使用string对象要优于基于char*的字符串,所以将authorname定义成如下方式更好:

1 const std::string authorname(“Scott”);

第二种特殊的情况是关于类中指定的常量。为了将常量的作用域限制在类中,必须将其声明成一个成员,为了保证至多只有一份常量的拷贝,你必须将其声明成static 成员:

1 Class GamePlayer{
2 
3 Private:
4 
5 Static const int NumTurns = 5;
6 
7 Int scores[NumTurns];
8 
9 }

上面看到的是NumTurns的声明而非定义,c++需要你为你所使用的任何东西(anything)提供一份定义,但是类专属的静态整型常量(intergers,chars,bools)是一个例外,只要你不使用他们的地址,你可以声明并且使用他们而不用提供一个定义。如果你需要取得类专属常量的地址或者你所使用的编译器错误的坚持类专属常量需要一个定义(即使不需要获取地址),你需要提供一个单独的定义:

1 Const int GamePlayer::NumTurns;

你需要把定义放到实现文件而不是头文件中。因为类专属对象的初始值是在声明时提供的,不允许在定义的时候对其进行初始化。

顺便说一句,不可以使用宏定义为类定义专属常量,因为宏定义没有作用域。一旦一个宏定义被定义,它就在余下的编译中有效(只要它没有被undefed)。这意味者宏定义不能当作类专属常量,它们也不能用来提供任何类型的封装,例如,没有私有的#define.

旧的编译器也许不会接受上面的语法,因为在过去,为静态类成员在声明处提供初始值是非法的,此外,只允许整型和常量进行类内部的初始化。一旦上面的语法不能用了,你需要把初始化值放在定义处。

1 Class costEstimate{
2 
3 Private:
4 
5 Static const double FudgeFactor;
6 
7 }
8 
9 Const double costEstimate::FudgeFactor=1.35;

这是你任何时候需要做的,唯一的例外是在类编译过程中你需要一个类常量值,例如在类中声明一个数组,在编译过程中需要知道数组的大小。这时候在类内部为静态整型常量值指定初始值是被禁止的(这是不正确的),补偿的做法是使用”enum hack”.这种技术利用了一个事实:枚举类型的值可被用在需要整型值的地方,所以可以如下定义:

 1 Class Gameplayer
 2 
 3 {
 4 
 5 Private:
 6 
 7 Enum{NumTurns=5};
 8 
 9 Int scores[NumTurns];
10 }

Enum hack技术值得被了解,有以下几个原因:

第一,  enum hack的行为在一些情况下更像宏定义而不是const,有时候这也是你所需要的。例如:取得const的地址是合法的,但获取枚举的地址是不合法的,同样的,获取宏定义的地址是不合法的。如果你不想让其他人获取指向整型常量的指针或者引用,枚举是进行这种约束的一个好的方法。同样,虽然好的编译器不会为整型常量分配额外的空间,一个草率的编译器可能会这么做,这是你不愿意看到的。像宏定义一样,枚举永远不会产生这样的不必要的内存分配。

第二,  enum hack是很实用的技术,很多代码都会使用到它,因此当你看到它你应给能够识别出来。事实上,enum hack是模板元编程的基本技术。

回到预处理器,#define的另外一个用法是实现一个看上去像函数的宏,但对其调用不会招致额外开销。下面是一个取最大值的例子:

1 #define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

这样的宏有许多缺点,想想都头疼。

当你实现这类宏时,你必须记住对宏定义体中的所有参数都要加上括号,否则别人在表达式中调用宏的时候会遇到 麻烦。但是即使你那么做了,你仍然会遇到奇怪的事情

1 int a = 5, b = 0;
2 CALL_WITH_MAX(++a, b); // a is incremented twice
3 CALL_WITH_MAX(++a, b+10); // a is incremented once

这里a加一的次数取决于a和一个多大的数进行比较。

幸运的是,你可以不必忍受这么无聊的事情。你可以通过定义一个内联函数模板来获得宏定义函数所有的效率并且可预知函数的所有行为,函数也是类型安全的。

1 template<typename T>                                                // because we don’t
2 inline void callWithMax(const T& a, const T& b) // know what T is, we
3 
4 {
5 
6   F(a>b?a:b);
7 }

这个宏定义会产生一个函数族,每个函数将相同类型两个对象作为参数,其中较大的调用f,不必给函数体内部的参数加括号,也不必担心参数会被求值多次。并且因为callWithMax是一个函数,它遵循作用域和访问规则。比如,你可以写出一个类的私有内联函数。宏定义却不能够做到。

鉴于consts,enums和inlines的实用性,你可以减少预处理器的使用,但是它并没有被清除,#inlcude仍然是必要的,#ifdef和#ifndef在编译控制上仍然发挥重要作用,还没有到让预处理器退休的时候,但是你绝对可以给它放一个长长的假期。

 

posted @ 2017-02-08 07:53  HarlanC  阅读(1050)  评论(0编辑  收藏  举报