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

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

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


这个条款或许改为“宁可以编译器替换预处理器”比较好,因为或许#define 不被视为语言的一部分。那正是它的问题所在。


问题产生


例如下面代码:


#define ASPECT_RATIO 1.653


记号名称ASPECT_RATIO也许从未被编译器看见;


也许在编译器开始处理源码 之前它就被预处理器移走了。


于是记号名称ASPECT_RATIO有可能没进入记号表 (symbol table)内。


于是当你运用此常量但获得一个编译错误信息时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是ASPECT_RATIO。如果 ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定对1.653以及它来自何 处亳无概念,于是你将因为追踪它而浪费时间。


这个问题也可能出现在记号式调试器(symbolic debugger),原因相同:你所使用的名称可能并未进入记号表(symbol table)


解决方法


解决之道是以一个常量替换上述的宏(#define):


const double AspectRatio = 1.653; // 大写名称通常用于宏,
                  // 因此这里改变名称写法。


作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入记号表内。


此外对浮点常量(floating point constant,就像本例)而言,使用常量可能比使 用#define导致较小量的码,因为预处理器"盲目地将宏名称ASPECT_RATIO替换 为1.653"可能导致目标码(object code)出现多份1.653,若改用常量AspectRatio 绝不会出现相同情况。


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


替换#defines的两种特殊情况


1. 定义常量指针 (constantpointers)


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


const char* const authorName = "Scott Meyers";


关于const的意义和使用(特别是当它与指针结合时),条款3有完整的讨论。 这里值得先提醒你的是,string对象通常比其前辈char*-based合宜,所以上述的 authorName往往定义成这样更好些:


const std::string authorName("Scott Meyers");


2. 定义class专属常量


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


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


然而你所看到的是NumTurns的声明式而非定义式。


通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类 型(integral type,例如ints, chars, bools),则需特殊处理。只要不取它们的地址, 你可以声明并使用它们而无须提供定义式。


但如果你取某个class专属常量的地址, 或纵使你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:


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


此时我们看到 NumTurns是没有赋值的,下面告诉你为什么没有给予数值:


请把这个式子放进一个实现文件而非头文件。由于class常量己在声明时获得初值(例如先前声明NumTurns时为它设初值5),因此定义时不可以再设初值。


顺带一提,请注意,我们无法利用#define创建一个class专属常量,因为 #defines并不重视作用域(scope)。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef ) 这意味#defines不仅不能够用来定义class专属常量, 也不能够提供任何封装性,也就是说没有所谓private #define这样的东西。而当然 const成员变量是可以被封装的,例如NumTurns就是。


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


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


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


Class GamePLayer{
private:
  enum{ NumTurns = 5 };   // the enum hack——令NumTurns
                // 成为5的一个记号名称
  int score[NumTurns];    // 这就没问题了             
  ...
};


认识enum的理由


基于数个理由enum hack值得我们认识。


行为


第一,enum hack的行为某方面说比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。


如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。(条款18对于“通过撰码时的决定实施设计上的约束条件” 谈得更多。)


此外虽然优秀的编译器不会为"整数型const对象”设定另外的存储空间(除非你创建一个pointer或reference指向该对象),不够优秀的编译器却可能如此,而这可能是你不想要的。enum和#defines 一样绝不会导致非必要的内存分配。


实用


认识enum hack的第二个理由纯粹是为了实用主义。许多代码用了它,所以看到它时你必须认识它。事实上"enum hack"是template metaprogramming (模板元编程,见条款48)的基础技术。


把焦点拉回预处理器。另一个常见的#define误用情况是以它实现宏(macros ) 宏看起来像函数,但不会招致函数调用(functioncall)带来的额外开销。下面这个 宏夹带着宏实参,调用函数f:


〃以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 被累加二次
CALL_WITH_MAX(++a, b+10); // a 被累加一次


在这里,调用f之前,a的递增次数竟然取决于“它被拿来和谁比较”!这简直太扯了。

幸运的是你不需要对这种无聊事情提供温床。你可以获得宏带来的效率以及一 般函数的所有可预料行为和类型安全性(type safety)——只要你写出


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


这个template产出一整群函数,每个函数都接受两个同型对象,并以其中较大者调用f。这里不需要在函数本体中为参数加上括号,也不需要操心参数被核算(求 值)多次……等等。


此外由于callWithMax是个真正的函数,它遵守作用域(scope) 和访问规则。例如你绝对可以写出一个"class内的private inline函数”。一般而言 宏无法完成此事。


有了 consts、enums和inlines,我们对预处理器(特别是#define)的需求 降低了,但并非完全消除。#include仍然是必需品,而#ifdef/#ifndef也继续扮 演控制编译的重要角色。目前还不到预处理器全面引退的时候,但你应该明确地给 予它更长更频繁的假期。


总结:


  • 对于单纯常量,最好以const对象或enums替换#defines。


  • 对于形似函数的宏(macros),最好改用inline函数替换#defines


相关文章
|
6月前
|
C++
C++中的const指针与const引用
C++中的const指针与const引用
77 2
|
5月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第一章 笔记
|
6月前
|
编译器 C++
C++中的内联函数与const限定词的使用
C++中的内联函数与const限定词的使用
39 1
|
5月前
|
编译器 C++
【C++】类和对象④(类的默认成员函数:取地址及const取地址重载 )
本文探讨了C++中类的成员函数,特别是取地址及const取地址操作符重载,通常无需重载,但展示了如何自定义以适应特定需求。接着讨论了构造函数的重要性,尤其是使用初始化列表来高效地初始化类的成员,包括对象成员、引用和const成员。初始化列表确保在对象创建时正确赋值,并遵循特定的执行顺序。
|
5月前
|
存储 安全 API
C++一分钟之-C++中的枚举类型(enum class)
【6月更文挑战第25天】C++的`enum class`(强类型枚举)在C++11中引入,增强了枚举的作用域和类型安全,减少命名冲突。它要求使用全名(如`Color::Green`)访问枚举成员,并能显式指定底层类型。常见问题包括默认值非0、隐式转换和范围溢出,解决办法是明确赋值、显式转换和选择合适底层类型。高效技巧包括用于状态机、作为函数参数、创建别名和迭代。掌握这些能提升代码质量。
167 0
|
5月前
|
编译器 C++
【C++】:const成员,取地址及const取地址操作符重载
【C++】:const成员,取地址及const取地址操作符重载
43 0
|
5月前
|
编译器 C++
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
《Effective C++ 改善程序与设计的55个具体做法》 第二章 构造/析构/赋值运算 笔记
|
9天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
36 4
|
10天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
33 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4