让自己习惯C++

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

今天的C++是个多重泛型编程语言(multiparadigm programming language),一个同时支持过程(procedural)、面向对象(object-oriented),函数形式(functional),泛型形式(generic),元编程形式(metaprogramming)的语言。

C++主要的次语言:

  • C。

  • Object-Oriented C++。class、封装、继承、多态等。

  • Template C++。C++泛型编程部分。

  • STL。容器、迭代器、算法以及函数对象

条款二:尽量以 const、enum、inline 替换 #define

#define 是在预处理阶段处理的,记号从未被编译器看见,所使用的名称也可能从未进入记号表(symbol table)。

解决方法是以一个常量替换宏。

有一个特殊情况是 class 专属常量。将常量的作用域限制在类内,必须让它成为 class 的 一个成员;为了确保此常量至多只有一份实体,必须让它成为 static 成员。

1
2
3
4
5
6
7
8
9
10

class GamePlayer {

private:

static const int NumTurns = 5; // 常量声明式

int scores[NumTurns]; // 使用该常量

};

通常 C++ 要求你对你使用的任何东西提供一个定义式,对于类内静态整形变量,要在实现文件 .c 中提供定义。

1
2

const int GamePlayer::NumTurns;

这几乎是你唯一需要做的事情。唯一例外是当你在 class 编译期间需要一个 class 常量值,万一你的编译器不允许“static整数型class常量”完成“in-class 初值设定”,可改用所谓的“The enum hack” 补偿做法。其理论基础是:一个属于枚举类型的数值可权充 ints 被使用。

1
2
3
4
5
6
7
8
9
10

class GamePlayer {

private:

enum { NumTurns = 5 }; // the enum hack

int scores[NumTurns]; // 使用该常量

};

认识 enum hack:

  • enum hack 的行为某方面来说比较像 #define 而非 const。取一个 const 地址是合法的,但 #defineenum 则不合法。

  • 纯粹为了实用主义,事实上,enum hack 是模板元编程的基础。

另一个常见的用法是用 #define 实现宏,看起来像函数,但没有函数调用的开销,但是这有很多缺点,有时候会产生一些不可预料的行为。解决方法是写出 template inline函数。

小结:

  • 对于单纯变量,尽量以 const 对象或者 enum 替换 #define。

  • 对于形似函数的宏,最好改用 inline 函数替换 #define。

条款三:尽可能使用 const

const 允许你告诉编译器或其他程序员某值应该保持不变,只要这是事实,就该说出来。

const 出现在 * 两边分别代表什么呢?

声明迭代器为 const 就像声明指针为 const 一样,表示迭代器不得指向不同的东西,但它指的东西的值是可以改变的。如果想让它指的东西的值不能变,则要用 const_iterator

const 最具威力的用法是面对函数声明时的应用。在一个函数声明式内,const 可以和函数返回值、参数、函数自身(成员函数)产生关联。

const 成员函数,是为了确认该函数可作用于const对象身上。两个关键原因:第一,使 class 接口比较容易被理解,哪些函数可以改动对象内容,哪些不可以;第二,使操作const对象成为可能。

两个成员函数如果只是常量性不同,可以被重载。这是一个重要的C++特性。

真实程序中,const对象大多用于 passed by pointer-to-const 或者 passed by reference-to-const 的传递结果。

同时实现 const版本和non-const版本的成员函数,如何避免代码重复?可以用 const 版本实现出 non-const版本,即用const成员函数实现出其non-const孪生兄弟。

反向做法,即令 const版本调用non-const版本以避免重复是不对的。如果在const成员函数内调用了non-const版本的,则不保证绝不改变对象的逻辑状态。

小结:

  • 将某些东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域的对象、函数参数、返回值、成员函数本体。

  • 编译器强制实行 bitwise constness,即成员函数只有在不更改对象之任何成员变量(static变量除外)时才可以说是const,但你编写程序时应该使用“概念上的常量性”。

  • constnon-const成员函数有着实质等价的实现时,令 non-const 版本调用 const 版本可避免代码重复,反之不允许。

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

读取未初始化的值会导致不明确的行为。

对于无任何成员的内置类型,必须手工完成此事。对于内置类型以外的任何其他东西,初始化责任落在构造函数上,规则就是:确保每个构造函数都将对象的每一个成员初始化。

这个规则很简单,重要的是别混淆了初始化和赋值。C++规定,对象的成员初始化动作发生在进入构造函数本体之前。

构造函数的一个较佳写法是,使用成员初始值列表替换赋值动作。对大多数类型而言,比起先调用default构造函数,再调用 copy assignment操作符,单只调用一次copy构造函数是比较高效的。对于内置类型来说,效率是一样的,但为了形式统一,最好也通过初始值列表来初始化。

C++有着十分固定的“成员初始化次序”。base classes总是早于其 derived classes被初始化,而 class 的成员变量总是按照声明的次序初始化,即使在成员初始化列以不同次序出现。

C++ 对“定义于不同编译单元的non-local static对象”的初始化次序并无明确定义。因为决定它们的次序非常困难。

消除这个问题的方法是:将每个 non-local static 对象搬到自己专属的函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所包含的对象,然后用户调用这个函数,而不直接指涉这些对象。换句话说,non-local static对象被local static对象替换了。这种结构下的reference-returning函数往往十分单纯:第一行定义并初始化一个local static对象,第二行返回它。

C++保证,函数内的local static对象会在“该函数被调用”,“首次遇上该对象定义式”时被初始化。

任何一种non-const static对象,不论它是localnon-local,在多线程环境下“等待某事发生”都会有麻烦。处理这个麻烦的一种做法是:在单线程启动阶段手工调用所有reference-returning函数,这可消除与初始化有关的竞态条件。

小结:

  • 手工初始化内置型对象;

  • 构造函数最好使用成员初始化列表,而不要在构造函数中使用赋值操作。初始值列表列出的成员变量,其排列次序应该和它们在类中的声明次序相同;

  • 为免除“跨编译单元之初始化次序”的问题,请以local static对象替代non-local static对象。

Donate comment here