Effective C++ 条款26-55
5. 实现
条款26:尽可能延后变量定义式出现的时间
- 确定需要一个对象时再定义它并赋以初值,以免产生不必要的构造和析构成本
- 增加程序的清晰度并改善程序效率
条款27:尽量少做转型动作
const_cast<T>
将对象的常量性转除dynamic_cast<T>
执行向下转型,用来决定某对象是否归属继承体系中的某个类型reinterpret_cast<T>
执行低级转型,实际动作取决于编译器,不可移植static_cast<T>
强迫隐式转换- 以上四种新式转型,在代码中更容易被识别出来,转型动作的目标更加窄
- 转型并不是什么都不做,不管是隐式转换还是显式转换真的令编译器编译处运行期间执行的代码
- 单一对象可能拥有一个以上地址,通过偏移量在两个地址见切换,但是这种偏移量计算地址的行为具有不可移植性
- 如果派生类的成员函数要求先调用基类的成员函数,使用转型可能会出错,在书119页的例子中,实际操作是在当前对象的基类成分的副本上调用Window::onResize
- dynamic_cast的许多实现版本执行速度都很慢,深度继承或多重继承的成本更高,在认定为派生类对象身上执行派生类成员函数,但是只有指向基类的指针或者引用,为了避免这种操作,采用以下两个做法
- 在容器中存储直接指向派生类的指针,而不需要通过基类接口处理对象的需要,但是这种做法无法再同一个容器内存储指针,要处理多种窗口类型,可能需要多个容器,必须具备类型安全性
- 通过基类接口处理所有可能的各种Window派生类,在基类内提供virtual函数做所有相对派生类做的事情
- 避免连串的dynamic_cast,这样产生代码又大又慢且基础稳
- 优秀的C++代码很少使用转型
条款28:避免返回handles指向对象内部成分
- 成员变量的封装性最多只等于“返回其引用”的函数访问级别
- 如果const成员函数传出一个reference,引用所指的数据和对象自身有关联,函数的调用者可以修改这个数据
- 引用、指针、迭代器都是号码牌(handle),返回代表对象内部数据的handle会降低对象的封装性,虽然成员函数是const却造成对象仍然状态仍然可以被修改
- 对象内部数据就是成员变量,不被公开的成员函数也是对象内部的一部分,这就意味着不要令成员函数指向访问级别较低的成员函数
- 将返回类型限制为const,客户可以访问对象的内部数据,但是不可以修改它们
- 但是这种情况还可能导致空悬额handle
条款29:为“异常安全”努力是值得的
- 异常安全的函数有一下性质:不泄露任何资源;不允许数据破坏
- 确保互斥锁会被安全释放
- 异常安全码必须保证三种中的一种:1)基本承诺,如果异常抛出,程序内的所有事物仍然保持在有效状态下;2)强烈保证,如果函数成功则完全成功,如果失败则返回到函数调用之前的状态;3)不抛掷保证,承诺不抛出异常
- 用智能指针管理成员变量,删除动作在新图像被成功创建之后发生,
r1::shared_ptr::reset函数只有在其参数Image被成功生成之后才会被调用
std::tr1::shared_ptr<Image> bgImage; bgImage.reset(new Image(imgSrc))
- delete值在函数reset函数内被使用,如果这个函数从未被调用,则delete就不会发生
- 使用copy and swap策略,为要修改的原件创建一个副本,在副本上做改变,如果修改动作抛出异常,原对象没有发生改变,所有改变都成功用副本替换原对象
- 在函数内还包括另外两个函数,将会产生连带影响,如果这两个函数对非局部数据有连带影响时,很难提供强烈保证
- 如果系统内有一个函数不具备异常安全性,则整个系统都不具备异常安全性
条款30:透彻的了解inline,并且慎重的使用它们
- inline函数免除函数调用成本,本质上是将对函数的每一个调用都用函数本体替换,这样做会增加目标码的大小
- inline是对编译器的申请而不是强制命令,大部分编译器会拒绝太过复杂的内联函数
- 隐喻的方式是将函数定义在类定义式内部
- 明确声明的方法是在函数声明前面加上inline
- inline函数通常被放在头文件中,大部分inline函数都在C++编译器完成,
- template通常也被放在头文件中
- virtual函数的调用也会是inline函数的调用落空
- 编译器可能不会通过函数指针对内联函数进行调用
- 构造函数和析构函数在调用过程中会发生一些无法预料的事情,不要将它们设置为inline
- inline函数无法随程序库的升级而升级
- 一开始不要讲任何函数设置为inline,除非是必须设置为inline的或者是平淡无奇的(隐喻)内联函数
- 将大多数inline使用在小型、被频繁使用的函数上
- 不要因为函数模板出现在头文件中就将它设置为inline,有一些建置环境在连接期才能执行模板的具现化
条款31:将文件间的编译依存关系降至最低
- 类的定义文件和含入文件形成了编译依存关系,如果头文件中有一个发生了改变,则所有含入或使用类的文件都要重新编译,这种连串编译依存关系会对许多项目造成难以形容的灾难
- 需要实现的是,在类的接口被修改过后,才需要被重新编译
- 以声明的依存性替换定义的依存性,将一个类分为两个类,一个只提供接口、一个负责实现接口
- 尽可能使用对象的引用或者指针传递,用类的声明式代替定义式
- 将class定义式从函数声明所在的头文件转移到内含函数调用的客户文件,将并非真正必要的类型定义与客户端之间的编译依存性去除掉
- 为声明式和定义式提供两个不同的头文件,程序库客户通常#include一个声明文件而不是前置声明若干函数,这样的类称为handle classes
- 另一种方法是令实现函数是抽象基类,一一描述派生类的接口,不带成员变量和构造函数,只有一个虚析构函数和一组纯虚函数用来描述所有接口。抽象基类内允许出现非虚函数,这种非虚函数在继承体系内的所有类内的实现都相同
- 抽象基类的客户必须有办法为这种类创建新的对象,用工厂函数或者虚构造函数,返回指针或者智能指针指向动态分配的对象,对象支持抽象基类的接口, 这类函数通常抽象基类内被声明为static
- 具象类必须被定义出来,且真正的构造函数必须被调用
- 具象类实现:1)从抽象基类继承接口规格,然后实现出接口覆盖的函数;2)多重继承
6. 继承与面向对象的设计
条款32:确定public继承塑模出是一种关系
class D: public B
,可以说D也是一种类型为B的类,反之则不行,说明B比D更具一般性- 要保证D有B的所有特性,基类的每一件事也要适用于派生类
条款33:避免遮掩继承来的名称
- 内层作用域的名称会遮掩外层作用域的名称
- 派生类的作用域嵌套在基类作用域内
- 如果派生类的函数将基类的函数遮掩掉,则从名称查找的观点来看,基类的同名函数不在被继承
- 这种行为是为了避免从程序库或者框架内建立新的派生时从疏远的基类重载函数
- 使用using Base::mf1; //使基类内所有名为mf1的东西都在派生类内可见
- 如果是私有继承中,不想要调用某个函数,可以使用隐喻inline的转交函数遮掩
- 也可以使用inline转交函数将不支持using声明的编译器的函数汇入派生类作用域内
条款34:区分接口继承和实现继承
- 客户不能创建抽象类的实体,只能创建它的派生类的实体
- 成员函数的接口总会被继承
- 纯虚函数有两个突出特性:1)必须被任何继承他们的具象class重新声明;2)在抽象类中没有定义
- 声明纯虚函数的目的是让派生类只继承函数接口
- 可以为纯虚函数提供定义,但是调用时必须明确指出class的名称,
Shape::draw
- 为非纯虚函数提供更加平常、安全的缺省实现
- 派生类继承非纯虚函数的接口,非纯虚函数提供一份实现代码,派生类可能会覆写它
- 切断虚函数和缺省实现之间的连接,没有对虚函数指定行为,将虚函数改为纯虚函数,仅提供飞行接口,定义了defaultFly,如果想要使用缺省实现,在派生类中的fly函数inline调用defaultFly
- 类C必须提供自己的fly版本
- defaultFly是一个非虚函数
- 定义纯虚函数,然后在派生类中拥有一份自己的定义
- 基类中的非虚函数表现出不变性,不论派生类表现出多大的特异性,他的行为都不可改变
- 根据实际需求选择三种不同的成员函数,不要将函数全部声明为非虚函数,也不要全部声明为虚函数
条款35: 考虑虚函数以外的其他选择
一、借由non-virtual interface实现NVI模式
- 保留成员函数为共有非虚函数,调用一个在私有部分定义的虚函数来完成操作,这个虚函数可以通过派生类重新定义
- 这种在公有非虚成员函数中调用私有虚函数的称为(non-virtual interface NVI),就是所谓的Template Method设计模式的一种表现形式,将这个非虚函数称为虚函数外覆器
- 在类内部完成定义的成员函数是隐喻的inline
- 外覆器确保在虚函数被调用之前设定好场景,并在调用结束之后清理场景
- NVI手法下没有必要虚函数没有必要一定得是虚函数
二、借由函数指针实现Strategy模式
- 同一人物的不同类型实体可以有不同的健康计数函数
- 已知任务的健康指数计算函数可以在运行期变更
- 健康指数计算函数不再是类继承体系内的成员函数,这些计算函数没有特别访问计算对象的内部成分
- 这些函数没有访问对象的非公有信息,如果这些函数需要非公有信息的精确计算,只能通过弱化class的封装
三、借由tr1::function完成Strategy模式
- 对象可以持有任何可调用物,如函数指针、函数对象或成员函数指针,只要其签名式兼容于需求端
int (const GameCharacter&)
表示接受一个const GameCharacter的引用并返回int- 兼容的意思是调用对象类型和返回类型可以通过隐式转换复合要求
- 如果以tr1::function替换函数指针,将允许客户计算人物健康指数时使用任何兼容的可调用物
四、古典的Strategy模式
- 两个继承体系的基类,其中一个的类的每一个对象都包含一个指针指向来自另一个继承体系的对象
- 用继承体系内的虚函数替换另一个继承体系的虚函数
条款36:绝不重新定义继承而来的非虚函数
- 如果派生类重新定义了非虚成员函数,指向同一个对象的两个指针访问这个成员函数时会指向两个不同的对象,D对象表现出B或者D的行为,决定因素在于声明的类型
- 虚函数是动态绑定,两个调用都是指向D::mf();
- 在任何情况下不应该在派生类中重新定义一个非虚成员函数
条款37:绝不重新定义继承而来的缺省参数值
- 虚函数是动态绑定,缺省参数是静态绑定,静态绑定是前期绑定,动态绑定是后期绑定
- 静态绑定下,函数不从基类继承缺省参数值,
- 对象的动态类型是指目前所指的对象的类型
- 动态类型可以在程序执行过程中更改,
Shape* pc=new Circle;
- virtual是动态绑定来的,调用一个虚函数是,究竟调用哪一份函数实现代码,取决于发出调用的对象的动态类型
- virtual是动态绑定,而缺省参数值是静态绑定,可能会出现基类和派生类各出一半力完成声明式,这种方法可以有效提高运行期效率
- 如果给基类和派生类函用户同时传缺省参数值,将会发生代码重复和相依性
- 如果基类的缺省参数值改变,派生类的这些缺省参数值也必须改变,否则就会导致重复定义继承而来的参数值
- 缺省的参数值是静态绑定,而虚函数要覆写的东西是动态绑定
条款38:通过复合塑模出有一个或者根据某物实现出
- 复合是类型之间的一种关系,某种类型的对象中包含其他类型的对象
- 复合还叫分层,内含,聚合,内嵌
- 当对象发生在应用域对象之间表现出has-a的关系,人、骑车、视频、画
- 当对象发生在实现域对象之间表现出根据某物实现出的关系,有缓冲区、互斥器、查找树
- 如果两个类之间并非is-a的关系,但是Set可以根据list对象实现出来,Set成员函数可以大量依靠list及标准程序库其他部分提供的机能来完成
条款39:明智而慎重的使用private继承
- 如果继承类型是private,编译器不会再自动将一个派生类对象转换为一个积累对象
- 私有继承的派生类成员都是private属性
- 使用D继承B,是要采用B内已经备妥的某些特性
- private是一种实现技术,private意味着部分继承,接口被略去
- 在基类内声明一个嵌套式私有类,可以有效阻止派生类重新定义虚函数
- 可以将基类和派生类的编译依存性降至最低
- 私有继承主要用于一个派生类想要访问基类的保护成分,或者重新定义一个或多个虚函数,涉及空间最优化使用私有继承
- 私有继承只适合处理的类不带任何数据,没有非静态成员函数,没有虚函数,没有虚基类
- 空白基类最优化一般只有在单一继承下才可行
- 不是is-a关系的两个类,一个类需要访问另一个类的保护成员,或者需要重新定义一个或者多个虚函数,私有继承可能是正统设计策略
- 在考虑了其他方法,但是仍然认为私有继承是最佳方法就使用它
条款40:明智而慎重的使用多重继承
- 多重继承造成从一个以上的基类继承相同的名称,根据重载函数调用原则,两个函数有相同的匹配程度就会造成歧义,为了解决这个歧义,指明调用的哪一个是基类内的函数
- 多重继承是继承一个以上的基类,但是这些基类不在继承体系内有更高级的基类,否则如果造成钻石型多重继承,将会导致复制路径歧义
- 将造成歧义的两个基类成为虚基类,继承时采用虚继承
- 使用共有继承时,应该用虚公有继承,虚继承产生的对象体积较大,访问成员变量的速度也比较慢
- 如非必要不要使用虚继承,如果使用虚继承尽量避免在其中存放数据
- 抽象类无法被实体化创建对象,只能用累的指针和引用来编写程序
- 抽象类可以用工厂函数将派生类实体化
7. 模板和泛型编程
条款41:了解隐式接口和编译期多态
- 显式接口在源码中明确可见,基类对象对基类的一些虚成员函数的调用表现出运行期多态
- 模板函数内,模板类型必须支持的函数和比较运算是模板类型的隐式接口
- 模板具现化发生在编译期,以不同的模板参数具现化会调用不同的函数,这就是编译期多态
- 显式接口由函数的签名式构成,隐式接口由有效表达式构成
- 模板的多态通过模板具现化和函数解析完成的,都在编译期完成,无法在模板中使用不支持模板要求的隐式接口的对象
条款42:了解typename的双重意义
template<typename T> class Widget;
和template<typename T> class Widget;
声明参数时typename和class完全一样- 两个local变量,有从属名称和非从属名称
- 嵌套从属名称可能导致解析困难,必须确定声明确实是一个类型,
typename C::const_iterator
放置关键字typename告诉C++这是个类型 - typename不可以出现在基类list内的嵌套从属类型名称之前,也不可以在成员初值列中作为基类修饰符
- typename用于声明嵌套从属类型名称,用typedef设定名称代表,
typedef typename
并列合理 - typename在不同的编译器上兼容性有问题,可能会在移植性上出现问题
条款43:学习处理模板化基类内的名称
- 模板参数在运行时确定,所以在编译期编译器无法调用基类的成员函数,因为无法辨别被调用函数的作用域
template<>
表示这既不是模板也不是标准类template<> class MsgSender<CompanyZ>
模板全特化,一旦参数被定义为CompanyZ,再也没有其他模板参数可供变化- 编辑器往往会拒绝在模板化基类内寻找继承来的名称
- 解决4的问题:1)在基类函数调用动作之前加上this->;2)加上using声明,将基类名称带入派生类作用域内;3)明白指出被调用的函数在基类的作用域内
- 5-3)如果被调用的是虚函数,这个操作会关闭virtual绑定行为
条款44:将与参数无关的代码抽离templates
- 模板生成多个类或多个函数,任何模板代码不该与某个造成膨胀的模板参数产生相依关系
- 费类型模板参数造成的代码膨胀可以用函数参数或者class成员变量替换
- 因类型参数造成的代码膨胀,可以通过完全相同的二进制表述的具现类型共享实现码
条款45:运行成员函数模板接受所有兼容类型
- 智能指针是行为像指针的对象,提供指针没有的机能
- 如果用两个base-devided关系的类型具现化某个模板,产生出来的两个具现体并不具有base-devided关系
模板和泛型编程
- 用函数模板来生成无数个拷贝构造函数来完成从一个类型生成另一个类型的智能指针的操作
- 没有声明explicit,允许原始指针类型之间的隐式转换
- 加入get函数筛查返回智能指针对象所持有的原始指针的副本,可以在构造模板实现代码中约束转换行为
- 只有存在隐式转换时可以通过编译
- tr1::shared_ptr中允许shared_ptr隐式转换成另一个shared_ptr类型,但是从内置指针或者其他智能指针隐式转换不被认可,显式强制转换可以cast
- 使用成员函数模板生成可接受所有类型的函数
- 如果声明用于泛化copy函数或泛化复制操作,还需要声明正常的拷贝构造函数和拷贝赋值运算符
条款46:需要类型转换时请为模板定义非成员函数
1 | Rational<int> oneHalf(1,2);//从int推断出实例化运算符的T是int |
- 要使用隐式转换函数,必须要先知道那个函数存在,为了知道这个必须先为相关的函数模板推导出参数类型,然后才能将适当的函数具现化出来
- 模板实参的推导过程中不考虑采纳通过构造函数发生的隐式类型转换
- 编译器总能在类模板具现化时得知T,因此将重载运算符函数声明为
friend const Rational operator*(const Rational& lhs, const Rational& rhs);
- 当oneHalf被声明为Rational
,类模板被具现化出来,作为过程的一部分,friend函数的重载运算符*函数被自动声明出来 - 这个friend是非函数模板,编译器在调用它时可以使用隐式转换函数
- 这个函数被声明在类之内,没有被定义出来,无法连接定义在类外部的定义式
- 要在类内部完成所有实参的具现化,满足这一要求的类内部非成员函数,只有定义在类内部的友元函数
- 如果要调用外部的辅助函数,必须先将友元函数的声明放在前面
条款47:请使用traits class表现类型信息
- 工具性模板函数advance将某个迭代器移动给定的某个位置,但是只有随机访问的迭代器才能完成+=操作,其他的只能反复执行++或者–
- 如果要使用+=操作,必须要首先判断迭代器是否是随机访问迭代器,traits允许在编译器间取得某些类型信息
- traits是一种技术,对内置类型和用户自定义类型表现的一样好,针对迭代器的被命名为iterator_traits
- 针对每一个类型IterT,在结构iterator_traits
一定会定义某个typedef名为iterator_category,来确认IterT的迭代器分类 - 指针不能嵌套typedef,指针的行径和随机访问迭代器类似
- 利用重载完成编译器匹配,利用重载的doAdvance在编译期完成参数匹配,获得正确的参数类型,在advance函数体内调用doAdvance
条款48:认识模板元编程(Template metaprogramming)
- TMP可以实现将工作从运行期转移到编译期,使错误可以在编译期被发现,上一个条款中的traits就是TMP
- 上例中traits_base版本,对其他迭代器调用+=,如果迭代器不是随机访问迭代器会导致调用失败,而TMP在编译期完成能够保证代码的正确调用
- TMP是函数式语言,TMP的循环借由递归完成
- 使用TMP可以达成很多目标,如:1)确保度量单位正确结合;2)优化矩阵运算;3)生成客户定制的设计模式
8. 定制new和delete
- C++内存管理例程主要是分配例程和归还例程
- 多线程环境下的内存管理需要适当的同步控制、有锁算法、防止并发访问等操作,否则会很容易导致堆的数据结构
- operator new和operator delete只能用来分配单一对象
- arrays用的内存用operator new[]和operator delete[]分配
- 堆内存是由容器所拥有的分配器对象管理,不是由new和delete直接管理的
条款49:了解new-handler的行为
- 如果operator new无法满足某一内存分配需求,会抛出异常,调用一个客户指定的错误处理函数,就是所谓的new-handler
1 | namespace std{ |
2. 一个良好的new-handler函数必须做到以下事情
- 让更多内存可以被使用
- 安装另一个new-handler
- 卸除new-handler
- 抛出bad_alloc(或派生自bad_alloc)的异常
- 不返回,调用abort或exit
3. 可以为每一个class提供自己的set_new_handler和operator new,自定义的Widget的operator new做以下事情:
- 调用标准set_new_handler告知Widget的错误处理函数
- 调用global operator new执行实际的内存分配
- 如果global operator new能够分配足够一个Widget对象所用的内存,Widget的operator new返回一个指针指向分配所得
4. set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
5. nothrow是一个局限性工具,只能保证operator new不抛出异常,但是构造过程中,构造函数仍然有可能会抛出异常
条款50:了解new和delete的合理替换时机
1. 要替换编译器提供的operator new和operator delete的三个常见理由:
- 用来检测运行上的错误
- 强化效能
- 收集使用上的统计数据
- 增加分配和归还的速度
- 降低缺省内存管理器带来的空间额外开销
- 弥补缺省分配器中的非最佳齐位
- 将相关对象成簇集中
- 获得非传统行为
2. 许多计算机体系结构要求特定的类型必须放在特定的位置上,称为齐位
- malloc在齐位要求下工作,operator new返回得自malloc的指针是安全的
- 如果没有获得适当齐位的指针,将会导致程序崩溃或者执行速度变慢
条款51:编写new和delete时需固守常规
- 实现一致性的operator new必须返回正确的值;内存不足时调用new-handing函数;必须有对应零内存需求的准备;避免不慎掩盖正常形式的new
- operator new不止一次的尝试分配内存,每次失败后都调用new-handling函数,假设这里的new-handling函数能够释放部分内存,只有new-handling函数的指针是null时,operator new才会抛出异常
- operator delete删除null指针永远安全
- 如果基类没有虚析构函数,则派生类对象传给operator delete的size_t数值可能存在错误,如果检查大小发生错误则申请调用::operator delete
条款52:写了placement new也要写placement delete
- 构造对象并分配动态内存时,如果内存分配完之后,构造函数出错,要使用正确的delete释放内存,如果使用的是带有附加参数的new,系统无法辨别出正确的delete
- 如果operator new函数除了必须有的size_t还有其他参数,这个函数就是placement new
- 在运行期选择寻找参数和类型与placement new相同的placement delete释放内存,如果没有找到,则什么都不做
- 基于上一条,我们有必要声明一个和placement new参数和类型相同的placement delete
- placement delete只有在伴随placement new调用而出发,删除一个对象调用正常的operator delete
- 成员函数的名称会掩盖外围作用域的相同名称,同时要避免类专属的new掩盖客户期望的其他new
- 基于6,建立一个基类包含所有正常形式的new和delete,要用自定义形式扩充标准形式的客户,利用继承机制和using声明使基类内的new和delete可见
9. 杂项讨论
条款54:熟悉包括TR1在内的标准程序库 Technical Reprot 1
TR1叙述了14个新组建,全部存放在std命名空间内,嵌套命名空间在tr1内,如std::tr1::shared_ptr
- 智能指针
- tr1::function,可以表示任何可调用物,如函数或函数对象,这是一个模板,以目标函数的签名为参数
- tr1::bind,第二代绑定工具。做STL绑定器bind1st和bind2nd所做的每一件事,且可以和const和non-const成员函数协同运作,可以和引用参数协同运作,不需要写作就可以处理函数指针
- hash tables,实现set,multiset,map和multi-map,名称是tr1::unordered_set等
- 正则表达式
- tuples变量组,是标准库程序pair模板的新一代,tr1::tuple可以支持任意个数的对象
- tr1::array,支持begin和end的数组,大小固定不适用动态内存
- tr1::mem_fn,语句构造上与成员函数指针一致的东西,纳入并扩充了C++98的men_fun和mem_ref
- tr1::reference_wrapper,让引用的行为更像对象,可以造成容器像引用,但是容器只是持有对象和指针
- 随机数生成工具,超越rand
- 数学特殊函数,Languerre多项式、Bessel函数、完全椭圆积分等
- C99兼容扩充
- type traits,提供编译期信息
- tr1::result_of,是一个模板,用来推导函数调用的返回类型
TR1是对程序标准库的纯粹添加,没有任何TR1组建用来替换,早期代码仍然适用