EffectiveC++
简介
effectiveC++ 这本书是C++程序员的工作必备之书,讲述了在C++开发中常用的一些,以及经常注意的一些规则,遵循它且不要忽视它,我们就能写出很好的友善的C++代码。
项目链接
点击下载: github项目链接
一. 让自己习惯C++
条款01: 视C++为一个语言联邦
C++是一个多重范型编程语言(Multiparadigm programing language)
- 支持过程形式
- 面向对象形式
- 函数形式
- 泛型形式
- 元编程形式
我们理解其C++时应该视其为 一个相关语言组成的联邦(有4个次语言)
- C:对于C++问题的解法类似C的高级解法时,高效的解法就是去映射C语言的规范,不要掺杂过多C++其他此语言特性
- Object-Oriented C++: 这部分即 C With Class,C++的主流编程
- Template C++: 十分强大的模板编程范式
- STL 模板库: STL 对于容器迭代器算法函数对象等等的规约有极佳的紧密配合与协调,伙同STL进行开发,要遵循其规约
C++对于该4个次语言都有它自己的规约, 记住这4个次语言你就会发现C++容易了解的多
- 建议: C++高校编程守则视情况而变化,取决于你使用C++的哪一个部分。
条款02: 尽量以 const, enum,inline 替换 #define
请看以下代码:
#define ASPECT_RATIO 1.653
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
ASPECT_RATIO 从未被编译器看到,而是被预处理器展开, 可能出现的问题:
- 运用该宏定义的常量时出现了编译错误信息时,错误提示提到的是 1.653 而非常量名,排查错误难
- 普通的变量会被编译器看到,会记录到记号表中(symbol table) ,而宏定义的常量不会被记录,导致出现
常量值的目标码(object code),从而可能造成代码膨胀(代码膨胀会导致额外的换页行为,降低指令高速缓存装置的击中率,带来效率损失)。 - 函数宏虽不会有函数调用的栈方面的开销,但是缺点很明显,难读,容易出错
一. 以 const 替换 宏定义常量
二. 以enum hack 替换 宏定义常量
三. 以 inline 替换 宏定义函数
总结
- 对于单纯常量, 最好以 const 对象或 enums 替换 #defines
- 对于形似函数的宏, 最好改用 inline函数替换 #define
条款03: 尽可能使用 const
一. const 是C++中的对于变量语义约束(不可修改),编译器会强制实行这项约束,只要该值不可被改变(事实),就应该去进行约束
二. const 最具威力的是面对函数声明时的应用, const可以与函数返回值,各参数,函数自身(成员函数)产生关联
- 令函数返回一个常量值,可以降低因客户操作而造成的(意外错误)
- const 实施成员函数: 确认该成员函数可作用于 const 对象身上。
第一:使得 class接口更加容易被理解,(得知那些函数可以改动对象内容那些不行)
第二:使得操作 const 对象成为可能, (pass by reference-to-const 方式传递对象),我们有const成员函数处理const对象。
成员函数如果只是常量性不同,可以被重载
如何实施对对象进行const限制的措施:
- bitwise const阵营:不更改对象内的任何一个 bit (太过于强硬)
- bitwise constness阵营: 对对象的成员变量实施 const(编译器的做法)
- logical constness(重要): 一个const成员函数可以修改它所处理的对象的某些 bits,但请确保客户端侦测不出 (实现办法是利用C++的一个与const相关的摆动场: mutable)
在const 和 non-const 成员函数中避免重复(写出重复的代码)
总结:
- 将某些东西声明为const能帮助编译器去甄别错误用法
(作用域对象,函数参数,函数返回类型,成员函数本体) - 编译器强制实施 bitwise constness,但我们编写程序时应该使用 “概念上的常量性”
- 当const 和 non-const成员函数有着实质等价的实现时,令 non-const 版本调用const版本可避免代码重复
条款04: 确定对象被使用前已经被初始化
一. C++的对象成员变量的初始化发生在进入构造函数本体之前
如上代码会对成员先进行默认构造函数的调用,之后在进行赋值。
解决办法是: 使用 member initalization list 初始化列表替换赋值动作。 为了规范统一:将全部成员(无物也要使用初始化列表初始化)
注意:C++类对象的初始化次序: base class 总是早于其 derived class 被初始化, class 的成员变量总是以其声明次序被初始化
二. C++对于定义在不同编译单元内的non-local static 对象的初始化相对次序无明确定义。
解决办法就是使用 Singleton 使得 non-local static 搬到自己的专属函数中
C++保证, 函数内的 local static对象会在 “首次遇上该对象之定义上” 被初始化。
多线程环境下执行顺序的麻烦性: 尽量在单线程运行期按一定顺序初始化这些 static
总结:
- 为内置型对象进行手工初始化,因为 C++不保证初始化他们
- 构造函数最好使用成员初始值,而不要在构造函数本体内使用赋值操作, 且初始
列列出的成员变量,其排序次序应该和他们在 class 中声明次序相同 - 为免除“跨编译单元之初始化次序”问题, 请以 local static 对象替换 non-static 对象
二. 构造/析构/赋值运算
条款05: 了解C++默认调用哪些函数
一. 检阅一个 empty 类编译器为其做的事情
二: 编译器合成的函数做的事情
发现: operator= 与 copy构造函数 都是编译器合成, 内置类型的成员使用拷贝bit方式,非内置则调用 其定义的 operator= 与 copy构造函数从右侧操作数拷贝数据
三. 注意点:
- 默认拷贝赋值运算符/拷贝构造函数在成员含引用类型时不能被生成
- 当基类将拷贝赋值运算符/拷贝构造函数声明为 private或 delete, 也是不能被生成的。
总结:
编译器可以暗自为class 创建default构造函数,copy构造函数,copy assignment 操作符,以及析构函数。
条款06: 若不想使用编译器自动生成的函数,就该明确拒绝
一. 最简单的拒绝(copy构造函数与 copy赋值运算符)办法是 声明其为 private
- 为了解决在成员函数和友元函数仍然还是能调用,声明其而不定义其是个好办法(会报链接错误)
- 为了将链接错误提前到编译期,需要将其继承一个阻止 copy 的 base class(利用了继承了基类的拷贝操作为private的类,编译器自身将不会生成其 拷贝操作,使用时就会报错)
条款07: 为多态基类声明 virtual 析构函数
一:问题浮现: 销毁一个heap分配的基类指针(指向的是派生类)内存泄漏问题
原因: 通过GetTimeKeeper 返回的指针是一个基类指针,销毁基类指针则会取基类的部分(调用基类的析构函数)
官方: C++明白指出,当derived class对象经由一个base class指针被删除,而其base class带一个 non-virtual函数, 其结果就是未定义-实际执行下来发生的就是对象的 derived 成分没被销毁
解决: 给base class 设置一个 virtual 析构函数即可
二: 验证: 任何 class 带有virtual函数都几乎确定应该有一个 virtual 析构函数, 没有理由地把所有 class 的析构函数设置为 virtual的行为是错误的。
三: 利用析构函数实现抽象类, 适用于没有其余能定义pure virtual函数的类
总结:
-
polymorphic base classes 应该声明一个virtual析构函数, 如果 class 带有任何 virtual 函数,他就应该拥有一个 virtual 析构函数
-
Class 的设计目的如果不是当作 base classes 使用,就不应该声明 virtual 析构函数
条款08: 别让异常逃离析构函数
首先C++并不禁止析构函数抛出异常,但在析构函数中抛出异常很容易导致内存泄漏(程序过早结束)
一: 验证析构函数抛出异常的问题
二: 使用最佳策略解决该问题,避免析构函数传播异常
我们要对 “导致 close 抛出异常” 的情况做出反应
重新设计 DBConn接口,使客户对有机会对可能出现的问题作出反应
1: 管理类提供一个 close 函数,赋予客户一个机会处理因该操作而发生的异常。
2: 管理类设置标志位并在析构函数调用时检测其是否正常关闭,如果未关闭,则正常关闭
3: 第二步在析构函数种再次关闭失败后,我们将又退回 “强迫结束程序或吞下异常的套路”
总结:
-
析构函数绝不要吐出异常, 如果一个被析构函数调用的函数可能抛出异常,析构函数
应该捕捉任何异常, 然后吞下他们(不传播)或结束程序。 -
如果客户端需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个 普通函数(而非在析构函数种)执行该操作
条款09: 绝不在构造和析构过程中调用 virtual 函数
一: 证明在 base class 构造期间, virtual 函数不是 virtual 函数
原因1: 如果在构造base class时调用的是 derived class 的函数(会使用到derived class成员, 但此时成员都是未构造的,会出现问题)。
原因2: 在derived class对象的 base class构造期间,对象本身是base class而不是 derived class不止virtual函数会被编译器解析至 base class,运行期类型信息,也会把对象视为 base class 。
相对应析构函数执行到base class部分,编译器也会视当前对象为 base class
二:如何确保每一次有 Transaction继承体系上的对象被创建,就会有适当版本的 logTransaction被调用
由于你无法在 base class构造时期通过 virtual函数 调用到 derived class 的函数,
因此可以使用 非virtual 通过在 derived class构造函数传(必要参数)传递到 base class 的构造函数, 进而调用 base class 的通过必要参数而实行的普通函数
总结
- 在base class 构造和析构期间不要调用 virtual 函数,因为这类调用从不下降至 derived class
(比起当前执行构造函数和析构函数那层),virtual本质上并没有用
条款10: 令 operator= 返回一个 reference to *this
这份协议可以说是为了实现连锁赋值而 创造的协议
总结:
连锁赋值几乎被所有内置类型和标准库程序提供的类型, string,vector都遵守,因此
我们自定义的也最好共同遵守
条款11: 在 operator= 中处理 “自我赋值”
自赋值是十分没有必要的,但也是代码漏点很多的一个问题
- 类中有heap资源, 编写代码时需要注意 异常安全性
- 自赋值的避免
总结:
确保当对象自我赋值时 operator= 有良好的行为(鲁棒性,通用性更强),其中技术包括
- 来源对象和目标对象的地址
- 精心周到的语句顺序
- copy and swap
确保任何函数如果操作一个以上的对象,且多个对象都是同一个对象时,其行为仍然正确
条款12:复制对象时勿忘其每一个成分
如果拒绝编译器自动生成copying函数,如果你的代码不完全,他们也不告诉你!!!
一: 局部拷贝的错误
这里如果没有复制新添加的变量,编译器也是不会进行提醒的,因为你已经拒绝编译器进行为你生成 copy 函数
实验二: 继承拷贝的错误
继承一个基类后进行编写派生类的 copy 函数时应注意编译器不会进行对 派生类的基类部分自动拷贝
需要手写基类部分的复制
总结:
- copying 函数应该确保复制 ”对象内的所有成员变量及所有 base class 成分“
- 不要尝试以某个 copying 函数实现另一个 copying函数,应该将共同机能放进第三个函数中
并由两个 copying 函数共同调用
三. 资源管理
条款13: 以对象管理资源
基于对象的资源管理办法,是十分有效的
一: 体验基于对象资源管理的方法
自行编写的写法
- 获得资源后立即放入管理对象内, ”以资源取得时机便是初始化时机“ (RAII)
- 管理对象运用析构函数确保资源被释放。 (不论控制流如何,只要对象被销毁,析构函数必会调用)在析构函数中有异常,请遵循条款8
测试:
总结:
- 为了防止资源泄漏,请使用 RAII对象, 他们在构造函数中获得资源并在析构函数中释放资源
- 两个常被使用的 RAII 对象 classes 分别是 std::shared_ptr 和 std::unique_ptr
shared_ptr 是一个 RCSP(reference count smart pointer)
条款14: 在资源管理类中小心 copying 行为
资源的类型并非都是 heap+based 资源时,你需要建立自己的资源管理类
一: 体验资源管理类中copying行为带来的问题
面对资源 copy 的动作有如下解决方案
- 禁止拷贝
- 对底层资源使用 RCSP ,引用计数法 (使用 shared_ptr)
- 深拷贝复制底部资源
- 转移底部资源的拥有权
二: 禁止 copy 行为
三: 引用计数 RCSP
总结
- 复制 RAII 对象必须一并复制它所管理的资源,资源的copying行为决定RAII对象的copying行为
- 普遍而常见的 RAII class copying行为是: 抑制copying,施行引用计数
条款15: 在资源管理类中提供对原始资源的访问
提供原始资源的访问以方便客户
例如: shared_ptr 的get获取原始指针
- 显式返回原始指针
- 隐式直接进行转换(在类中定义 转换资源类型的 运算符)
测试:
总结
- APIs 往往要求访问原始资源,所以每个 RAII class 应该提供一个
”取得其所管理之资源“ 的办法 - 对原始资源的访问可能经由显示转换或隐式转换。一般而言显示转换比较安全,
但隐式转换对客户比较方便。
条款16: 成对使用 new 和 delete 时要采取相同形式
- new 对应 delete, new[] 对应 delete[]
- 尽量少使用数组,多使用 vector string 标准库
条款17: 以独立语句将 newed 对象置入智能指针
一: 我们来演示一个复杂的错误(异常导致内存泄漏)
c++ 语言的函数传参调用顺序弹性很大
可能会出现调用顺序如下的表现:
- new Widget
- priority() //假若这里抛出了异常,就会有内存泄漏问题
- shared_ptr
更好的办法
总结
- 尽量以独立的语句将 heap 资源置入 smart pointer
四. 设计与声明
条款18: 让接口容易被正确使用, 不易被误用
理想上,如果客户企图使用某个接口而却没有获得他预期的行为,这个代码不能通过编译
一:明智而审视地导入新类型对于预防“接口被误用” 有神奇疗效
二: 以函数替换对象,预先定义有效的 对象使得接口更具备安全性
另外: 除非有非常好的理由,否则应令 type 与 内置 type 提供一致的行为接口。
总结:
- 好的接口很容易被正常使用,不容易被误用
- “促进正确使用”的办法包括接口的一致性, 以及与内置类型的行为兼容
- “阻止误用” 的办法包括建立新类型, 限制类型上的操作,束缚对象值, 以及消除客户的资源管理责任
- shared_ptr 支持定制型删除其,可防范在不同DLL间出现的引用计数的问题,可被用来自动解除互斥锁等等资源问题
条款19: 设计 class 犹如设计 type
如何设计高效的 classes 呢, 遵循问题产出设计规范:
- 新的type的对象应该如何被创建和销毁? (构造函数析构函数)
- 对象的初始化和对象的复制该有什么样的差别? (构造函数与拷贝赋值运算符)
- 新type的对象如果被passed by value(以值传递),意味着什么? (copy构造函数)
- 什么是新typed “合法值”? (维护约束条件,要在构造,赋值,setter函数进行错误检查)
- 你的新 type 需要配合某个继承图系? (virtual 与 non-virtual的影响, 特别是析构函数 virtual)
- 你的新 type 需要什么类型的转换? (explict 与 non-explict,以及隐式转换运算符定义)
- 什么样的操作符和函数对该新type而言是合理的? (声明哪些函数, memeber函数还是否)
- 什么样的标准函数应该被驳回? ( 编译器自动生成的那些声明其为 delete)
- 谁该取用新的 type 的成员? (存取函数进行约束)
- 什么是新的 type 的“未声明接口”? ()
- 你的新 type 有多么一般化? (template 或 一整个types家族)
- 你真的需要一个新type吗? (如果只是为了扩充功能而进行派生,倒不如直接定义一个 non-member函数)
条款20: 宁以 pass-by-reference-to-const 替换 pass-by-value
一: 使用引用类型能提升效率问题
二:使用引用进行传参可以实现多态且避免对象切割问题
总结
- 尽量以 pass by reference, 即高效也支持多态,避免切割问题
- 内置类型以及 STL 迭代器和函数对象,使用 pass by value 比较适当,(属于c语言块的内容以c语言方式进行处理)
条款21: 必须返回对象时,别妄想返回其 reference
一: 拒绝返回局部作用域的局部变量的引用,那其实指向了一片不存在的区域
总结:
- 绝对不要返回 pointer 或 reference 指向的一个 local stack 对象,或者返回引用指向一个
heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static对象而有可能同时
需要多个这样的对象
条款22: 将成员变量声明为 private
一: 如何对成员变量进行有效的控制
总结:
-
切记将成员变量设置为 private,这可赋予客户访问数据的一致性,可细微划分访问控制,允许约束条件 得到保证,并提供 class 作者以充分的实现弹性。
-
仅存在两种访问权限: private(提供封装)和 其他(不提供封装),protected 并不比 public 更具封装性。
条款23: 宁以non-member,non-friend 替换 member 函数
一: 在许多情况下 非成员非友元函数的做法比 member 好得多。
- 面向对象守则要求数据尽可能被封装,member函数封装性比 non-member 函数差,且需要重新编译整个类与相关联的,因为不想调用到该函数的实例对象也能调用到,但如果是非成员版本就是谁能用谁调用.
- 愈多东西被封装,我们改变那些东西的能力就越大,越方便,因为涉及到有限的客户代码
二: C++ 对于命名空间的妙用,适用 non-member,non-friend
将所有便利函数防止多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多 non-member non-frient 函数到此命名空间内。
总结
- 宁可拿 non-member non-friend 函数替换member函数,这样做可能增加封装性,包裹弹性
和机能扩充性。
条款24: 若所有参数皆需类型转换,请为此采用 non-member 函数
总结
- 若重载运算符函数的操作数皆需类型转换,就声明其为非成员函数
条款25: 考虑写出一个不抛异常的 swap 函数
一:. 实现一个 pimpl 手法的类, 并写出其 特例化的swap 函数
总结:
- 提供一个 public swap 成员函数,让它高效地置换你的类型的两个对象值,这个函数不能抛出异常
- 在你的 class 或 template 所在的命名空间内提供一个 non-member swap,并令它调用上述 swap 成员函数
- 如果你正编写一个 class, 为你的class 特化 std::swap ,并令它调用你的 swap 成员函数
- 最后调用 swap 时,确保包含一个 using 声明符,以便让 std::swap 在你的函数曝光
然后不加任何 namespace 修饰符, 赤裸裸地调用 swap。 - 注意:不能抛出异常,因为swap是帮助 class 提供异常安全性的保障, 基于的条件就是 swap不能抛出异常
五. 实现
条款26: 尽可能延后变量定义式的出现时间
尽可能延后的真正意义
1: 不仅只是延后变量的定义,直到非带使用该变量的前一刻为止,甚至
应该尝试延后这份定义直到能够给它初值实参为止。
2. 不仅能够避免构造(析构)非必要对象,还能避免无意义的default的构造行为。
一: 1: 尽可能延后能优化代码
二:循环中所使用变量需不需要延后,还是提前定义?
总结
尽可能延后变量定义的出现,这样做可以增加程序的清晰度并改善程序效率。
条款27: 尽量少做转型动作
const_cast() //去除添加 const常量性(一般用于引用)
dynamic_cast() //动态类型转换,将指向派生类对象的基类指针(引用)转换为派生类指针(引用)
reinterpret_cast() //不可移植行为类似 c 语言转
static_cast() // 静态类型转换(隐式转换显示表现)
演示使用:
一: 派生类的virtual 动作先调用 base 的对应函数,容易出现的错误
二: 更容易避免使用 dynamic_cast 的两种方法
普遍的实现版本基于 class 名称之字符串比较, 当深度继承时,其strcmp就会变多,因此注重效率的代码应该 对 dynamic_cast 保持机敏和猜疑
之所以使用 dynamic_cast, 通常是因为你想在你认定为 derived_class 对象身上执行 derived class 操作函数,但你手上仅有一个 指向 base 的 pointer 或 reference
- 直接使用容器存储 指向 derived class 对象的指针(比较不切实际)
- base class 提供 virtual 函数做你想对各个 Window 派生类做的事情,使用多态性质即可 (推荐)
直接使用容器存储 指向 derived class 对象的指针(比较不切实际)
base class 提供 virtual 函数做你想对各个 Window 派生类做的事情,使用多态性质即可 (推荐)
总结:
- 如果可以,请避免转型,特别是注重效率的代码中避免 dynamic_casts,如果有个
设计需要转型动作,请试着发展无须转型的替代设计 - 如果转型是必要的,试着将它隐藏于某个函数背后, 客户随后可以调用该函数, 而不需要
将转型放在他们的代码中, othertype ObjectToOther(object &obj) - 宁可使用C++ style(新式)转型, 不要使用旧式转型, 前者很容易辨识出来,而且也有比较分门别类的职掌
条款28: 避免返回 handles 指向对象内部成分
handles(指针,引用,迭代器)
一: 探索返回对象内部成分带来的弊端
- 破坏封装性
- 导致空悬指针等情况(临时对象的内部竟然能被指向)
总结
- 避免返回 handles(ref, 指针,迭代器)指向对象内部,增加封装性,且帮助 const 成员函数像一个 const,减少 空悬 的发生(临时对象的内部竟然能被指向)
条款29: 为 “异常安全” 而努力是值得的
异常抛出时,带有异常安全性的函数会
- 不泄露任何资源 2. 不允许数据败坏
异常安全性提供以下三个保证之一:
-
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
没有任何对象或数据结构因此而败坏,所有对象都处于一种内部前后一致的状态 -
强烈保证:如果异常被抛出, 程序状态不改变。
调用可能抛出异常的操作,如果调用失败应该恢复到调用之前的状态,调用成功就是完成成功 -
不抛出异常保证: 承诺绝不抛出异常, 因为他们总是能完成它们原先承诺的功能。
(作用于内置类型) nothrow
一: 使用 copy and swap 实现强烈保证级别的异常安全性。
条款30: 透彻了解 inlining 的里里外外
-
inlining 在C++程序中是编译期行为
-
过度热衷inlining会造成程序体积体大,即使有虚拟内存,inline 造成
的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,降低效率。 -
virtual 的调用会使得 inlining 落空 (运行期确定的多态行为,当然会使得inlining落空)
-
大部分调试器面对 inline 函数都束手无策(不存在的函数设置断点 真的很荒唐)
总结:
- 将大多数 inlining 限制在小型,被频繁调用的函数身上,可以使得日后的调试
和二进制升级更容易,也可使得潜在的代码膨胀问题最小化,使程序的速度提升机会最大化
条款31: 将文件间的编译依存关系降至最低
一: 相依赖于声明式,而不要相依定义式实现一个类,遵循以下
- 如果能够使用 object references 或者 object pointers可以完成任务,就不要使用 objects
使用 objects 就需要定义该类型的定义式,在声明一个类时需要知道其sizeof大小 - 如果能够,尽量以 class 声明式替换 class 定义式
总结:
- 支持“编译依存性最小化”的一般构想是: 相依于声明式, 不要相依定义式
基于此构想两个手段是 Handle classes(将自身的实现作为另一个类,且将该类的指针作为自己的实现) 和 Interface classes(以抽象类作为接口) - 程序库头文件应该以 “完全且仅有声明式” 的形式存在。
六. 继承与面向对象设计
条款32: 确定你的 public 继承素模出 is-a 关系
C++进行面向对象编程最重要的规则是:public inheritance 意味着是 is-a 的关系(正向一类)
一:验证 ia-a: D 继承 B,则每一个类型为D的对象本身也是类型为B的对象,反则不成立
虽然正方形继承自长方形,但适用于长方形的行为并不适合正方形, 这显然是非常不正确的,
继承后遵循 is-a 的行为,因此正方形必须能适应长方形的所有行为。
总结:
public 继承意味着 is-a,适用于 base class 的事情也一定适用于 derived class身上
未遵循is-a的代码即使编译通过,但不保证程序的行为是正确的
条款33:避免遮掩继承而来的名称
一:derived class 内的名称会遮盖 base classes 内的名称, 在public 继承下从来没有人希望如此
总结
- derived class 内的名称会遮盖 base classes 内的名称, 在public 继承下从来没有人希望如此
- 为了让遮盖的名称再见天日,可使用using声明式子,或转交函数 类名::function
条款34: 区分接口继承和实现继承
public 继承观念由两部分组成: 函数接口继承和函数实现继承
一: 浏览接口继承与实现继承的具体体现
pure virtual函数的目的是为了让 derived class 只继承函数接口
impure virtual 函数的目的是让 derived class 继承该函数的接口和缺省实现
non-virtual函数的目的是为了令derived class 继承函数的接口及一份强制性实现(不变性,不应该被重新定义)
二: pure virtual 的定义 与 派生重写
我们可以为 pure virtual 提供一个定义,C++并不会发出怨言,但调用其途径仅 “调用时指定其class名称”
重要说明:pure virtual 函数必须在 derived class 重新声明,但也可以有自己的一份实现,需要显式调用而已, 适用于 接口与缺省情况并存的情况下 (比 impure class 好用)
总结:
- 接口继承和实现继承不同,在public 继承下, derived classes总是继承 base class 的接口
- pure class 函数只具体指定接口继承
- 简朴的 impure virtual 函数具体指定接口继承与缺省实现继承
- non-virtual 函数具体指定接口继承以及强制性实现继承
- 一个典型的程序有 80% 的时间花费在 20% 的代码上,请将心里放在那举足轻重的代码上。
条款35: 考虑 virtual 函数以外的其他选择
- virtual 函数带来的虚函数表以及虚表指针的负担
- 其灵活性十分差,继承重写虚函数后不具有可变与可增内容灵活度
virtual 函数以外的其他选择:
一: 使用 NVI(Non-Virtual Interface) 手法实现 Template Method 模式(主张 virtual应该总是private)
私有impure virtual 使得派生类重新定义, member function 调用其 impure virtual 实现(且在调用前后都可以做额外的事情)
二: 以策略模式替换 virtual, 以基于对象的思路(std::bind + std::function)替换virtual+多态
策略模式的构成:
- GameCharacter 作为策略的执行者
- HealthCalcFunc 作为策略(角色生命值健康情况的计算)
演示1: 不使用bind+function,使用指针作为回调函数实现策略模式
Strategy 提供了有趣的弹性:
- 同一人物类型之不同实体可以有不同的健康计算函数。
- 某已知人物之健康指数计算函数可以在运行期变更。
演示2: 由 std::function+std::bind 完成 Strategy 模式
std::function 这样的对象可保持任何可调用物质函数指针,函数对象,成员函数指针(而非仅函数指针)且具有一定的兼容性,可调用物的参数可以隐式转换为其function声明的参数以及返回值能隐式转换为 function声明的返回值 就可以兼容
总结:
- virtual 的替换手法八廓 NVI 手法以及 Strategy 设计模式的多种形式
NVI 手法自身是一个特殊形式的 Template Method 设计模式 - 将机能从成员函数移到 class 外部函数,带来的一个缺点是,非成员函数无法访问
class 的 non-public 成员。 - std::function 对象的行为就像一般函数指针,这样的对象可接纳 “与给定之目标签名式” 兼容
的所有可用调用物。
条款36: 绝不重新定义继承而来的 non-virtual 函数
绝不重新定义继承而来的 non-virtual 函数
条款37: 绝不重新定义继承而来的缺省参数值
由于派生类永远不会重新定义继承来的 non-virtual 函数,所以我们这条是针对 virtual 而言的
一: 缺省参数值执行的是静态绑定, 而不是运行期再次确定,缺省参数遵循调用者的静态类型而定
shape_sptr 的静态类型是 shape 的智能指针, 因此缺省值是 shape的draw的缺省值,而非 rectangle 的draw的缺省值,C++ 坚持以这种夸张的方式来运作是基于运行期效率来做的。
解决: 使用 NVI手法替换虚函数所表现的行为不是很满意的情况
base 的 public non-virtual 函数调用 private virtual, virtual 可被重新定义
我们让 non-virtual 函数指定缺省参数, private virtual 负责真正的工作
总结:
- 绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数都是静态绑定,
而virtual函数—你唯一应该覆写的东西-却是动态绑定。
条款38: 通过复合塑模出 has-a 或 “根据某物实现出”
复合包括
关联: 彼此并不负责对方的生命周期一般使用指针或者引用
聚合: 对象之间的关系表现为分为整体和局部, 整体部分并不负责局部对象的销毁
组合 : 对象之间的关系表现为分为整体和局部, 整体部分负责局部对象的销毁
is-a : 是继承,意味着 派生类必须能作为基类 完成基类能完成的所有功能
has-a: 是复合 挑选合适的关系帮我们完成更好的面向对象设计
一: 做一个必须以复合完成的功能实例, 以 list 实现 set
总结:
- 复合的意义和public继承完全不同
- 在应用领域,复合意味着 has-a(有一个),在实现领域,复合意味着(根据某物实现出)
条款39: 明智而审慎地使用 private 继承
一: 理解private 继承所能实现的效果
- 将 base class 中的所有成员变为 private 属性
- 编译器不会自动将一个derived class 对象转换为一个 base class 对象
- private 继承意味着 根据某物实现出,而非 is-a 语义,只有实现部分被继承,接口不会被继承
编译器不会自动将一个derived class 对象转换为一个 base class 对象
private 继承意味着 根据某物实现出,而非 is-a 语义,只有实现部分被继承,接口不会被继承
二: 其实使用 public 继承加复合 比 private继承 更好
- 阻止了派生类重新定义virtual的要求
- 将Widget的编译依存性降至最低
private 继承主要用于 “当一个意欲” 成为 derived class 者想访问一个
意欲成为 base class 者的protected成分,或为了重新定义一或多个 virtual 函数
空间最优化会促使你选择 private 继承而非 “继承加复合”
空类的sizeof为1,c++会为其安插一个char到空对象中,使得空对象间有一定的区分
EBO:空间基类最优化,将空类(未含成员的类)空间在继承后优化掉
考虑过所有方案后,仍认为 private 继承是“表现程序内两个 classes 之间的关系”的最佳办法,才使用
总结:
- private 继承意味着 (根据某物实现出)的语义, 比复合的级别低,但是当
derived class 需要访问 protected base class 的成员,或者重新定义继承而来的 virtual 函数时,是合理的 - 和复用不同,private继承可用造成 empty base 最优化, 这对致力于 “对象尺寸最小化” 的程序开发者来说很重要。
条款40: 明智而审慎地使用多重继承
一: 多个base class 的成员名字相同造成的歧义
二. 菱形继承的问题(虚拟继承)
虚拟继承带来的影响
-
使用virtual继承的那些 classes 所产生的对象往往比使用 non-virtual
继承的兄弟们体积大(安插共享指针),访问 virtual base classes 成员也慢 -
virtual base 的初始化责任是由继承体系中的最底层(最高级别的派生类)
负责。
关于虚拟继承的建议
- 第一,非必要不使用 virtual bases,平常请使用non-virtual继承
- 第二,如果你必须使用 virtual base classes,尽可能避免在其中放置数据,这样
可用避免在 base classes 初始化发生错误
三: 正确使用多重继承
使用 CPerson 表现人的实体,公有继承接口(IPerson)与 私有继承实现 (PersonInfo)
由于CPerson是转调Personinfo的实现来完成自己的接口,且需要重新定义 virtual 函数
那么就具备 (根据某物实现的语义), 使用private继承或者 public继承+复合 来完成
总结:
- 多重继承比单一继承复杂,且容易引起歧义,使用显示指定base class可解决
- virtual继承会增加大小,速度,初始化复杂度等成本,如果 virtual base classes
不带任何数据,那将是最具实用价值的情况 - 多重继承的确有正当用途,其中最常用即public 继承某个 interface 与 private 继承某个协助实现的 class 的情况
七. 模板与泛型编程
条款41:了解隐式接口和编译期多态
一对template参数而言,接口是隐式的,基于有效表达式,多态是通过template具现化和函数重载解析发生于编译器。
模板在实例化时会进行带入,之后发生编译器多态,对有效表达式进行检测
有效表达式即隐式接口(并非知道该隐式接口是否是有效,仅在具现化模板时进行编译期检测)
总结:
- classes 和 templates 都支持接口和多态
- class 而言接口都是显示的,以函数签名为中心,多态则是通过virtual函数发生在运行期
- 对template参数而言,接口是隐式的,基于有效表达式,多态是通过template具现化和函数重载解析
发生于编译器。
条款42:了解 typename 的双重意义
一. typename 定义模板类型
二: 从属名称与 typename 指涉嵌套从属类型名称
嵌套从属类型名称不需指定 typename 的情况:
对内嵌从属类型进行 起别名:
总结:
- 声明template参数时, 前缀关键字 class 与 typename 可互换
- 请使用关键字 typename 标识嵌套从属类型名称, 但不得在 base class lists(基类列)
或 member initialization list(成员初始值列)内以它作为 base class 修饰符。
条款43: 学习处理模板化基类内的名称
一: 善用模板特例化解决特殊情况
这里: 如果 Company 没有 sendClearText,就会使得调用失败
使用模板特例化完成针对 CompanyZ 的MsgSender(使Company具现化在定义时)
二: C++ 模板继承时由于 base class 并没有具现化,C++并不知道继承的究竟是什么类,因此其内含的 members全部被隐藏, 因此继承而来的 sendClearMsg 会被隐藏
总结:
- 对于特别的模板参数类型可采用模板特例化完成
- 可在派生template类内通过 this-> 或 using 暴露其 base class template 成员名称
条款44: 将与参数无关的代码抽离 templates
一. 非类型模板参数会带来代码膨胀
二: 共性与变性的分析, 对类以 private 继承 或 复合 来抽离代码,并以函数参数或class成员变量替换 template 参数
三. 类型模板参数也会导致膨胀,比如 vector 与 vector, 但只要二进制表述相同(参考指针的空间字节数)我们可以实现共享码
总结
-
Templates 生成多个classes 和 多个函数,所以任何 template 代码都不应该与 某个造成膨胀
的 template 参数形成相依关系 -
因非类型模板参数造成的代码膨胀, 往往可以消除, 做法是以函数参数或class成员变量替换templates参数
-
因类型参数而造成的代码膨胀,往往可降低,做法是以相同二进制表述的具现类型共享实现码。
条款45: 运用成员函数模板接受所有兼容类型
一:同一个template的不同具现体之间不存在固有关系,需要定义泛化的隐式转换
具现体的基本类型存有 转换(派生类指针到基类指针的转换),但具现体并不具备
解决: 写出一个泛化copy构造函数来兼容,限制工作交给实际的类型去转换
二: 泛化的 copy 构造函数并不会阻止编译器生成它们自己的 copy 构造函数, 如果你想完完全全控制 copy动作,请写出泛化版本与非泛化版本
总结:
- 请使用 member function templates(成员函数模板)生成 ”可接受所有兼容类型“ 的函数
- 泛化的 copy 构造函数并不会阻止编译器生成它们自己的 copy 构造函数,
如果你想完完全全控制 copy动作,请写出泛化版本与非泛化版本
条款46: 需要类型转换时请为模板定义非成员函数
一. 复现 24 (”唯有non-member函数才有能力在所有实参上实施隐式类型转换“)在template不适用
原因: template 在实参推导过程中并不考虑”通过构造函数而发生的隐式类型转换“,因此 operator*在该情况下并不会被推导出
二. 解决: 以template 相关的 ”函数支持所有参数之隐式类型转换“时,请将那些函数定义为 class template 内部的friend函数
在template class 中指涉 operator(**)函数为友元,由于类模板推导不依赖 tempalte 实参推导(施行于 function templates上),所以编译器总是能够在class Rational 具现后找到 友元函数 operator*的声明 (并因此缓和 template 实参推导)
注意: friend 仅代表声明, 我们需要定义,可以在友元声明处直接定义,或定义额外的non-member供友元调用
这里解释一下
- 为了让类型转换可能发生在所有实参上,我们需要一个 non-member 函数,为了让
这个函数被自动具现化, 我们需要将它声明在 class 内部,而在 class 内部声明 non-member 的唯一方法是友元。
总结:
- 当我们编写一个 class Template, 而它所提供之 ”与此template相关的“ 函数支持 ”所有参数之隐式类型转换“ 时,请将那些函数定义为 ”class template“ 内部的friend 函数。
条款47: 请使用 traits classes 表现类型信息
一: 迭代器类型的区分与traits(类型萃取技术)的实现
STL 中 通过萃取容器得到其 iterator 类型之后才能实施不同的算法
萃取技术的实现:
- 确定若干你想将来可取得的类型相关信息
- 为该信息选择一个名称
- 提供一个 template 和一组 特化版本(有需要的话),内含你希望支持的类型信息
使用:
二: 模仿 STL 中的做法,使用重载解决 CharacterTraits::category 在编译阶段能完成
但由于if 的原因却是推迟到运行期核定的问题 得到解决
三: 如何正确使用一个 traits class
-
建立一组重载函数(劳工)或函数模板,彼此之间的差异仅在各自的 traits 参数,令每个函数实现码与 其接收之 traits 信息相应和
-
建立一个控制函数(身份像工头)或函数模板(advance),它调用上述那些 ”劳工函数“并传递 traits class 所需信息
总结:
- Traits classes 使得 ”类型相关信息“ 在编译期可用, 他们以 templates 和 ”templates特化“ 完成实现
- 整合重载技术后, traits classes 有可能在编译期对类型执行 if…else 测试
条款48: 认识 template 元编程
一: 了解什么是 template 元编程(TMP template metaprograming)
简介: TMP 是编写 模板程序 并执行于编译期的过程(也可以是说执行于C++编译器内的程序)。
优点:执行与编译期,检测错误更早,程序更高效,较小的执行文件,较短的运行期,较少的内存需求。
缺点:编译时间变长。
二: TMP 实现计算阶乘
总结:
- TMP 可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率
- TMP 被用来 生成 ”基于政策选择组合“ 的客户定制代码,也可以避免生成对某些特殊类型
并不适合的代码:
八. 定制 new 与 delete
条款49: 了解 new-handler 的行为
一: 认识 new-handler 和 set_new_handler,并懂得设计一个 new-handler
new-handler: 当operator new 抛出异常以反映一个未获满足的内存需求之前,它会现调用一个客户指定的错误处理函数,一个所谓的 new-handler
set_new_handler: 参数是指针,用于传入指定的 new-handler函数,返回值是( 被替换的那个 new-handler)
std下的标准库函数声明,模拟
使用 set_new_handler
二: 设计一个良好的 new-handler
当operator new无法满足内存申请时,它会不断调用 new-handler 函数,直到找到足够内存
那就是一个设计良好的 new-handler 函数必须做以下事情:
-
让更多内存被使用: 如果operator new 失败,下一次的分配动作可能成功
策略: 程序一开始就分配一大块内存,而后当 new-handler 第一次被调用,将它们释放给程序使用 -
安装另一个new-handler, 如果目前这个new-handler 并不能满足,让它有能力知道另外的 new-handler可以分配.
策略: 令new-handler 修改 “会影响 new-handler行为” 的static数据,namespace数据或 global 数据 -
卸除 new-handler: 将 null 指针传给 set_new_handler ,一旦没有安装任何任何的 new-handler, operator new 会在内存分配不成功时抛出异常
-
抛出 bad_alloc(或派生自 bad_alloc) 的异常, 这样的异常不会被operator new 捕捉,因此 会被传播到内存索求处。
-
不返回,通常调用abort 或 exit
三. 根据每个 class 不同定制不同的内存分配失败情况
1. 首先需要 class 提供自己的 set_new_handler 和 operator new
set_new_handler 使得客户得以指定 class 专属的 new-handler
operator new 确保在分配 class 对象内存的过程中以 class 专属之 new-handler 替换 global new-handler
set_new_handler 任务
- 设置新 new-handler
- 返回旧 old-handler
operator new 任务
1.(更换错误处理函数) 调用标准 set_new_handler 设置自身保存的 current_handler,
2. (执行内存分配) 分配失败则 global operator new 会调用 current_handler
3. 其中 new-handler 在以异常的方式处理或退出operator new时,应注意将 class 中的 current_handler 回复到第1步设置之前
(保证不影响接下来的内存分配动作)
注意: 第3点使用RAII处理更好
使得更通用: 由于该动作并不因 class 的不同而不同, 将 Widget 的 operator new 与 set_new_handler 动作进行复用是必要的
三. nothrow 形式
总结:
- set_new_handler 允许客户指定一个函数,在内存分配无法获得满足条件时被调用
- Nothrow new 是一个颇为局限的工具,因为它只适用于内存分配,后继的构造函数调用
还是可能抛出异常。
条款50: 了解 new 和 delete 的合理替换时机
总结:
- 为了检测运用错误
- 为了收集动态分配内存之使用统计信息
- 为了强化效能
- 为了增加分配和归还的速度
- 为了降低缺省内存管理器带来的空间额外开销
- 为了弥补缺省分配器中的非最佳齐位
- 为了将相关对象成簇集中 (减少内存页错误的出现频率)
- 为了获得非传统的行为
条款51: 编写 new 和 delete 时需固守常规
一: 编写 operator new 需注意的规矩
- 内含一个无限循环, 并在其中尝试分配内存,无法满足时调用 new-handler
- 处理 0 bytes 申请
- Class专属版本应该处理 ”比正确大小的(错误)“ 申请
operator new(伪码)
基本实现版本:
二. 编写 operator delete 需注意的规矩
- C++保证删除 null 指针永远安全
- Class专属版本应该处理 ”比正确大小的(错误)“ 申请
伪码:
基本实现:
总结:
- operator new 应该内含一个无穷循环, 并在其中尝试分配内存,如果它无法满足内存需要
就应该调用 new-handler,它也应该有能力处理 0 bytes申请, Class 专属版本则还应该处理
”比正确大小更大的(错误)申请“ - operator delete应该在收到 null 指针时不做任何事, Class 专属版本则还应该处理
”比正确大小更大的“ (错误) 申请。
条款52: 写了 placement new 也要写 placement delete
一: 关于使用 new 因构造函数抛出异常出现的内存泄漏问题
placement new 要与 placement delete 对应 才能使得运行期系统寻找到 处理异常导致内存泄漏问题。
问题:
new 一个对象有两部,一部是分配内存,一步调用对象的构造函数
- 如果内存分配成功,但构造函数抛出异常,内存会交付给运行期系统处理,但需要对应的 delete 版本
- 如果使用了 placement new ,但未定义对应的 placement delete 就会出现问题
解决: 定义参数个数与类型相同的 placement new 与 placement delete
二. 解决派生类隐藏 Base 的 placement new 与 placement delete版本
问题:Base 声明的 placement new 与 placement delete 会被 Derived 定义的隐藏掉
标准形式的 operator new 与 operator new 也会因为 Class 声明而被隐藏
解决: 将正常形式的 new和delete 全部放在 一个 Base class里
Derived class 声明 placement new 与 delete, 且使用using声明式子将 Base class 中的
operator new 和 delete 暴露
总结:
-
当你写一个 placement operator new, 请确定也写出了对应的 placement
operator delete, 如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏 -
当你声明了 placement new 与 delete, 请确定别无意识第遮掩了 他们的正常版本