关于EffectIve C++ 的总结(侯捷翻译版)

时间:2022-08-29 19:48:20

C++ 提供四种不同而有相辅相成的编程范型(programming paradigms) 如下:

(1)procedural based 

(2)object based

(3)object oriented

(4)generics


item1: 将C++视为一个语言联邦

这个语言联邦有四个“州”(即C++包含这四个部分):

(1)C, C++ 起源于C, 一开始只是在C的基础上加上了一些面向对象的特性。 C++ 的最初名称为C with classes。 发展到今天, C 语言没有的特性如下: 没有模板(template), 没有异常(exception), 没有重载(overloading)等等。

(2)object-oriented C++。 这是C with class所诉求的。 classes, 封装(encapsulation), 继承(Inheritance), 多态(Polymorphism), Virtual 函数(动态绑定)等等。 这是面向对象的。

(3)Template C++. 这是C++ 泛型编程的部分。 模板很强大。

(4)STL. STL 是一个template程序库。 具有容器, 迭代器, 算法, 函数对象。


Item 2: 尽量使用const, enum, inline, 用于替换由#define的macro(宏)

这个条款的意思是“宁可以编译器替换预处理器”。 因为#define 不被视为语言的一部分。 例如:

#define ASPECT_RATIO  1.653

记号 ASPECT_RATIO 并不被我们的Compiler看见。 编译器在处理我们的source code的之前, 记号ASPECT_RATIO 就被预处理器移走了。 ASPECT_RATIO 就没有进入Compiler 用于记录源程序的记号表中(symbol table).  所以出现错误的时候。 错误信息会提到1.653而不是ASPECT_RATIO。 如果ASPECT_RATIO被定义在一个非你所写的头文件中, 我们可能对于1.653 毫无概念。 调试起来也很费时间。 

解决之道是用常量替换上述的宏(#deine):

const double AspectRation = 1.653; // 大写名称常用于宏, 所以这里改变名称写法
作为语言常量, AspectRatio 肯定会被编译器看到。 当然也会进入几号表内。 另外, 如果使用const (常量)代替#define 宏的另外一个好处是导致内存小。 因为预处理器会盲目的将ASPECT_RATIO 替换为1.653, 这样我们的object code 就会有多份1.653。 但是对于常量Aspect_Ratio , 这是绝对不会发生的。  因为如果我们的含有常量Aspect_Ratio 定义的头文件被多个source file 包含的时候, 我们的编译器会产生对这个常量重复定义的错误, 即 a linker error from multiple definitions of the symbol。  但是#define的头文件被多个源文件包含是, 编译却不会发生错误。 通常宏#define 的内存为: 宏的大小 x 包含含有宏的头文件的源文件个数。


创建class的专属常量。 也就是说将常量的作用域(scope)限制于class内, 你就必须让常量称为class的一个member。 为了确保常量至多只有一份实体, 必须将其声明为static 成员。

例如:

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



注意, 上面的NumTurns 是声明式, 而非定义常量的式子。 

通常C++ 要求你对你所使用的任何东西提供一个定义式。 但是如果它是一个class的专属常量, 又是static,并且为整形类型(Integral type, 例如int, char, bool), 则需要特别处理。 只要不取用它的地址, 我们可以声明而无需提供定义式。 但是如果你取某个class的专属常量的地址, 或者总是你不取, 但是你的编译器却(不正确的)坚持要看到一个定义式, 此时, 你就必须另外提供定义式如下:

const int GamePlayer::NumTurns; //NumTurns 的定义, 下面告诉你为什么没有给与数值
上述的这个式子应该放在实现文件中, 而非头文件中。 由于class的常量已经在声明的时候获得初值(为5), 所以定义的时候不可以再设初值。

我们无法使用#define 去创建一个class的专属常量。 因为一宏被定义, 它在其后的编译过程中有效(除非在某处被# undef)。 这意味着宏无法提供封装性。


旧式编译器一般不支持上述的语法。 如果你的编译器不支持, 你可以将初值放在定义式中:


class CostEstimate {
   private:
      static const double FudgeFactor; // 常量声明式
      ...
}; //  位于头文件中


const double CostEstimate::FudgeFactor = 1.35; // 放置在实现文件中

 这几乎是你在任何时候唯一需要做的事。 唯一的例外是, 当你的class在编译期间需要一个class的常量值。

例如, 在上述的GamePlayer::scores的数组声明式中(编译器坚持在编译期间知道数组的大小(除非动态分配的动态数组))。 这时候万一你的编译器(错误的)不允许“static const” 常量无法完成“in class 初值设定”, 那么可以改用the enum hack 的补偿做法。 

这样做的理论基础是: 一个属于枚举类型的数值可充当ints 被使用。 这样定义如下:

 

class GamePlayer {
   private:
      enum{NumTurns = 5}; 
int scores[NumTurns]; 
};

enum hack 的认识如下:

取一个const 的地址合法, 但是取一个enum 的地址是不合法的, 而去一个#define 的地址也是不合法的。


现在看看预处理器。

一个常见的#define的误用情况是依他实现宏(macros)。

宏看起来像函数, 但是不会招致函数调用(function call )带来的额外开销。

宏有很多的缺点, 想想就让人痛苦。

无论何时, 定义宏的时候, 需要在宏中将所有的实参奖赏小括号。 否则别人调用起来就麻烦了。 幸运的是, 我们可以写出template inline 函数, 一便获得宏的效率的同时, 由能够避免宏的缺点:

template <class T>
inline void callWithMax(const T&a, const T& b) {
   f(a>b ? a:b);
}
这样决定较大者调用函数f。


有了const, enum, inline, 我们对预处理器的需求降低了。 但是#include 仍然必须, 而#ifndef等也继续扮演着控制编译的重要的角色。 

但是请记住:

(1)对于单纯的常量, 最好以const 对象或者enum 去替换 #define。

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







class CostEstimate { private: static const double FudgeFactor; // 常量声明式...}; // 位于头文件中const double CostEstimate::FudgeFactor = 1.35