effectiveC++ 这本书是C++程序员的工作必备之书,讲述了在C++开发中常用的一些,以及经常注意的一些规则,遵循它且不要忽视它,我们就能写出很好的友善的C++代码。
点击下载: github项目链接
C++是一个多重范型编程语言(Multiparadigm programing language)
我们理解其C++时应该视其为 一个相关语言组成的联邦(有4个次语言)
C++对于该4个次语言都有它自己的规约, 记住这4个次语言你就会发现C++容易了解的多
请看以下代码:
#define ASPECT_RATIO 1.653
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))
ASPECT_RATIO 从未被编译器看到,而是被预处理器展开, 可能出现的问题:
三. 以 inline 替换 宏定义函数
总结
一. const 是C++中的对于变量语义约束(不可修改),编译器会强制实行这项约束,只要该值不可被改变(事实),就应该去进行约束
二. const 最具威力的是面对函数声明时的应用, const可以与函数返回值,各参数,函数自身(成员函数)产生关联
如何实施对对象进行const限制的措施:
在const 和 non-const 成员函数中避免重复(写出重复的代码)
总结:
一. C++的对象成员变量的初始化发生在进入构造函数本体之前
如上代码会对成员先进行默认构造函数的调用,之后在进行赋值。
解决办法是: 使用 member initalization list 初始化列表替换赋值动作。 为了规范统一:将全部成员(无物也要使用初始化列表初始化)
注意:C++类对象的初始化次序: base class 总是早于其 derived class 被初始化, class 的成员变量总是以其声明次序被初始化
二. C++对于定义在不同编译单元内的non-local static 对象的初始化相对次序无明确定义。
解决办法就是使用 Singleton 使得 non-local static 搬到自己的专属函数中
C++保证, 函数内的 local static对象会在 “首次遇上该对象之定义上” 被初始化。
多线程环境下执行顺序的麻烦性: 尽量在单线程运行期按一定顺序初始化这些 static
总结:
一. 检阅一个 empty 类编译器为其做的事情
二: 编译器合成的函数做的事情
发现: operator= 与 copy构造函数 都是编译器合成, 内置类型的成员使用拷贝bit方式,非内置则调用 其定义的 operator= 与 copy构造函数从右侧操作数拷贝数据
三. 注意点:
总结:
编译器可以暗自为class 创建default构造函数,copy构造函数,copy assignment 操作符,以及析构函数。
一. 最简单的拒绝(copy构造函数与 copy赋值运算符)办法是 声明其为 private
一:问题浮现: 销毁一个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 析构函数
首先C++并不禁止析构函数抛出异常,但在析构函数中抛出异常很容易导致内存泄漏(程序过早结束)
一: 验证析构函数抛出异常的问题
二: 使用最佳策略解决该问题,避免析构函数传播异常
我们要对 “导致 close 抛出异常” 的情况做出反应
重新设计 DBConn接口,使客户对有机会对可能出现的问题作出反应
1: 管理类提供一个 close 函数,赋予客户一个机会处理因该操作而发生的异常。
2: 管理类设置标志位并在析构函数调用时检测其是否正常关闭,如果未关闭,则正常关闭
3: 第二步在析构函数种再次关闭失败后,我们将又退回 “强迫结束程序或吞下异常的套路”
总结:
析构函数绝不要吐出异常, 如果一个被析构函数调用的函数可能抛出异常,析构函数
应该捕捉任何异常, 然后吞下他们(不传播)或结束程序。
如果客户端需要对某个操作函数运行期间抛出的异常做出反应,那么 class 应该提供一个 普通函数(而非在析构函数种)执行该操作
一: 证明在 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 的通过必要参数而实行的普通函数
总结
这份协议可以说是为了实现连锁赋值而 创造的协议
总结:
连锁赋值几乎被所有内置类型和标准库程序提供的类型, string,vector都遵守,因此
我们自定义的也最好共同遵守
自赋值是十分没有必要的,但也是代码漏点很多的一个问题
确保当对象自我赋值时 operator= 有良好的行为(鲁棒性,通用性更强),其中技术包括
确保任何函数如果操作一个以上的对象,且多个对象都是同一个对象时,其行为仍然正确
如果拒绝编译器自动生成copying函数,如果你的代码不完全,他们也不告诉你!!!
一: 局部拷贝的错误
这里如果没有复制新添加的变量,编译器也是不会进行提醒的,因为你已经拒绝编译器进行为你生成 copy 函数
实验二: 继承拷贝的错误
继承一个基类后进行编写派生类的 copy 函数时应注意编译器不会进行对 派生类的基类部分自动拷贝
需要手写基类部分的复制
总结:
基于对象的资源管理办法,是十分有效的
一: 体验基于对象资源管理的方法
自行编写的写法
总结:
资源的类型并非都是 heap+based 资源时,你需要建立自己的资源管理类
一: 体验资源管理类中copying行为带来的问题
面对资源 copy 的动作有如下解决方案
二: 禁止 copy 行为
总结
提供原始资源的访问以方便客户
例如: shared_ptr 的get获取原始指针
总结
一: 我们来演示一个复杂的错误(异常导致内存泄漏)
c++ 语言的函数传参调用顺序弹性很大
可能会出现调用顺序如下的表现:
更好的办法
理想上,如果客户企图使用某个接口而却没有获得他预期的行为,这个代码不能通过编译
一:明智而审视地导入新类型对于预防“接口被误用” 有神奇疗效
二: 以函数替换对象,预先定义有效的 对象使得接口更具备安全性
另外: 除非有非常好的理由,否则应令 type 与 内置 type 提供一致的行为接口。
总结:
如何设计高效的 classes 呢, 遵循问题产出设计规范:
一: 使用引用类型能提升效率问题
二:使用引用进行传参可以实现多态且避免对象切割问题
总结
一: 拒绝返回局部作用域的局部变量的引用,那其实指向了一片不存在的区域
总结:
一: 如何对成员变量进行有效的控制
总结:
切记将成员变量设置为 private,这可赋予客户访问数据的一致性,可细微划分访问控制,允许约束条件 得到保证,并提供 class 作者以充分的实现弹性。
仅存在两种访问权限: private(提供封装)和 其他(不提供封装),protected 并不比 public 更具封装性。
一: 在许多情况下 非成员非友元函数的做法比 member 好得多。
二: C++ 对于命名空间的妙用,适用 non-member,non-friend
将所有便利函数防止多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多 non-member non-frient 函数到此命名空间内。
总结
一:. 实现一个 pimpl 手法的类, 并写出其 特例化的swap 函数
总结:
尽可能延后的真正意义
1: 不仅只是延后变量的定义,直到非带使用该变量的前一刻为止,甚至
应该尝试延后这份定义直到能够给它初值实参为止。
2. 不仅能够避免构造(析构)非必要对象,还能避免无意义的default的构造行为。
一: 1: 尽可能延后能优化代码
二:循环中所使用变量需不需要延后,还是提前定义?
总结
尽可能延后变量定义的出现,这样做可以增加程序的清晰度并改善程序效率。
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 派生类做的事情,使用多态性质即可 (推荐)
总结:
handles(指针,引用,迭代器)
一: 探索返回对象内部成分带来的弊端
总结
异常抛出时,带有异常安全性的函数会
异常安全性提供以下三个保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
没有任何对象或数据结构因此而败坏,所有对象都处于一种内部前后一致的状态
强烈保证:如果异常被抛出, 程序状态不改变。
调用可能抛出异常的操作,如果调用失败应该恢复到调用之前的状态,调用成功就是完成成功
不抛出异常保证: 承诺绝不抛出异常, 因为他们总是能完成它们原先承诺的功能。
(作用于内置类型) nothrow
一: 使用 copy and swap 实现强烈保证级别的异常安全性。
inlining 在C++程序中是编译期行为
过度热衷inlining会造成程序体积体大,即使有虚拟内存,inline 造成
的代码膨胀亦会导致额外的换页行为,降低指令高速缓存装置的击中率,降低效率。
virtual 的调用会使得 inlining 落空 (运行期确定的多态行为,当然会使得inlining落空)
大部分调试器面对 inline 函数都束手无策(不存在的函数设置断点 真的很荒唐)
总结:
一: 相依赖于声明式,而不要相依定义式实现一个类,遵循以下
总结:
C++进行面向对象编程最重要的规则是:public inheritance 意味着是 is-a 的关系(正向一类)
一:验证 ia-a: D 继承 B,则每一个类型为D的对象本身也是类型为B的对象,反则不成立
虽然正方形继承自长方形,但适用于长方形的行为并不适合正方形, 这显然是非常不正确的,
继承后遵循 is-a 的行为,因此正方形必须能适应长方形的所有行为。
总结:
public 继承意味着 is-a,适用于 base class 的事情也一定适用于 derived class身上
未遵循is-a的代码即使编译通过,但不保证程序的行为是正确的
一:derived class 内的名称会遮盖 base classes 内的名称, 在public 继承下从来没有人希望如此
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 好用)
总结:
virtual 函数以外的其他选择:
一: 使用 NVI(Non-Virtual Interface) 手法实现 Template Method 模式(主张 virtual应该总是private)
私有impure virtual 使得派生类重新定义, member function 调用其 impure virtual 实现(且在调用前后都可以做额外的事情)
二: 以策略模式替换 virtual, 以基于对象的思路(std::bind + std::function)替换virtual+多态
策略模式的构成:
演示1: 不使用bind+function,使用指针作为回调函数实现策略模式
Strategy 提供了有趣的弹性:
演示2: 由 std::function+std::bind 完成 Strategy 模式
std::function 这样的对象可保持任何可调用物质函数指针,函数对象,成员函数指针(而非仅函数指针)且具有一定的兼容性,可调用物的参数可以隐式转换为其function声明的参数以及返回值能隐式转换为 function声明的返回值 就可以兼容
绝不重新定义继承而来的 non-virtual 函数
由于派生类永远不会重新定义继承来的 non-virtual 函数,所以我们这条是针对 virtual 而言的
一: 缺省参数值执行的是静态绑定, 而不是运行期再次确定,缺省参数遵循调用者的静态类型而定
shape_sptr 的静态类型是 shape 的智能指针, 因此缺省值是 shape的draw的缺省值,而非 rectangle 的draw的缺省值,C++ 坚持以这种夸张的方式来运作是基于运行期效率来做的。
解决: 使用 NVI手法替换虚函数所表现的行为不是很满意的情况
base 的 public non-virtual 函数调用 private virtual, virtual 可被重新定义
我们让 non-virtual 函数指定缺省参数, private virtual 负责真正的工作
总结:
复合包括
关联: 彼此并不负责对方的生命周期一般使用指针或者引用
聚合: 对象之间的关系表现为分为整体和局部, 整体部分并不负责局部对象的销毁
组合 : 对象之间的关系表现为分为整体和局部, 整体部分负责局部对象的销毁
is-a : 是继承,意味着 派生类必须能作为基类 完成基类能完成的所有功能
has-a: 是复合 挑选合适的关系帮我们完成更好的面向对象设计
一: 做一个必须以复合完成的功能实例, 以 list 实现 set
一: 理解private 继承所能实现的效果
编译器不会自动将一个derived class 对象转换为一个 base class 对象
private 继承意味着 根据某物实现出,而非 is-a 语义,只有实现部分被继承,接口不会被继承
二: 其实使用 public 继承加复合 比 private继承 更好
private 继承主要用于 “当一个意欲” 成为 derived class 者想访问一个
意欲成为 base class 者的protected成分,或为了重新定义一或多个 virtual 函数
空间最优化会促使你选择 private 继承而非 “继承加复合”
空类的sizeof为1,c++会为其安插一个char到空对象中,使得空对象间有一定的区分
EBO:空间基类最优化,将空类(未含成员的类)空间在继承后优化掉
考虑过所有方案后,仍认为 private 继承是“表现程序内两个 classes 之间的关系”的最佳办法,才使用
总结:
一: 多个base class 的成员名字相同造成的歧义
虚拟继承带来的影响
使用virtual继承的那些 classes 所产生的对象往往比使用 non-virtual
继承的兄弟们体积大(安插共享指针),访问 virtual base classes 成员也慢
virtual base 的初始化责任是由继承体系中的最底层(最高级别的派生类)
负责。
三: 正确使用多重继承
使用 CPerson 表现人的实体,公有继承接口(IPerson)与 私有继承实现 (PersonInfo)
由于CPerson是转调Personinfo的实现来完成自己的接口,且需要重新定义 virtual 函数
那么就具备 (根据某物实现的语义), 使用private继承或者 public继承+复合 来完成
总结:
一对template参数而言,接口是隐式的,基于有效表达式,多态是通过template具现化和函数重载解析发生于编译器。
模板在实例化时会进行带入,之后发生编译器多态,对有效表达式进行检测
有效表达式即隐式接口(并非知道该隐式接口是否是有效,仅在具现化模板时进行编译期检测)
总结:
一. typename 定义模板类型
嵌套从属类型名称不需指定 typename 的情况:
总结:
一: 善用模板特例化解决特殊情况
这里: 如果 Company 没有 sendClearText,就会使得调用失败
使用模板特例化完成针对 CompanyZ 的MsgSender(使Company具现化在定义时)
二: C++ 模板继承时由于 base class 并没有具现化,C++并不知道继承的究竟是什么类,因此其内含的 members全部被隐藏, 因此继承而来的 sendClearMsg 会被隐藏
总结:
一. 非类型模板参数会带来代码膨胀
二: 共性与变性的分析, 对类以 private 继承 或 复合 来抽离代码,并以函数参数或class成员变量替换 template 参数
三. 类型模板参数也会导致膨胀,比如 vector 与 vector, 但只要二进制表述相同(参考指针的空间字节数)我们可以实现共享码
总结
Templates 生成多个classes 和 多个函数,所以任何 template 代码都不应该与 某个造成膨胀
的 template 参数形成相依关系
因非类型模板参数造成的代码膨胀, 往往可以消除, 做法是以函数参数或class成员变量替换templates参数
因类型参数而造成的代码膨胀,往往可降低,做法是以相同二进制表述的具现类型共享实现码。
一:同一个template的不同具现体之间不存在固有关系,需要定义泛化的隐式转换
具现体的基本类型存有 转换(派生类指针到基类指针的转换),但具现体并不具备
解决: 写出一个泛化copy构造函数来兼容,限制工作交给实际的类型去转换
二: 泛化的 copy 构造函数并不会阻止编译器生成它们自己的 copy 构造函数, 如果你想完完全全控制 copy动作,请写出泛化版本与非泛化版本
总结:
一. 复现 24 (”唯有non-member函数才有能力在所有实参上实施隐式类型转换“)在template不适用
原因: template 在实参推导过程中并不考虑”通过构造函数而发生的隐式类型转换“,因此 operator*在该情况下并不会被推导出
二. 解决: 以template 相关的 ”函数支持所有参数之隐式类型转换“时,请将那些函数定义为 class template 内部的friend函数
在template class 中指涉 operator(**)函数为友元,由于类模板推导不依赖 tempalte 实参推导(施行于 function templates上),所以编译器总是能够在class Rational 具现后找到 友元函数 operator*的声明 (并因此缓和 template 实参推导)
注意: friend 仅代表声明, 我们需要定义,可以在友元声明处直接定义,或定义额外的non-member供友元调用
这里解释一下
总结:
一: 迭代器类型的区分与traits(类型萃取技术)的实现
STL 中 通过萃取容器得到其 iterator 类型之后才能实施不同的算法
萃取技术的实现:
二: 模仿 STL 中的做法,使用重载解决 CharacterTraits::category 在编译阶段能完成
但由于if 的原因却是推迟到运行期核定的问题 得到解决
三: 如何正确使用一个 traits class
建立一组重载函数(劳工)或函数模板,彼此之间的差异仅在各自的 traits 参数,令每个函数实现码与 其接收之 traits 信息相应和
建立一个控制函数(身份像工头)或函数模板(advance),它调用上述那些 ”劳工函数“并传递 traits class 所需信息
总结:
一: 了解什么是 template 元编程(TMP template metaprograming)
简介: TMP 是编写 模板程序 并执行于编译期的过程(也可以是说执行于C++编译器内的程序)。
优点:执行与编译期,检测错误更早,程序更高效,较小的执行文件,较短的运行期,较少的内存需求。
缺点:编译时间变长。
二: TMP 实现计算阶乘
总结:
一: 认识 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 任务
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 动作进行复用是必要的
总结:
总结:
一: 编写 operator new 需注意的规矩
基本实现版本:
二. 编写 operator delete 需注意的规矩
伪码:
总结:
一: 关于使用 new 因构造函数抛出异常出现的内存泄漏问题
placement new 要与 placement delete 对应 才能使得运行期系统寻找到 处理异常导致内存泄漏问题。
问题:
new 一个对象有两部,一部是分配内存,一步调用对象的构造函数
解决: 定义参数个数与类型相同的 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, 请确定别无意识第遮掩了 他们的正常版本