Effective C++ (札记) : 条款01 - 条款04

Effective C++ (笔记) : 条款01 -- 条款04

条款01:视C++为一个语言联邦

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

"宁可以编译器替换预处理器",使用#define那么在编译器之前预处理器已经完成了替换,记号名称没有进入记号表(symbol table),所以在调试或者编译错误的时候会莫名其妙。以常量替换宏是个好主意:const double AspectRatio = 1.653替换#define ASPECT_RATIO 1.653

如果你不想让别人获得一个指针或者引用指向你的某个整数常量,enum可以实现这个约束。

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

这种长相的宏,看着就头疼。可使用template inline替换:

  1. template<typename T>
  2. inline void callWithMax(const T &a, const T &b){
  3. f(a > b ? a : b)
  4. }

对于单纯常量,最好以const对象或enum替换#define。 
对于形似函数的宏,最好使用inline函数替换#defines

条款03:尽可能使用const

const修饰左侧的对象,如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在星号两边,表示被指物和指针两者都是常量。

STL迭代器是以指针为根据塑模出来的。所以迭代器的作用像是个T *指针。声明迭代器为const就像声明指针为const一样(T * const),表示这个迭代器不能指向不同的东西(位置),但是它指向的东西是可以改变的。如果你希望迭代器所指的东西不能改变(const T *),需要使用const_iterator。这两者是有差别的,比较如下:

  1. vector<int> vec;
  2. const vector<int>::iterator iter = vec.begin(); // T* const
  3. *iter = 10; //没问题
  4. ++iter; //错误,不能改变指针的值
  5. vector<int>::const_iterator cIter = vec.begin(); // const T*
  6. *cIter = 10; //错误,值不能改变
  7. ++cIter; //没问题

const最有威力的地方是面对函数声明时的应用。可以和返回值、参数函数自身产生关联。

  • 返回值:令函数返回一个常量值
  1. const Rational operator*(const Rational &lhs, const Rational &rhs);

可以避免if(a * b = c)的发生。

  • const成员函数

const实施于成员函数的目的,是为了确认该成员函数可作用于const对象身上。有两个原因:(1)得知那个函数可以改动对象哪个不能,这很重要。(2)这使得操作const对象成为可能。根据参数的不同属性调用不同的版本。

  1. const char & operator[](size_t position) const
  2. { ...; ...; ...; return text[position]; }
  3. char & operator[](size_t position )
  4. { ...; ...; ...; return text[position]; }
  5. void print(const TextBlock & ctb)
  6. {
  7. cout << ctb[0]; //会根据ctb的常量属性调用不同的函数
  8. }

两个成员函数如果只是常量性(函数声明参数列表后有无const)不同,可以被重载。

mutable释放掉non-static成员变量的不可改变约束,这些变量可能总是会被改变,即使在const成员函数内。因此,在const成员函数内可以改变这些值,不会引起错误。对哪些变量使用mutable是要有很好的估计的,因为它能突破很多限制。

在上面的代码中,两个版本的operator[]除了返回值和常量属性不同外别无区别,存在着冗余和代码膨胀。有两个方法做的更好:(1)提取公共部分作为工具函数。(2)转型。显然,第一种方法仍不能根本上较少代码膨胀。

  1. char & operator[](size_t position )
  2. {
  3. //转型是安全的
  4. return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
  5. }

我们做了两次转型,第一次是用来为*this添加const(这使接下来调用operator[]时得以调用const版本,避免对自己的递归调用),第二次是从const operator[]的返回值中移除const

这样,我们就能使用常量版本实现非常量版本,实现‘避免代码重复’。反过来是不行的,因为不能在常量版本中改变值。

条款04:确定对象在使用前已被初始化

除了内置类型,初始化的责任落在构造函数身上。规则很简单:确保每一个构造函数都对对象的每一个成员初始化。

注意的是:区分构造函数初始化和赋值的不同。在构造函数大括号中写的是赋值,因为初始化发生在进入大括号之前。因此使用初始化列表才是真正的初始化。虽然它们的结果相同,但是效率更高(赋值版本有发生一次构造一次赋值)。

总是使用初始值列表

在初始时列表中,有着固定的初始化顺序:基类早于派生类,类的成员变量以生命顺序初始化(即使在列表中以不同次序出现)。