本文是笔者阅读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对象置于智能指针内;如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏
受笔者知识水平和表达能力所限,有些问题上难免出现疏漏,欢迎在评论区指正