本文是笔者阅读 Effective C++ 第三版时的读书笔记,主要内容包括:
- 对书上内容的概括提取
- 笔者自己不清楚的小知识点的提取
- 对书中每个条款的后的 “请记住” 部分的(基本上是)原文摘录
这种灰色部分一般是笔者自己的一些注释
原书一共九章,本文(上篇)记录第一至三章的笔记
1. 让自己习惯 C++
条款 01:视 C++ 为一个语言联邦
- 将 C++ 视为四种 sublanguage,即:
- C(没有模板、异常、重载)
- Object-Oriented C++(面向对象、多态性)
- Template C++ (泛型编程)
- 由此衍生出模板元编程(template metaprogramming, TMP)
- STL (容器、迭代器、算法、函数对象)
- 在 sublanguage 间切换时,高效的编程守则也会改变,例如:
- 对内置 (C-like) 类型,值传递比引用传递更加高效!
- 迭代器和函数对象由 C 指针塑造,所以对它们也应该采用值传递
C++ 高效编程守则取决于你使用 C++ 的哪一部分
条款 02:尽量以 const,enum,inline 替换 #define
- 宏的定义也许不会被编译器看见(预处理器处理),进而导致报错时不会带有宏的记号信息,导致难以调试;而常量则可以避免此种问题
- 使用常量可能导致较小量的码 (object code);而宏可能会导致多份盲目替换,导致目标码大一些
往往
const string
比const char* const
要好一些这里还不知道是为啥
#define
一般不能提供封装性,常量则不然- in-class 初值设定只能对整型常量进行
关于 enum hack:
- 取
enum
的地址不合法,但是取const
的地址是合法的(有时这是一种约束) enum
绝不会导致内存分配,而某些不优秀的编译器也许会给const
分配空间综上来说,enum 的行为某方面更像 #define
- 取
使用 template inline 函数替代宏
对于单纯常量,最好以 const 对象或 enum 替换 #define
对于形似函数的宏,用 inline 函数替换 #define
条款 03:尽可能使用 const
- 迭代器就像一个
T*
指针const iterator iter
类似T* const t
const_iterator
类似const T*
(用于只读访问)
- 函数返回值是 const 可以预防一些奇怪的错误(书 P19 上方)
- 两个成员函数只有常量性不同可以被重载
- 关于 bitwise constness 和 logical constness(见书 P21, 派别之争并不是重点,重点后者的思想是保证客户端侦测不出变化,与编译器只认 bitwise 之间的矛盾,从而导出下一条)
- 在声明变量前加上前缀
mutable
,const 成员函数就可以更改这些变量 在 const 和 non-const 之间避免重复:
- 只应该让 non-const 函数调用 const 函数(反之则可能导致成员变量改动,使 const 函数失去 const 特性)
具体写法:
1
2
3
4
5
6
7
8
9
10const char& operator[](std::size_t pos) const {
...
return str[pos];
}
char& operator[](std::size_t pos) {
return
const_cast<char&>( // 转除op[]返回的const
static_cast<const ClassName&>(*this)[pos] // 把*this变const以调用const函数
);
}注意到,这里把 * this 转成了 const ClassName&,相当于进行了一个加 const
将某些东西声明为 const 可以帮助编译器侦测错误用法。const 可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体
编译器强制实施 bitwise constness,但你应该使用 “概念上的常量性”
当 const 和 non-const 成员函数有等价实现时,令 non-const 版本调用 const 版本来避免代码重复
条款 04:确定对象使用前已先被初始化
确保每一个构造函数都将对象的每一个成员初始化
- 分清初始化 (initialization) 和赋值 (assignment)
- 通常使用初始化列表,即直接调用拷贝构造,比先默认构造再赋值来得快
- const 和 reference 变量必须使用初始化列表
总而言之,总是在初值列中列出所有成员变量
- 过多的重复下,可以将那些 “赋值表现和初始化一样好” 的成员变量移到某个 private 函数里供构造函数调用
- class 的成员变量以其声明次序被初始化,和初始化列表顺序无关
- 对于不同编译单元之间的 non-local static 对象(位于 namespace 或 global 作用域)的初始化次序没有明确规定
- 合适的设计手法是,把 non-local static 对象变成函数里的 static 对象,然后让这个函数返回该对象的引用
- 函数内的 local static 对象会在 “该函数调用期间”“首次遇上该对象之定义式” 时被初始化
- 尽管如此,任何涉及 non-const static 对象涉及到多线程都会有不确定性,一般是在单线程启动阶段先把这样的 reference-returning 函数手动调用一遍
- 合适的设计手法是,把 non-local static 对象变成函数里的 static 对象,然后让这个函数返回该对象的引用
为内置型对象进行手工初始化,因为 C++ 不保证初始化它们
构造函数最好使用初值列代替赋值操作;初值列排列次序应该和声明次序相同
为了免除跨编译单元时初始化次序的问题,请用 local static 对象替换 non-local static 对象
2. 构造 / 析构 / 赋值运算
条款 05:了解 C++ 默默编写并调用哪些函数
- 编译器默认声明的拷贝构造、赋值、构造、析构函数都是 public 且 inline 的
- 只有这些默认的函数需要被调用,它们才会被创建
- 编译器产出的析构函数默认 non-virtual
- 有三种情况编译器会拒绝生成一个默认的 copy assignment
- 类中含有 reference 成员
- 类中含 const 成员
- 基类的 copy assignment 声明为 private
编译器可以暗自为 class 创建 default 构造函数、copy 构造函数、copy assignment 操作符,以及析构函数
条款 06:若不想使用编译器自动生成的函数,就该明确拒绝
- 可以通过仅声明,不定义 copy 构造、copy assignment 操作符,并且声明为 private,来避免编译器的自动创建和不被希望的调用
- 进一步,如果不希望 friend 和 member 函数调用,可以实现一个基类,并将它的 copy 构造、copy assignment 操作符声明为 private,然后继承它(甚至未必要是 public 继承)
为驳回编译器(暗自)提供的机能,可将相应的成员函数声明为 private 并不予实现。或者采用 Uncopyable 基类的做法。
条款 07:为多态基类声明 virtual 析构函数
- 任何 class 只有带有 virtual 函数都几乎确定应该也有一个 virtual 析构函数
- 如果不作为基类,不应使用 virtual, 因为包含了 vptr (virtual table pointer),可能会导致:
- 对象的体积可能会增大 50%~100%!
- 不再和其他语言(如 C)中的相同声明拥有一样的结构,从而不能传递至其他语言所写函数
- string,以及所有 STL 容器,都不带 virtual 析构函数!(因而不适合被继承)
如果希望有一个抽象类,可以将类的析构函数声明为纯虚,但是仍然需要提供定义!(否则连接器会报错)
这里比较奇怪,基类的析构该怎么写还是要怎么写,比如该 delete 还是要 delete
带有多态性质的基类应该声明一个 virtual 析构函数;如果类中有任何 virtual 函数,它就应该有一个 virtual 析构函数
类的设计目的如果不是为了作为基类,或者是基类但不是为了具有多态性,就不应该声明 virtual 析构函数
条款 08:别让异常逃离析构函数
- 析构函数不应该吐出异常,理由有二:
- 可能导致异常后面的资源释放不会执行
- 异常可能会导致程序调用其他的析构函数来释放资源,这可能会造成新的异常(两个异常同时存在的情况下,程序若不是结束执行就是导致不明确行为)
- 解决这种问题的方法:
- 析构函数内有异常就
abort()
结束程序 - 析构函数吞下异常(不一定是个好主意,但是可行!)
- 重新设计一个普通函数接口来进行操作,从而给客户一个处理异常的空间,同时在析构函数里操作作为双保险
- 析构函数内有异常就
析构函数绝对不要吐出异常;析构函数应该捕捉任何异常,吞下它们或结束程序
如果客户需要对某个操作函数运行期间抛出的异常做出反应,类中应该提供一个普通函数进行该操作
条款 09:绝不在构造和析构过程中调用 virtual
- 原因是在基类构造期间运行期类型信息(dynamic_cast 和 typeid 之类)把对象视为 base class 类型,虚函数也只会被解析至 base class(这样的好处是可以防止虚函数调用还没有初始化的派生类成员)
- 析构函数也同样
- 如果想在构造对象时输出适当的对应版本的信息,可以通过给构造函数增加参数一直传给基类,并且在基类构造函数中使用 non-virtual 函数
在构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至当前执行层的 derived class
条款 10:令 operator= 返回一个 reference to *this
- 为了实现连锁赋值,所以返回一个指向操作符左侧实参的引用(也包括 += 之类的运算符)
令赋值操作符返回一个 reference to \this*
条款 11:在 operator= 中处理 “自我赋值”
三种方法处理这种情况:
“证同测试”:
1
if (this == &rhs) return *this;
保持异常安全性的同时处理了自我赋值
1
2
3Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
return *this;这段代码的好处是即使 new 的时候出现异常,pb 也会保持原状
使用所谓 copy and swap 技术
1
2
3Widget tmp(rhs);
swap(tmp); // 将*this和tmp的数据交换
return *this;而如果
rhs
是值传递的,还可以省略第一行将 copying 动作从函数本体内移至 “函数参数” 构造阶段有时可令编译器生成更高效的代码(也许指用值传递 rhs)
确保当对象自我赋值时 operator = 有良好行为;其中技术包括比较地址,安排语句顺序,以及 copy-and-swap
确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确
条款 12:复制对象时勿忘其每一个成分
- 对于基类部分的复制,往往在派生类的复制函数中手动调用基类的复制函数,如
BaseClass::operator=(rhs)
- 故编写 copying 函数时应该:
- 复制所有 local 成员变量
- 调用所有基类的适当 copying 函数
- 让 copy 构造函数和 copy assignment 操作符之间互相调用是不合理的(如果要代码复用,应该建立一个新的函数给两者调用)
Copying 函数应该确保复制 “对象内的所有成员变量” 及 “所有 base class 成分 ”
不要尝试以某个 copying 函数实现另一个 copying 函数;应该将共同机能放进第三个函数中供两者共同调用
3. 资源管理
条款 13:以对象管理资源
- 以对象管理资源的一般想法:
- 获得资源后立刻放进管理对象
- 管理对象运用析构函数确保资源被释放
为防止资源泄漏,请使用 RAII(Resource Acquisition Is Initialization,资源获取就是初始化)对象,它们在构造函数中获得资源,在析构函数中释放资源
两个常被使用的 RAII classes 是 tr1::shared_ptr
和 auto_ptr
,前者通常是较佳选择,因为其 copy 行为比较直观,后者的复制动作会使它指向 null
条款 14:在资源管理类中小心 copying 行为
- 对于 RAII 对象的复制,一般有几种态度(前两种情况较多):
- 禁止复制(手法见条款 06)
- 使用引用计数法
- 通常只要内含
tr1::shared_ptr
,便可以实现这种行为 tr1::shared_ptr
允许指定一个 “删除器” 作为第二参数,即引用数归零时被调用的行为
- 通常只要内含
- 复制底部资源,即进行深拷贝
- 转移底部资源的拥有权,即
auto_ptr
的行为
复制 RAII 对象必须一并复制它所管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为
普遍而常见的 RAII copying 行为是:抑制 copying、施行引用计数法;不过也有其他可能
条款 15:在资源管理类提供对原始资源的访问
- 将 RAII class 对象转换为其内含的原始资源:
- 显式转换
- 智能指针的
.get()
方法 - 自行在类内提供一个
get()
方法返回资源
- 智能指针的
- 隐式转换
- 智能指针对
->
和*
的重载 - 在类内提供一个类型转换的函数(例如 operator int () 之类)
- 智能指针对
- 显式转换
APIs 往往要求访问原始资源,所以每一个 RAII class 应该提供一个 “取得其所管理之资源” 的办法
对原始资源的访问可能经由显式或隐式转换;一般而言显式比较安全,但隐式对客户比较方便
条款 16:成对使用 new 和 delete 时要采取相同形式
- 在运用
typedef
的时候要小心 t,要注意new
这种类型的时候该用什么delete
(所以一般不对数组形式做typedef
,而是直接使用vector
之类的)
如果你在 new 表达式中使用 [],必须在相应的 delete 表达式中也使用 []; 反之亦然
条款 17:以独立语句将 newed 对象置入智能指针
- 这样做是为了防止 “资源被创建” 和 “资源被转换为资源管理对象之间” 发生异常干扰(同语句内 C++ 编译器对各项操作的调用顺序可能有一定的自由度,例如函数参数传递时调用的函数,详见书 P76)
以独立语句将 newed 对象置于智能指针内;如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏
受笔者知识水平和表达能力所限,有些问题上难免出现疏漏,欢迎在评论区指正