条款1:视C++为一个语言联邦
这句话是从c++支持特别多的语言形式来说的,首先它支持过程形式,其次支持面向对象形式(主体),函数形式,泛型形式,元编程形式。这些能力和弹性使C++成为一个无可匹敌的工具。
C++作为总语言可以细分为下面四个分语言:
* C语言 C相比于c++的局限:没有模板,没有异常,没有重载。
* Object-Orriented C++
* Template C++ 由于templates威力强大,它们带来的崭新的编程规范,即所谓的 模板元编程(TMP:template metaprogramming)
* STL
条款2:尽量以const,enum,inline替换#define
首先你要知道#define并不被视为语言的一部分,这将导致一些问题:
#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见;也许在编译器开始处处理源代码之前就背预处理器移走了。于是记号名称ASPECT_RATIO有可能没有进入符号表(symbol table)内。于是当你运用此常量但获得一个错误信息时,可能带来困惑,因为错误信息也许会提到1.653而不是ASPECT_RATIO。如果ASPECT_RATIO被定义在一个非你所写的头文件内,你肯定对1.653以及它来自何处毫无该奶奶,于是你将因为追踪它而浪费时间。原因相同:你所使用的名称可能并未进入符号表。
解决办法是定义如下代码替换之:
const double Aspectratio = 1.653 ;
作为一个语言常量,AspectRatio肯定会被编译器看到,当然就会进入符号表内。另外对浮点常量(如本例),使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称ASPECT_RATIO替换为1.653”可能导致目标码出现多份1.653,若改为常量AspectRadio绝不会出现相同情况。
如果你想写一个字符串常量,可以如下写:
const char* const authorName = “Jack.shi” ;
因为string对象通常比其前辈char*-based合宜,所以上述的authorName往往定义成下面这样:
const std::string authorName("Jack.shi") ;
第二个值得注意的地方时class专属常量。为了将常量的作用域限制于class内,你必须让它成为一个成员;而为确保此常量至多只有一份实体,你必须让它成为一个static成员:
class GamePlayer
{
private:
static consts int NumTurns = 5 ;//常量声明式
int scores[NumTurns] ;// 使用该常量
...
}
然而你所看到的是NumTurns的声明式而非定义式。通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class的专属常量有是static且为整数类型(如ints,chars,bools),则需要特殊处理。只要不取它们的地址,你可以声明并使用它们并无需提供定义式。但如果你取某个专属常量的地址,或总是你不取其地址而你的编译器却(不正确地)坚持要看到一个定义式,你就必须另外提供定义式如下:
const int GamePlayer::NumTurns ;// NumTurns的定义,下面告诉你为什么这里没有赋予数值
请把这个式子放进一个实现文件而非头文件。由于class常量已在声明时获得初值(例如先前声明的NumTurns它的初值设为5),因此定义时不可以再设为初值。
顺带一提,请注意,我们无法利用#define创建一个class专属常量,因为#defines并不重视作用域。一旦宏被定义,它就在其后的编译过程中有效(除非在某处被#undef)。这意味着#defines不仅不能用来定义class专属常量,也不能够提供任何封装性,NumTurns就是。
旧式编译器也许不支持上述语法,它们不允许static成员在其声明式上获得初值。此外所谓的“in-class初值设定”也只允许对整数常量进行。如果你的编译器不支持上述语法,你可以将初值放在定义式:
// 头文件 class CostEstimate { private: static const double FudgeFator ; ... } // 实现文件 const double CostEstimate::FudgeFactor = 1.35 ;
当你在class编译期间需要一个class常量值,例如在下述的 GamePlayer::scores 的数组声明式中(是的,编译器坚持必须在编译期间知道数组的大小)。这时候万一你的编译器(错误地)不允许 "static整数型class常量"完成"in class初值设定",可改用所谓的"the enum hack"补偿做法。其理论是:"一个属于枚举类型的数值可权充ints被使用",于是GamePlayer可定义如下:
class GamePlayer { private: enum {NumTurns = 5}; int scores[NumTurns] ;// "the enum hack"--令NumTurns成为5的记号名称,这就没有问题了 } ;
基于数个理由enum hack值得我们认识。第一,enum hack 的行为某方面说比较像#define而不像const,有时候这正是你想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你实现这个约束。此外虽然优秀的编译器不会为“整数型const对象“设定另外的存储空间(除非你创建一个pointer或reference指向该对象),不够优秀的编译器却可能如此,而这这可能是你不想要的。Enums和#defines一样绝对不会导致非必要的内存分配。
认识enum hack的第二个理由纯粹是为了使用主义。许多代码用了它,所以看到它时你必须认知它。事实上"enum hack"是template metaprogramming(模板元编程)的基础技术。
让我们回到#define,因为#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被累加两次
CALL_WITH_MAX(++a, b + 10) ; // a被累加一次
在这里,在调用f之前,a的调用次数居然取决于”它拿来和谁比较“!
幸运的是你不需要为这种无聊的事情提供温床。你可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性--只要你写出template inline函数
template<typename T> inline void callWithMax(const T& a,constT& b)// 由于我们不知道T是什么,所以采用 pass by reference-to-const { f (a > b ? a:b) ; }
有了consts,enums和inlines,我们对#define的需求降低了,但并非完全消除。#include仍然是必需品,而 #ifdef/#ifndef也继续扮演控制编译的重要角色。目前还不知道预处理全面引退的时候,但你应该明确地给予它更长更频繁的假期。
请记住:
* 对于单纯常量,最好以const对象或enums替换#defines。
* 对于形似函数的宏,最好改用inline函数替换#defines。