Effective C++条款02:尽量以const,enum,inline替换#define

一、为什么不推荐使用#define

  • 在程序中你肯能会定义下面一个宏,来表示一个数值
#define ASOECT_RATIO 1.653

#define的缺点

  • ①宏的名称是不会被编译器看见的,因为在程序的预处理阶段,宏会被替换为其对应的数值/表达式,因此宏对于编译器来说是不存在“名称”这一概念的。
  • ②因为宏没有名称这一概念,因此宏的名称是不会进入符号表中(symbol table),宏在符号表中只是其对应的数值/表达式。于是当你调试时(例如使用gdb或其他调试工具),是看不到宏的名称的。例如上面的“ASOECT_RATIO”宏在符号表中只是一个数值1.653。因此当你在调试时如果你这个宏出现了问题,但是很难发现问题,因为其只是一个数字。如果这个“ASOECT_RATIO”宏定义在非你缩写的头文件中,那么就会更难发现问题了

二、以const替换宏

  • 例如下面定义一个常量来替换上面的宏:
const double AspectRatio = 1.653;

以const替换宏之后的好处

  • ①常量肯定是会被编译器看到的,因此也就会进入符号表中
  • ②减少了目标码:代码中用到宏“ASOECT_RATIO”的地方,都会被替换为1.653,因此会导致目标码增多。改用常量之后就减少了目标码

三、以const替换宏之后的两种特殊情况

①常量指针、指针常量、常量指针常量

  • 如果你是定义常量指针。由于常量定义通常被放在头文件中,因此有必要将指针声明为const
  • 例如如果要在头文件内定义一个常量的char*字符串,你必须写两次const:
const char* const authorName = "Scott Meyers";
  • 关于“常量指针、指针常量、常量指针常量”可以参阅:https://blog.csdn.net/qq_41453285/article/details/91659705
  • 关于const的使用,条款3会有完整的讨论
  • 当然,C++中的string对象会比char*更加合适,所以上面的authorName最好定义为如下的形式:
const std::string authorName("Scott Meyers");

②定义class的专属常量(类的静态成员)

  • 无法利用#define创建一个class专属常量,因为#define是不重视作用域的。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味着#defines不仅不能够用来定义class专属常量,也不能提供任何封装性(private、protected、public)
  • 下面你需要使用static为类创建一个常量。例如你在一个类中定义一个static成员:
class GamePlayer{
private:
    static const int NumTurns = 5; //常量声明式
    int scores[NumTurns];
}
  • class内部的NumTurns是声明而非定义(重点)。一般C++要求你为任何东西提供一个定义,但是如果其是class的静态成员且为整数类型(int、、char、bool等),则需要特殊处理
  • 只要不取它们的地址,你可以声明并使用它们而无须提供定义。但如果你取某个class专属常量的地址,那么你就必须在类外提供一个定义(重点),如下
/*如果把这个式子放在一个实现文件而非头文件。由于class常量已经
在class内部声明了初值(上面为5),因此定义的时候就不用设定初值了*/

const int GamePlayer::NumTurns; //NumTurns的定义

四、以enum替换#define

  • 在“三”中我们介绍了定义一个类静态成员。但是旧的编译器不支持上面的语法(即:在类内给成员赋初值),因此你需要在类内声明,在类外定义,例如下面的代码是对上面GamePlayer的改进
class GamePlayer{
private:
    static const int NumTurns; //常量声明式(通常位于头文件内)
    int scores[NumTurns];
}

GamePlayer::NumTurns=1.35;//定义(可以位于头文件/实现文件内)

为什么使用enum?

  • 虽然上面我们将NumTurns常量声明在类内,定义在类外。但是如果你的编译器在编译时告知不允许以静态成员NumTurns来初始化scores数组。那么需要使用enum来替换常量成员。例如,下面改写GamePlayer类
class GamePlayer{
private:
    enum { NumTurns = 5; }; //令NumTurns成为5的一个记号名称
    int scores[NumTurns];   //编译通过
}

使用enum的两个理由

  • enum的行为与const相比,其比较接近于#define。例如:
    • 不能取#define宏定义的地址,enum也不能
    • 不能获得一个指针/引用指向于某个整数常量,enum可以实现这个约束(见条款18对于“通过撰码时的决定实施设计上的约束条件”)
    • 有的编译器不会为“整型const对象”设定另外的存储空间(除非你创建一个指针或引用指向该对象),但是有的编译器却可能会。但是enum和#define就不会导致非必要的内存
  • ②enum更实用。许多代码用了它。事实上,enum是“模板元编程”的基础技术(见条款48)

五、以“inline函数”替换“#define调用函数”

先来看看一个使用#define调用函数带来的错误

  • 下面的宏调用了一个函数f
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
  • 但是我们以下面两种方式调用了宏,明显看到了错误之处
int a=5,b=0;

CALL_WITH_MAX(++a,b);   //a被累加2次
CALL_WITH_MAX(++a,b+10);//a被累加1次

以inline函数来替换上面的#define

  • 下面使用一个内联函数模板(见条款30)来替换上面的宏
template<typename T>
inline void callWithMax(const T& a,const T& b)
{
    f(a>b?a:b);//调用函数f
}

上面使用inline比#define的好处

  • ①使用#define时需要强烈注意"()"的使用,因此一不小心就会产生意想不到的结果,而inline函数就不需要
  • ②函数遵守作用域和访问规则,而#define没有。因此你也可以书写一个属于class的内联函数,而宏不可以

六、总结

  • 预处理器(特别是#define)并不是无用的。因为在很多地方还需要用到它们(例如#include包含头文件、#ifdef/#ifndef扮演者控制编译的角色等)
  • 请记住:
    • ①对于单纯常量,最好以const对象或enum替换#define
    • ②对于形似函数的宏,最好改用inline函数替换之
Effective C++条款02:尽量以const,enum,inline替换#defineEffective C++条款02:尽量以const,enum,inline替换#define 江南、董少 博客专家 发布了1384 篇原创文章 · 获赞 930 · 访问量 29万+ 他的留言板 关注
上一篇:在线实例理解 C++ 中的枚举类型 Enum


下一篇:for 循环在C++11中的新用法