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

Item 02: Prefer const,enums,and inlines to #define


本条款或许改为“宁愿选择编译器而不是预处理器”。

宏与常量

当你写出这样的代码的时候:

#define ASPECT_RATIO 1.653

可能会带来一些麻烦。
解决之道是以一个常量替换上述的宏(#define):

const double AspectRatio = 1.653

作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。此外对浮点常量而言,使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称ASPECT_RATIO替换为1.653”可能导致目标码出现多分1.653,若改用常量AspectRatio绝不会出现相同的情况。

当我们以常量替换#define,有两种特殊情况值得说一下。

常量指针

由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(包括指针所指之物)声明为const。例如若要在头文件内定义一个常量的(不变的)char*-based字符串,你必须写const两次:

const char* const authorName = “Hello”;

这里string对象通常比其前辈char*-based适合,所以上述的authorName定义成这样更好些:

const std::string authorName("Hello");

class专属常量

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

class GamePlayer{
private:
    static const int NumTurns = 5;  //常量声明式
    int scores[NumTurns];           //使用该常量
}

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

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

请把这个式子放进一个实现文件而非头文件。由于class常量已在声明时获得初值,因此定义式不可以再设初值。

来看一下我们的测试例子:

#include <iostream>
using namespace std;

class Shape {
public:
    static const int NumTurns = 5;  //常量声明式
    int scores[NumTurns];           //使用该常量
};

//const int Shape::NumTurns;

int main()
{
    cout << Shape::NumTurns << endl;    //输出为5,无编译警告错误
    return 0;
}

但是我们改为

int main()
{
    cout << &Shape::NumTurns << endl;   //error: undefined reference to `Shape::NumTurns'
    return 0;
}

然后,我们再改:

const int Shape::NumTurns;

int main()
{
    cout << &Shape::NumTurns << endl;   //输出0x404064,无编译警告错误
}

然后,我们再改:

const int Shape::NumTurns = 10;

int main()
{
    cout << Shape::NumTurns << endl;    //error: duplicate initialization of 'Shape::NumTurns'
}

我们无法利用#define创建一个class专属常量,因为#define并不重视作用域。一旦宏被定义,它就在气候的编译过程中有效(除非在某处被#undef)。这就意味着#defines不仅不能够用来定义class专属常量,也不能够提供任何封装性,也就是说没有所谓private#define这样的东西。

旧式编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初值。此外所谓的“in-class初值设定”也只允许对整数常量进行。所以我们最好的做法是把初值放在定义式:

class CostEstimate{
private:
    static const double FudgeFactor;    //static class常量声明
    ...                                 //位于头文件内
};
const double CostEstimate::FudgeFactor = 1.35//static class常量定义位于实现文件内

enum hack

上述做法看起来已经非常完美了。但唯一例外是当你在class编译期间需要一个class常量值,例如在上述的GamePlayer::scores的数组声明式中(编译器坚持必须在编译期间知道数组的大小)。这时候万一你的编译器不允许“static整数型class常量”完成“in class初值设定”,可改用所谓的“the enum hack”补偿做法。其理论基础是:“一个属于枚举类型的数值可权充ints被使用”,于是GamePlayer可定义如下:

class CostEstimate{
private:
    enum { NumTurns = 5};   //"the enum hack"————令NumTurns成为5的一个记号名称
    int scores[NumTurns];
    ...
};

enum hack的行为某方面来说比较像#define而不像const。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或reference指向你的某个正数常量,enum可以帮助你实现这个约束。Enums和#defines一样绝不会导致非必要的内存分配。“enum hack”是模板元编程的基础技术。

所谓的“宏函数”

另一个常见的#define误用情况是以它实现所谓的“宏函数”,宏看起来像函数,但不会招致函数调用带来额外的开销。

//以a和b的较大值调用f
#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a) : (b))

这种宏有太多缺点,无论何时当你写出这种宏,你必须记住为宏中的所有实参加上小括号,否则某些人在表达式中调用这个宏时可能会遭遇麻烦。但即时你为你所有实参加上小括号,看看下面不可思议的事情:

int a = 5, b = 0;
CALL_WITH_MAX(++a, b);      //a被累加两次,f((++a)>(b)?(++a):(b));
CALL_WITH_MAX(++a, b + 10)  //a被累加一次,f((++a)>(b+10)?(++a):(b+10));

在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”。
有什么好的替换方法呢?

模板内联函数

通过模板内联函数你可以获得宏带来的效率以及一般函数的所有预料行为和类型安全性。

template <typename T>   //由于我们不知道T是什么,所以采用pass by reference-to const
inline void callWithMax(const T& a, const T& b)
{
    f(a > b ? a : b);
}

这个模板产出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用f。这里不需要在函数本体中为每个参数加上括号,也不需要操心参数被求值多次……。此外,由于callWithMax是个真正的函数,它遵守作用域和访问规则。例如你绝对可以写出一个“class内的private inline函数”,一般而言宏无法完成此事。

有了consts、enums和inlines,我们对预处理器的需求降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮演控制编译的重要角色。

NOTE:

  • 对于单纯常量,最好以const对象或enums替换#defines.
  • 对于“宏函数”,最好改用inline函数替换#defines.

Effective C++

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值