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



    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


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

      "这个条款或许改为”宁可以编译器替换预处理器“比较好。如:

    #define PI 3.14
    
    • 1

       因为你使用宏定义时,它也许并不会被编译器看见,其可能在编译器开始处理源码之前就被预处理器移走了。

    1 宏定义缺点:

    1. 宏的名称是不会被编译器看见的,因为在程序的预处理阶段,宏会被替换为其对应的数值/表达式。
    2. 宏定义只做替换,不做类型检查和计算,也不求解,容易产生错误。
    3. 宏定义只做替换,没有名称这一概念,因此宏的名称是不会进入记号表中(symbol table),宏在符号表中只是其对应的数值/表达式。于是当你调试时(例如使用gdb或其他调试工具),是看不到宏的名称的。例如上面的“PI”宏在符号表中只是一个数值3.14。因此当你在调试时如果你这个宏出现了问题,但是很难发现问题,因为其只是一个数字。如果这个“PI”宏定义在非你缩写的头文件中,那么就会更难发现问题了,因为你将会花费时间去追踪它,但它们都有共同的原因:你使用的名称可能并未进入记号表。

    2 解决之道

    2.1 const 替换宏

       如,我们可以使用 const 定义一个常量来替换宏:

    const double PI= 3.14;
    
    • 1

    const替换#define之后的好处:

    1. 做为一个语言常量,它肯定是会被编译器看到的,当然就会进入记号表中
    2. 减少了目标码:代码中用到宏“PI”的地方,都会被替换为3.14,因此会导致目标码增多,存在多份3.14,改用常量之后绝对不会出现这种情况。
    3. const 常量有数据类型,编译器可以对其进行类型安全检查。
    以const替换宏之后的两种特殊情况

    1. 定义常量指针

    定义常量指针。由于常量定义通常被放在头文件中,因此有必要将指针(而不是指针所指之物)声明为const;
    例如如果要在头文件内定义一个常量的char* 字符串,你必须写两次const:
    const char* const authorName = "Scott Meyers";
    关于const的使用,条款3会有完整的讨论
    当然,C++中的string对象会比char*-based更加合适,所以上述的authorName最好定义为如下的形式:
    const std::string authorName("Scott Meyers");

    2. 定义class专属常量

    为了将常量的作用域(scope)限制于class内,你必须让它成为class的一个成员(member);而为确保此常量至多只有一份实体,你必须让它成为static成员;
    下面你需要使用static为类创建一个常量。例如你在一个类中定义一个static成员:

    class GamePlayer { 
    private:
        static const int NumTurns = 5; //常量声明式
        int scores[NumTurns]; //使用该常量
        ...
        };  
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    上述你看见class内部的NumTurns是声明而非定义。通常 C++ 要求你为任何东西提供一个定义,但是如果它是 class 的静态成员(class专属常量又是static)且为整数类型(int、、char、bool等),则需要特殊处理;
    只要不取它们的地址,你就可以声明并使用它们而无须提供定义。但如果你取某个 class 专属常量的地址,那么你就必须在类外提供一个定义(重点),如下:
    const int GamePlayer::NumTurns; //NumTurns的定义
    倘若把这个式子放在一个实现文件而非头文件。由于class常量已经在class内部声明了初值(上面为5),因此定义的时候就不用设定初值了。

    NOTE:无法利用#define创建一个class专属常量,因为#defines 并不重视作用域。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味着#defines不仅不能够用来定义 class 专属常量,也不能提供任何封装性(private、protected、public),而当然const 成员变量可以被封装的,NumTurns就是。

    2.2 enum替换#define

      前面我们介绍了定义一个类静态成员。但是旧的编译器不支持上面的语法,它们不允许static成员在其声明式上获取初值(即:在类内给成员赋初值),因此你需要在类内声明,在类外定义,你可以将初值放在定义式,如:

    class GamePlayer{
    private:
        static const int NumTurns; //常量声明
        int scores[NumTurns];
    }
     
    GamePlayer::NumTurns=1.35;//实现文件内
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

      虽然上面我们将 NumTurns 常量声明在类内,定义在类外。但是如果你的编译器在编译时告知不允许以静态成员 NumTurns 来初始化scores数组。那么需要使用enum来替换常量成员。如:

    class GamePlayer{
    private:
        enum { NumTurns = 5; }; //令NumTurns成为5的一个记号名称
        int scores[NumTurns];   
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

      这里的理论基础是,一个属于枚举类型的数值可权充ints被使用。

    使用enum的理由:

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

    2.3 “inline函数”替换“#define调用函数”

      一个#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会带来函数调用的额外开销。如,下面的宏调用了一个函数f:

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

    明眼就能看出来其缺点众多,看看下面不可思议的事情:

    int a=5,b=0;
     
    CALL_WITH_MAX(++a,b);   //a被累加2次
    CALL_WITH_MAX(++a,b+10);//a被累加1次
    
    • 1
    • 2
    • 3
    • 4

    这里,调用f前,a的递增次数居然取决于它和谁比较,这想想非常糟糕。

    如果用 inline 函数来替换上面的 #define 这就变得简单多了,下面我们使用一个内联函数模板(见条款30)来替换上面的宏,使得代码更易懂,不混乱

    template<typename T>
    inline void callWithMax(const T& a,const T& b)
    {
        f(a>b?a:b);//调用函数f
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    使用inline比#define的好处

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

    小结

      一个事物存在即有它的用处,好比自天生我才必有用,虽然通过上述我们对预处理器的需要降低了,但它(特别是#define)并不是无用的。因为在很多地方还需要用到它们(例如#include包含头文件、#ifdef/#ifndef扮演者控制编译的角色)。

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    【PHP】入门知识
    Maven3.8.1下载、配置本地仓库、阿里云镜像、JDK版本
    node篇 CommonJS规范
    PHP获取自然周日期(周一~周日)
    HDR相关文章收集
    数字音频矩阵
    核货宝:服装店收银系统如何选择?收银系统选购指南!
    go 并发
    无线振动传感器在热电厂设备状态监测中的应用
    Go 1.19 发行说明(翻译)
  • 原文地址:https://blog.csdn.net/CltCj/article/details/127813448