第六章:继承与面向对象设计
-
条款32:确定你的public继承塑模出is-a关系
-
条款33:避免遮掩继承而来的名字
例如:
-
条款34:区分接口继承和实现继承
接口继承和实现继承不同。在public继承之下,derived class 总是继承base class 的接口
声明一个pure virtual 函数的目的是为了让derived classes只继承函数接口。
声明 impure virtual 函数的目的是让derived classes 继承该函数的接口和缺省实现。
声明 non-virtual 函数的目的是为了令derived classes 继承函数接口并强制性实现。代表的是不变性。
-
条款35:考虑virtual 函数以外的其他选择
NVI 手法,Template Method设计模式:通过 public non-virtual 成员函数间接调用 private virtual函数。把non-virtual函数称为virtual函数的外覆器。
借由函数指针实现Strategy模式。运用函数指针替换virtual 函数
-
条款36:绝不重新定义继承而来的non-virtual 函数
-
条款37:绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数-你唯一应该重写的东西--却是动态绑定。
知识点:
静态类型,就是它在程序中被声明时所采用的类型。发生在编译期
不论它们真正指向什么,它们的静态类型都是Shape*
动态类型,就是目前所指对象的类型。发生在运行期。以上例而言,pc 的动态类型是Circle*,pr 的动态类型是Rectangle*。
动态类型,可在程序执行过程中改变(通常经由赋值动作)。
Virtual 函数是动态绑定的,意思是调用一个virtual 函数时,究竟调用哪一份函数实现代码,取决于发出调用的对象的动态类型。
-
条款38:通过复合塑模出has-a 或“根据某物实现出”
-
条款39:明智而审慎地使用private继承
private 继承意味着只有实现部分被继承,接口部分被略去。也意味着“根据某物实现出”。所以尽可能使用复合,必要时使用private继承
-
条款40:明智而审慎地使用多重继承(MI)
第七章 模板与泛型编程
知识点:
- 运行期多态通过虚函数发生在运行期
- 编译器多态:以不同的模板参数具现化导致调用不同的函数。(泛型编程)
- 显式接口:由函数的签名式(函数名称、返回类型,参数类型)组成。例如
- 隐式接口:由有效表达式组成
-
条款41:了解隐式接口和编译器多态
class 和 template 都支持 接口和多态。
对class 而言,接口是显式的,以函数签名为中心。多态则是通过virtual 函数发生于运行期。
对 template 参数而言,接口是隐式的,多态通过template 具现化和函数重载解析发生于编译期。
-
条款42:了解typename 的双重意义
任何时候,当你想要在template 中 refer to一个嵌套从属类型名称,就必须在它前面加上关键字 typename。typename 只被用来验明嵌套从属类型名称。
声明 template 参数时,前缀关键字 class 和 typename 可互换。如 template < typename C> 或者 template<class C>
-
条款43:学习处理模板化基类内的名称
可在derived class template 内通过 "this->" 指涉 base class template 内的成员名称。
-
条款44:将与参数无关的代码抽离template
因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class 成员变量替换 template参数
-
条款45:运用成员函数模板接受所有兼容类型
可以根据 SmartPtr<U>生成一个SmartPtr<T>,类型的兼容。
shared_ptr 的部分定义
-
条款46:需要类型转换时请为模板定义非成员函数
当我们编写一个class template,而它所提供的 “与此template 相关的” 函数支持“所有参数的隐式类型转换”时,请将那些函数定义为“class template 内部的 friend 函数”。
如:
- 当定义 Rational<int> oneHalf (1,2); Rational<int> result=oneHalf * 2; //无法通过编译。编译器无法推算出 2 的数据类型。因为template 实参推导过程中从不将隐式类型转换函数纳入考虑。
我们需要这样修改:
- 因为当对象oneHalf 被声明为一个 Rational<int>,class Rational<int> 于是被具现化,而作为过程的一部分,friend 函数 operator*(接受Rational<int> 参数)也就被自动声明出来,后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数。
-
条款47:请使用 traits classes 表现类型信息
-
条款48:认识 template 元编程
Template metaprogramming (TMP) 模板元编程 是一个图灵机。
TMP 可将工作由运行期移往编译期,因而得以实现早期错误侦测和更好的执行效率。
第八章 定制new 和delete
-
条款49:了解new-handler 的行为
当 operator new 抛出异常以反映一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler,调用set_new_handler可以指定该函数。
设计一个良好的new-handler 函数必须做以下事情:
- 让更多内存可被使用。
- 安装另一个new-handler (只要调用set_new_handler),让new-handler修改自己的行为
- 卸除new-handler,也就是将null 指针传给set_new_handler,一旦没有安装任何new-handler行为,operator new会在内存分配失败时抛出异常。
- 抛出 bad_alloc 的异常
- 不返回,通常调用abort 或 exit.
set_new_handler 允许客户指定一个函数,在内存分配无法获得满足时被调用。
-
条款51:编写new和delete时需固守常规
operator new,如果有能力供应客户申请的内存,就返回一个指针指向那块内存,否则抛出bad_alloc异常。operator new 实际上不止一次尝试分配内存,并在每次失败后调用new-handling 函数,这里假设new-handling 函数也许能够做某些动作将某些内存释放出来。只有当指向new-handling 函数的指针是null,operator new 才会抛出异常。
C++规定,即使客户要求0 bytes,operator new 也得返回一个合法指针。
non-member operator new 伪码
void* operator new (std::size_t size) throw (std::bad_alloc)
{
if(size==0) size=1; //处理0-byte申请,将它视为 1-byte申请
while(true){
//尝试分配 size bytes;
if (分配成功)
return (一个指针,指向分配得来的内存);
//分配失败
new_handler globalHandler =set_new_handler(0);
set_new_handler(globalHandler);
if(globalHandler) (*globalHandler)();
else throw std::bad_alloc();
}
}
operator new内含一个无穷循环,退出循环的唯一办法是:内存被成功分配或 new-handling函数做了一件条款49提到的事情,或是承认失败直接return。
-
条款52:写了placement new 也要写placement delete
假设你写了一个class 专属的operator new,要求接受一个ostream 用来log相关分配信息。
如果operator new 接受的参数除了size 之外还有额外参数,这个就是所谓的placement new。意味着带任意额外参数的new
典型的placement new 版本如下:
void* operator new (std::size_t, void* pMemory) throw(); //placement new
这个版本的new 已被纳入C++标准程序库,只要#include<new>就行。这个版本的new 用途之一是负责在vector 的未使用空间上创建对象。一个特定位置上的new。
当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生内存泄漏。
当构造函数抛出异常时,对应的placement delete 会被自动调用。如果没有抛出异常,当客户delete pw 时,调用的是正常版本的operator delete。