学习程序语言根本大法是一回事,学习如何以某种语言设计并实现高效程序则是另一回事。这种说法对C++尤其适用。
Chapter 0 术语
声明式:告诉编译器某个东西的名称和类型,但略去细节。
1 | extern int x; // 对象声明 |
每个函数的声明揭示其签名式(signature),也是参数和返回类型
定义式:提供给编译器声明式遗漏的细节;
- for object:编译器为此对象分配内存的位置;
- for function | template:提供代码本体;
- for class | template:列出成员;
初始化:“给予对象初值”的过程。
关键字explicit
可以被用在自定义类型的构造函数前,可以阻止构造函数被用来执行隐式类型转换(但仍可进行显示类型转换)
1 | Class B{ |
Chapter 1 View C++ as a federation of languages
一开始,c++只是c加上一些面向对象的特征,所以最初叫”C with Classes”
今天的C++已经是个多重泛型编程语言(multiparadigm programming language)
最简单的方法是将C++视为一个由相关语言组成的Federation而非单一语言,在每个次语言中,各种守则都倾向于简单、直观易懂。
一共四个:
- C:说到底C++仍是以C为基础的语言,但也反映出C语言的局限性:没有模板、异常、重载等,面向过程。
- Object-Oriented C++:这部分是C with Class追求的。引入了类的概念、封装、继承、多态、虚函数(动态绑定)等。
- Template C++:这部分是C++的泛型编程部分。由于templates威力强大,他们带来崭新的编程范型“模板元编程”
- STL:template程序库
Chapter 2 Prefer consts, enums, and inline to #defines
原则:以编译器替换预处理器,因为#define
不被视为语言的一部分,因此它定义的内容不会进入符号表,因此错误信息中也不会有记录。
const/enums替换define中两种值得提及的情况:
定义常量指针
const char* const authorName = "Scott Meyers";
因为常量定义通常被放在头文件里,因此可能会被多个文件访问到,所以有必要将指针(而不是指向的内容)声明为const;若还要将指向的内容也定义为
const
,那么需要写两次const
。定义
class
专属常量为了将常量作用域限制于
class
内部,必须让他作为一个成员;而为了保证此常量最多只有一份实体,必须让成为一个
static
成员
1 | class GamePlayer{ |
在这里NumTurns
是一个声明而非定义式。有可能不被旧编译器支持,他们不允许static成员在其声明式上获得初值,in-class
初值设定只允许对整数常量进行(包括int、char、bool),那么可以:
1 | class CostEstimate{ |
enum hack
如果你在class编译期间一定需要一个class常量值, 例如GamePlayer
中编译器一定坚持必须在编译期间就知道数组的大小,可以采用enum hack方法:
1 | class GamePlayer{ |
enum hack
的行为某些方面比较像#define
而不是const
,例如取一个const的地址是合法的,但是取一个enum
/#define
的地址通常不合法enum hack
是模板元编程的基础技术(见C48)
inline替换define:
1 |
|
改成具有可预测行为和类型安全的template inline
函数:
1 | template<typename T> |
Chapter 3 Use const whenever possible
const
出现在星号*
左边时,表示被指向的对象是常量,如果出现在*
右边,则说明指针自身是常量,如果出现在两边,则表示被指对象和指针都是常量;
所以下面两种写法是一样的:
1 | void f1(const Widget* pw); |
const成员函数,让函数返回一个常量值,可以有效避免诸如if( (a*b) = c )
这种错误(编译器报错)
non-const成员调用const成员函数并转型可以避免代码重复
Chapter 4 Make sure that objects are initialized before they’re used
- 内置类型:手动完成:
1 | int x = 0; |
- 非内置类型:构造函数——确保每一个构造函数都将对象的每一个成员初始化。注意,C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,即初始化列表中;
- 父类总是先于子类被初始化;class成员变量以其声明次序被初始化(即使他们在初始化列表中的位置可能不同)
- non-local static对象的初始化:
函数内的static对象称为local static对象,其他(包括global、定义在namespace中、classes中、file作用域内被声明为static的对象)称为non-local static对象。
如果一个编译文件里的某个non-local static对象初始化动作使用了另一个文件中的non-local static对象,此时他用到的这个对象可能尚未被初始化。
解决方法:将non-local static对象放进它的专属函数中,也就是将non-local static替换为local static对象,因为C++保证:函数内的local static对象会在“该函数被调用期间,首次遇上该对象的定义式”时被初始化。这也是单例模式(Singleton)的一个常见实现手法。
Chapter 5 Know what functions C++ silently writes and call
C++中一个空类默认包含:
- 空的构造函数
- copy构造函数
- non-virtual析构函数
- copy assignment操作符(=)
只有当这些函数被调用时,他们才会被编译器创建出来
1 | Empty el; // 默认构造函数 |
默认的copy行为是将对象的所有非static变量拷贝到目标对象。
Chapter 6 Explicitly disallow the use of compiler-generated functions u do not want
- 阻止copy行为:声明copy函数为private但不实现
- 进一步:定义一个
Uncopyable
基类,并将有“阻止copy行为”需求的类作为它的private派生类
1 | class Uncopyale{ |
Chapter 7 Declare destructors virtual in polymorphic base classes
- 通过为基类声明virtual析构函数,来让base class指针控制的derived对象正确销毁。
- 当class不被用来作为base class时,不要用virtual析构函数。因为virtual导致在class中增加vptr指针指向vtbl,导致占用空间增大
- 只有当class内至少含有一个virtual函数,才为他添加virtual析构函数
- 任何带多态性质的base class,应该为其声明一个virtual析构函数
Chapter 8 Prevent exception from leaving destructors
- 不要让异常跑出析构函数
- 可能会引发两个未被处理的异常同时存在,例如:
1 | void doSomething(){ |
- 两个方法处理该问题:
- 在析构函数中处理异常,遇到异常就在catch中调用
abort()
结束程序,“抢先制不确定行为于死地”; - “吞下”异常;在catch中记录该次调用失败。后续可以交由用户在普通函数中进行处理。
- 在析构函数中处理异常,遇到异常就在catch中调用
Chapter 9 Never call virtual functions during construction or destruction
derived class对象的base class构造期间调用的virtual函数是base class中的virtual函数,因为此时derived class的成员变量还没有被构造;此时virtual被调用的实际行为并不符合virtual函数被期望的行为(实现多态),但是编译器不会报错。
相同的道理适用于析构函数,一旦derived class的析构函数开始执行,其对象内所有的成员变量都应视为未定义值。
解决方法:
- 将要调用的函数声明为non-virtual;
- 从derived class的构造列表中传值给base class的构造函数
- 使用private static函数生成要传的值,比较可读,也保证了使用的变量在使用时已经被初始化完毕(因为仅能使用static变量)
Chapter 10 Have operator=
return a reference to *this
- 字面意思,令赋值操作符返回一个指向当前对象的引用
1 | Widget& operator=(const Widget& ths){ |
以实现类似于x = y = z = 15
的操作
Chapter 11 Handle assignment to self in operator=
- 当出现
a[i] = a[j]
且i == j
,即自我赋值时,可能会出现不安全现象:
1 | Widget& Widget::Operator= (const Widget& rhs){ |
解决方法:
- identity test 证同测试:
if(this == &rhs) return *this;
缺点:引入了新的控制流分支,降低执行速度; - 另外一个思路是,通过“消除异常安全性”同时保证“自我赋值安全性”——在编写代码时就考虑到
&rhs
是否可能是this
这个问题,从而正确安排代码顺序; - 采用Chapter 29提到的copy and swap技术:
1
2
3
4Widget& Widget::Operator= (const Widget rhs){ // 传入副本
swap(rhs); // 将副本与当前对象进行数据交换
return *this;
}- identity test 证同测试:
Chapter 12 Copy all part of an object
如果你声明自己的copying函数,意思就是告诉编译器你并不喜欢默认实现中的某些行为。编译器仿佛被冒犯似的,会以一种奇怪的方式回敬:当你的实现代码几乎必然出错时却不告诉你:)
- copying函数 == copy构造函数 + copy assignment操作符
- 当自定义copying函数时,如果缺少成员变量,编译器不会报错;因此注意要复制所有local成员变量
- 尤其是当编写derived class的copying函数时,必须也复制其base class的成分(通过调回base class的copying函数)
- 如果copy构造函数和copy assignment操作符有相近的代码,那么就可以从中抽出一个函数给二者调用。
资源管理
所谓资源,就是那些使用之后必须还给系统的东西。如果不这样,糟糕的事情就会发生。
C++中最常见的资源就是动态分配内存,包括文件描述符、互斥锁、图形界面中的字型和笔刷、数据库连接、网络sockets。无论哪种资源,重要的是,当你不再使用他,必须将他还给系统。
Chapter 13 Use Object to manage resources
- 获得资源后立即放入对应的管理对象内(Resource Acquisition Is Initialization RAII)
- 管理对象运用析构函数确保资源被正确释放
- 常用的RAII classes:
tr1::shared_ptr
和auto_ptr
- 智能指针被销毁时会自动删除它指向的对象,所以不要让多个智能指针指向同一个对象
- 为了解决上述问题,智能指针有如下特性:若通过copying函数复制它,它本身会变成null,而复制得到的指针将得到原资源的唯一拥有权:
1 | auto_ptr<Investment> |
STL容器要求元素发挥“正常的”复制行为,因此不允许容器类型为
auto_ptr
shared_ptr
采用引用计数追踪对象被引用的情况,当无人指向该对象时自动删除该资源auto_ptr
和shared_ptr
都是在析构函数中做delete
而不是delete []
,因此不能在动态申请的数组身上使用二者!
Chapter 14 Think carefully about copying behavior in resource-managing classes
- 当一个RAII对象被复制时,几种常见的行为:
- 禁止复制
- 引用计数(使用
shared_ptr
) - 复制底层资源
- 转移资源拥有权
Chapter 15 Provide access to raw resources in resource-managing classes
- 在资源管理类中封装并提供对原始资源的访问
- 例如
shared_ptr
和auto_ptr
都提供一个get成员函数,返回一个指向指针内部的原始指针(显示转换):
1 | auto_ptr<Investment> p1(createInvestment()); |
当然也可以通过指针取值操作符(->
/ *
)隐式转换至底部指针:
1 | class Investment{ |
自定义隐式转换函数:
1 | class Font{ |
Chapter 16 Use the same form in corresponding uses of new and delete
- 因为对象数组和单一对象的内存空间结构不同,导致了delete [] 和delete的行为不同
- 因此new一个数组时需要用delete[ ],单个对象使用delete;交错使用的行为都是不可预测的
- 警惕typedef:尽量不要对数组形式做typedef,否则容易引发用户不正确的delete;
Chapter 17 Store new
ed objects in smart pointers in standalone statements
- 使用智能指针时,需要用独立的语句将对象置入:
1 | int priority(); |
上述情况下,第一实参由两部分组成:
- 执行
new Widget
表达式 - 调用
shared_ptr
构造函数
那么processWidget
之前,编译器执行了如下三件事:
- 计算第一参数
- 执行
new Widget
表达式 - 调用
shared_ptr
构造函数
- 执行
- 计算第二参数
- 调用
priority
- 调用
编译器只保证参数内部的计算顺序,不能保证计算各个参数的顺序,因此实际执行时可能会变成:
- 执行
new Widget
表达式 - 调用
priority
- 调用
shared_ptr
构造函数
此时,如果调用priority时抛出异常,那么之前申请的空间就会发生泄漏。为了避免上述状况的出现:
1 | std::tr1::shared_ptr<Widget> pw(new Widget); |
这样才是最安全的。因为编译器对跨越语句的操作没有重新排列的自由。