Effective C++

C++最初的名称C with Classes,一开始只是C加上一些面向对象特性。今天的C++已经是个多重范型编程语言,一个同时支持过程形式(procedual)、面向对象形式(object-oriented)、函数形式(functional),泛型形式(generic),元编程形式(metaprogramming)

本系列为《Effective C++》的读书笔记。本文为第一章让自己习惯C++

  • 尽量以const, enum, inline替换#define
1
#define ASPECT_RATIO 1.653

记号名称ASPECT_RATIO从未被编译器看见,因此编译,调试阶段一旦有出错信息,显示都是1.653而并不是ASPECT_RATIO。解决之道是以一个常量替换上述的宏:

1
const double AspectRatio = 1.653;

使用常量可能比使用#define导致较小量的码,因为预处理器“盲目地将宏名称替换为1.653”可能导致目标码出现多份1.653

enum hack:

1
2
3
4
5
class GamePlayer{
private:
enum{NumTurns = 5};
int scores[NumTurns];
}

对于一些旧式编译器(C++98)不允许“static整数型class常量”完成“in class初值设定”,可用以上方法实现.其理论基础是:一个属于枚举类型的数值可以权当ints被使用。

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

必须记住宏中的所有实参都需要加上小括号,但即使这样,也有意想不到的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define CALL_WITH_MAX(a, b) f((a) > (b)? (a):(b))
//a的递增次数居然取决于它和谁比较
int a = 5, b = 0;
CALL_WITH_MAX(++a, b);
CALL_WITH_MAX(++a, b + 10);
//通过template inline函数可以获得宏带来的效率以及一般函数的所有可预料行为和类型安全性
template<typename T>
inline void callWithMax(const T& a, const T& b)
{
f(a > b ? a: b);
}

请记住

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

  • 尽可能使用const

如果关键字const出现在星号左边,修饰的是*ptr,表示被指物是常量,如果在星号右边,那么修饰的是ptr,即指针自身是常量。

如果被指物是常量,有些程序员习惯将const写在类型前,有些则相反,两种写法意义相同

const最具威力的用法是面对函数声明时的应用。

const成员函数
将const实施于成员函数的目的,是为了确认该成员函数可以作用于const对象身上。这一类成员函数之所以重要,基于两个理由。第一,它们使class接口比较容易被理解。第二,它们使“操作const对象”成为可能。令const成员函数调用non-const函数,就冒了风险:曾经承诺不改动的对象被改动了。因此这是一种错误行为。

请记住

  • 将某些东西声明为const可以帮助编译器侦测出错误用法
  • 编译器强制实施bitwise constness,但是你编写程序时应该使用“概念上的常量性”
  • 当const和non-const成员函数有着实质性等价的实现时,令non-const版本调用const版本可以避免代码重复

  • 确定对象被使用前已先被初始化

读取未初始化的值会导致不明确的行为。在某些平台上,仅仅是读取未初始化的值,就可能让你的程序终止运行。请记住,永远在使用对象之前先将它初始化。对于无任何成员的内置类型,必须手动完成此事。例如

1
2
3
4
int x = 0;
const char* text = "A C-style string";
double d;
std::cin >> d;

对于内置类型以外的任何其他东西,初始化责任落在构造函数身上。规则很简单:确保每个构造函数都将对象的每一个成员初始化(初始化而非赋值,即通过初始化列表而非构造函数中的赋值操作)。

通过初始化列表其效果和赋值是一样的,但通常效率更高。基于赋值的操作首先调用default构造函数然后在进行赋予新值。而初始化列表的做法避免了这一问题,因为初始值列表中针对各成员变量而设的实参,被拿去作为各成员变量之构造函数的实参。

有些情况下即使面对的成员变量属于内置类型(那么其初始化与赋值的成本相同),也一定得使用初始值列表。例如成员变量是const或references,它们就一定需要初始值,不能被赋值。

C++有着十分固定的“成员初始化次序”。base classes更早与其derived classes,而class的成员变量总是以其声明次序被初始化(即使初始化列表里顺序不同)

针对non-local static对象,C++对于定义于不同的编译单元内的non-local static对象的初始化次序并无明确定义。可以通过一个小小的方法消除这个问题:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接访问这些对象。这个手法的基础在于:C++保证函数内的local static对象会在该函数被调用期间,首次遇上该对象之定义式时被初始化。

这么做的好处有两点:

  • 保证了所获得的reference将指向一个历经初始化的对象。
  • 如果从未调用这个封装函数,就绝对不会引发构造和析构成本。

请记住:

  • 为内置类型进行手工初始化,因为C++不保证初始化它们。
  • 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表中列出的成员变量,其排列次序应该和它们在类中声明的次序相同。
  • 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local static对象。