1、仔细区别 pointers 和 references
当你知道你需要指向某个东西,而且绝不会改变指向其他东西,或是当你实现一个操作符而其语法需求无法由 pointers 达成,你就应该选择 references;任何其他时候,请采用 pointers
2、最好使用 C++ 转型操作符
static_cast:拥有与C旧式转型相同的威力与意义,但不能移除表达式的常亮性,这一能力交给了const_cast
const_cast:用来改变表达式中的常量性或变异性(volatileness).
dynamic_cast:用于继承体系中, 将基类的引用或指针转换为派生类的引用或指针
reinterpret_cast:与编译平台相关, 不具有移植性, 用于转换函数指针类型. --- 函数指针转换
3、绝不要以多态(polymorphically)方式处理数组
多态(polymorphism)和指针算术不能混用
数组对象几乎总是会涉及指针的算术运算,所以数组和多态不要混用,通过基类指针删除一个由派生类对象构成的数组, 其结构是未定义的.
4、非必要不提供 default constructor
class缺乏一个default constructor,使用这个class时会存在某些限制。
在产生数组的时候,没有办法为数组中的对象指定constructor自变量(通过new []创建时)。
这些class不适用于许多template-based container classes。
virtual base classes如果缺乏default constructor,与之合作的将是一种惩罚。因为virtual base class constructors的自变量必须由欲产生对象的派生层次(most derived)最深的class提供。于是,一个缺乏default constructor的virtual base class,要求其所有的derived classes-无论距离多么遥远-都必须知道其意义,并且提供virtual base classes的constructors自变量。
但是,添加无意义的default construtors,也会影响classes的效率。
如果member functions必须坚持字段是否真的被初始化了,其调用者必须为测试行为付出时间代价,并为测试代码付出空间代价,因为可执行文件和程序库都变大了。万一测试结果为否定,对应的处理程序又需要一些空间代价。
如果class constructors可以确保对象的所有字段都会被正确地初始化,上述所有成本便可以免除。
所以,如果default constructors无法提供这些保证,那么最好避免让default constructor出现。虽然可能会对classes的使用方式带来某些限制,但同时也带来一种保证:当你真的使用了这样的classes,可以预期它们所产生的对象会被完全地初始化,实现上亦富有效率。
5、对定制的 “类型转换函数” 保持警觉
单自变量 constructors 可通过简易法(explicit 关键字)或代理类(proxy classes)来避免编译器误用
隐式类型转换操作符可改为显式的 member function 明确调用来避免非预期行为
6、区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式
前置式累加后取出,返回一个 reference;后置式取出后累加,返回一个 const 对象(使得i++++不合法)
处理用户定制类型时,应该尽可能使用前置式 increment;后置式的实现应以其前置式兄弟为基础
7、千万不要重载 &&
,||
和 ,
操作符
&&
与 ||
的重载会用 “函数调用语义” 取代 “骤死式语义”;
,
的重载导致不能保证左侧表达式一定比右侧表达式更早被评估
8、了解各种不同意义的 new 和 delete
new operator
、operator new
、placement new
、operator new[]
;delete operator
、operator delete
、destructor
、operator delete[]
9、利用 destructors 避免泄漏资源
以一个对象存放“必须自动释放的资源”,并依赖该对象的destructor释放。在 destructors 释放资源可以避免异常时的资源泄漏
10、在 constructors 内阻止资源泄漏
由于 C++ 只会析构已构造完成的对象,因此在构造函数可以使用 try...catch 或者 unique_ptr(以及与之相似的 classes) 处理异常时资源泄露问题
11、禁止异常流出 destructors 之外
避免 terminate 函数在 exception 传播过程的栈展开(stack-unwinding)机制中被调用
协助确保 destructors 完成其应该完成的所有事情
12、了解 “抛出一个 exception” 与 “传递一个参数” 或 “调用一个虚函数” 之间的差异
exception objects 总是会被复制(by pointer 除外,那将造成类型不吻合),如果以 by value 方式捕捉甚至被复制两次,而传递给函数参数的对象则不一定得复制
“被抛出成为 exceptions” 的对象,其被允许的类型转换动作比 “被传递到函数去” 的对象少(只有继承架构的类转换;从“有型指针”转为“无型指针”)
catch 子句以其 “出现于源代码的顺序” 被编译器检验对比,其中第一个匹配成功者便执行(first fit最先吻合),而调用一个虚函数,被选中执行的是那个 “与对象类型最佳吻合”(best fit) 的函数
13、以 by reference 方式捕获 exceptions
可避免对象删除问题、exception objects 的切割问题,可保留捕捉标准 exceptions 的能力,可约束 exception object 需要复制的次数
14、明智运用 exception specifications(在 c + + 11 中已弃用,并在 c + + 17 中删除)
exception specifications 对 “函数希望抛出什么样的 exceptions” 提供了卓越的说明
缺点:编译器只对它们做局部性检验、很容易不经意地违反,此外可能会妨碍更上层的 exception 处理函数处理未预期的 exceptions——即使更上层的处理函数已经知道该怎么做
15、了解异常处理的成本
粗略估计,如果使用 try 语句块,代码大约整体膨胀 5%-10%,执行速度亦大约下降这个数
因此请将你对 try 语句块和 exception specifications 的使用限制于非用不可的地点,并且在真正异常的情况下才抛出 exceptions
16、谨记 80-20 法则
软件的整体性能几乎总是由其构成要素(代码)的一小部分决定的,可使用程序分析器(program profiler)识别出消耗资源的代码,这远比靠“猜”来的要好
17、考虑使用 lazy evaluation(缓式评估)
以某种方式撰写classes,使它们延缓计算,直到那些运算结果刻不容缓被迫切需要为止。可应用于:Reference Counting(引用计数)来避免非必要的对象复制、区分 operator[] 的读和写动作来做不同的事情、Lazy Fetching(缓式取出)来避免非必要的数据库读取动作、Lazy Expression Evaluation(表达式缓评估)来避免非必要的数值计算动作
18、分期摊还预期的计算成本
当你必须支持某些运算而其结构几乎总是被需要,或其结果常常被多次需要的时候,over-eager evaluation(超急评估)可以改善程序效率,有两种具体的做法:caching(高速缓存)——将“已经计算好而有可能再被需要”的数值保留下来;prefetching(预先取出)——预先支取空间取出需要的东西
19、了解临时对象的来源
在C++中真正的临时对象是看不见的——不出现在源代码中。建立一个没有命名的非堆(non-heap)对象会产生临时对象
这种未命名的对象通常在两种条件下产生:为了使函数成功调用而进行隐式类型转换和函数返回对象时
仅当通过传值(by value)方式传递对象或传递常量引用(reference-to-const)参数时,才会发生这些类型转换。当传递一个非常量引用(reference-to-non-const)参数对象,就不会发生
C++语言禁止为非常量引用(reference-to-non-const)产生临时对象。临时对象是有开销的,所以你应该尽可能地去除它们。在任何时候只要见到常量引用(reference-to-const)参数,就存在建立临时对象而绑定在参数上的可能性。在任何时候只要见到函数返回对象,就会有一个临时对象被建立(并于稍后销毁)
20、协助完成“返回值优化(RVO)”
C++规则允许编译器优化不出现的临时对象(temporary objects out of existence)。
21、利用重载技术(overload)避免隐式类型转换(implicit type conversions)
在C++中有一条规则是每一个重载的operator必须带有一个用户定义类型(user-defined type)的参数
利用重载避免临时对象的方法不只是用在操作符函数上
没有必要实现大量的重载函数,除非你有理由确信程序使用重载函数以后其整体效率会有显著的提高
22、考虑以操作符复合形式(op=)取代其独身形式(op)
复合操作符比对应的独身版本效率高,因为独身版本还需要负担一个临时对象的构造和析构成本
同时提供操作符的复合形式(较易撰写、调试、维护,并在80%时间内供应足可接受的性能)和独身形式(效率较高),允许客户在效率和便利性上做取舍
当面临命名对象或临时对象的抉择时,最好选择临时对象。以便让编译器能进行优化
23、考虑使用其他程序库
由于不同的程序库将效率、扩充性、移植性、类型安全性等的不同设计具体化,有时候可以找到另一个功能相近的程序库而其在效率上有较高的设计权重。如果必要,改用它可大幅改善程序性能
24、了解virtual functions、multiple inheritance、virtual base classes、runtime type identification的成本
使用虚函数,会使用所谓的 virtual tables 和 virtual table pointers ,通常简写为 vtbls 和 vptrs 。
vtbl通常是一个函数指针的数组或链表,每一个声明或继承虚函数的类都有自己的vtbl,其中的每一个元素就是该类的各个虚函数的指针。
虚函数所需的代价:
必须为每个包含虚函数的类的virtual table留出空间;
每个包含虚函数的类的对象里,必须为额外的指针付出代价;
实际上放弃了使用内联函数,虚函数是运行时绑定的,而 inline 是编译时展开的,即使你对虚函数使用 inline ,编译器也通常会忽略。
多继承问题:
在单个对象里有多个vptr(一个基类对应一个),”找出对象内的vptrs”会变得比较复杂,它和虚基类一样,会增加对象体积的大小。
在non-virtual base的情况下,如果派生类对于基类有多条继承路径,那么派生类会有不止一个基类部分, 让基类为virtual可以消除基类的数据成员在每一个子类复制滋生。
然而虚基类也可能导致另一成本: 其实现做法常常利用指针,指向”virtual base class”部分,因此对象内可能出现一个(或多个)这样的指针。例如多重继承的”菱形”结构。
RTTI(运行时类型识别)能让我们在运行时找到对象和类的有关信息,所以肯定有某个地方存储了这些信息让我们查询,这些信息被存储在类型为type_info的对象里。通常,RTTI被设计为在类的vbtl上实现。
25、将constructor和non-member functions 虚化
产生新对象,而且可以根据获得的输入产生不同类型的对象的函数称为virtual constructor,还有一种特别virtual constructor——所谓的——virtual copy constructor会返回一个指针,指向其调用者的新副本,通常命名为copySelf或cloneSelf或clone,我们可以利用virtual copy constructor实现一个copy constructor(对指针指向的对象进行拷贝)
derived class重新定义其base class的一个虚函数时不要求返回类型一定相同,如果函数原本返回类型是一个指向base class的handle时,derived class可以返回一个指向derived class的handle
non-member functions 虚化就是写一个虚函数做实际工作,然后再写一个什么都不做的非虚函数来负责调用虚函数
26、限制某个class所能产生的对象数量
函数封装法:使用一个函数来统一管理类的构造,使用static防止重复构造, 只适用于一个数量,函数可以放入全局命名空间、放入类中、放入namespace中。
类封装法: 使用一个类来统一管理某一个类的构造和销毁,类似接口,适用于任意数量限制
建立一个基类,构造函数和复制构造函数中计数加1,若超过最大值则抛出异常;析构函数中计数减1。
函数实现和类实现相比具有两个优势:“class拥有一个static对象”的意思是,即使从未被用过也会被构造(及析构),而函数则在第一次调用时才会产生,为此你不需要为你并不使用的东西付出代价;第二个优势是我们确切知道function static的初始化时机,而class static则不一定什么时候初始化
由于inline只是一种对编译器的建议请求,所以如果函数内含有static则inline被阻止
带有private构造函数的类不能作为基类使用, 也不能嵌入到其它对象中
设计伪构造函数(调用私有的构造函数,返回指针),但是依旧不能实现派生,不过可以通过这样完成包含
27、要求(或禁止)对象产生于heap中
只能在堆上
方法:将析构函数设置为私有
原因:C++ 是静态绑定语言,编译器管理栈上对象的生命周期,编译器在为类对象分配栈空间时,会先检查类的析构函数的访问性。若析构函数不可访问,则不能在栈上创建对象。
方法:将所有构造函数设置为私有
缺点:类常常有多个构造函数,类的作者必须记住将他们每一个声明为私有,如果这些函数由编译器产生,则总是公有
但这也会妨碍继承和内含,妨碍继承的解决方案是将析构函数设置为保护,必须内含类对象的类可以修改为内含一个指向所要指对象的指针
只能在栈上
方法:将 new 和 delete 重载为私有
原因:在堆上生成对象,使用 new 关键词操作,其过程分为两阶段:第一阶段,使用 new 在堆上寻找可用内存,分配给对象;第二阶段,调用构造函数生成对象。将 new 操作设置为私有,那么第一阶段就无法完成,就不能够在堆上生成对象。
判断某个对象是否位于heap内
没有合适办法能判断一个对象是否在堆中,我们为什么要判断对象是否在堆上?真实的需求是,判断执行delete是否安全。
判断一个对象是否可以安全用delete删除,只需在operator new中将其指针加入一个列表,然后根据此列表判断指针是否在其中,如果在,执行delete就是安全的,否则不安全。
28、Smart Pointers(智能指针)
智能指针和普通指针的区别
智能指针实际上是对普通指针加了一层封装机制,使得用户可以将两者等同。这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期
智能指针就是模拟指针动作的类。智能指针从模板生成,因为要与内建指针类似,必须是强类型的;模板参数确定指向对象的类型
智能指针operator&返回的是指针所指对象的引用
智能指针operator->返回的是指针所指对象的指针
29、Reference counting(引用计数)
作用:简化heap objects周边的簿记工作,让所有等值对象共享一份实值
引用计数的成本:每一个拥有引用计数能力的实值都携带一个引用计数器,大部分操作都需要查验或处理这个计数器,对象的实值因而需要更多内存,我们需要执行更多代码。
引用计数的优点:引用计数是个优化技术,其适用前提是对象常常共享实值,在这种情况下它可节省你的空间和时间。以下是引用计数改善效率的最适当时机:
相对多数的对象共享相对少量的实值的时候。”对象/实值”数量比越高,引用计数带来的利益越大。
对象实值产生或销毁成本很高,或是他们使用很多内存的时候。
30、Proxy classes(替身类、代理类)
Proxy类可以完成一些其它方法很难甚至可不能实现的行为。多维数组是一个例子,左值/右值的区分是第二个,限制隐式类型转换是第三个
proxy类缺点:作为函数返回值,proxy对象是临时对象,它们必须被构造和析构。Proxy对象的存在增加了软件的复杂度。从一个处理实际对象的类改换到处理proxy对象的类经常改变了类的语义,因为proxy对象通常表现出的行为与实际对象有些微妙的区别
31、让函数根据一个以上的对象类型来决定如何虚化
根据分派可以基于多少种宗量,可以将面向对象的语言划分为单分派语言和多分派语言。单元分派语言根据一个宗量的类型(真实类型)进行对方法的选择,多分派语言根据多于一个的宗量的类型对方法进行选择。
C++和Java以及Smaltalk都是单分派语言;多分派语言的例子包括CLOS和Cecil。按照这样的区分,C++和Java就是动态的单分派语言,因为这两种语言的动态分派仅仅会考虑到方法的接收者的类型,同时又是静态的多分派语言,因为这两种语言对重载方法的分派会考虑到方法的接收者的类型和方法所有参量的类型。
解决方法:虚函数+RTTI,只使用虚函数,自行仿真虚函数表格,使用“非成员(non-Member)函数”的碰撞处理函数
32、在未来时态下发展程序
在未来时态下设计程序,就是接受“事情总会改变”的事实,并准备应因之道。
以C++“本身”来表现各种规范
提供完整的类,即使某些部分目前用不到。如果有了新的需求,你不太需要回过头去改它们。
将你的接口设计得有利于共同操作,阻止共同错误。使得类容易正确使用而不易用错。例如,阻止拷贝构造和赋值操作不合理的类请禁止动作的发生。防止部分赋值的发生。
尽量使代码一般化,除非有不良的巨大后果。例如,如果在写树的遍历算法,考虑将它通用得可以处理任何有向不循环图。
33、将非尾端类(non-leaf classes)设计为抽象类(abstract classes)
如果你有两个具体类C1和C2,而你希望C2以public方式继承C1,你应该将原本的双类继承体系改为三类继承体系:产生一个新的抽象类A,并令C1和C2都以public方式继承A
当你使用第三方厂商的各种C++类库的时候,如果你发现自己需要产生一个具体类,继承自程序库的一个具体类,而你只能使用该程序库,不能修改,怎么办?
将你的具体类派生自既有的(程序库的)具体类,但需要注意本条款一开始所验证的assignment相关问题,还有多态数组的问题。
试着在程序库集成体系中找到更高的抽象类,其中有你需要的大部分功能,继承它。
以“你所希望继承的那个程序库类”来实现你的新类,相当于包含程序库的对象,has-a的关系。
做个乖宝宝,手上有什么就用什么。
34、如何在同一个程序中结合C++和C
名称重整:C++编译器给程序的每个函数换一个独一无二的名字。在C中,这个过程是不需要的,因为没有函数重载,但几乎所有C++程序都有函数重名。要禁止名变换,使用C++的extern “C”。不要将extern “C”看作是声明这个函数是用C语言写的,应该看作是声明这个函数应该被当作好像C写的一样而进行调用。
Statics的初始化:在main执行前和执行后都有大量代码被执行。尤其是,静态的类对象和定义在全局的、命名空间中的或文件体中的类对象的构造函数通常在main被执行前就被调用。这个过程称为静态初始化。同样,通过静态初始化产生的对象也要在静态析构过程中调用其析构函数,这个过程通常发生在main结束运行之后。
动态内存分配:C++部分使用new和delete,C部分使用malloc(或其变形)和free。尽量避免调用标准程序库以外的函数或是大部分计算平台尚未稳定的函数。
数据结构的兼容性:在C++和C之间这样相互传递数据结构是安全的----在C++和C下提供同样的定义来进行编译。在C++版本中增加非虚成员函数或许不影响兼容性,但几乎其它的改变都将影响兼容。
如果想在同一程序下混合C++与C编程,记住下面的指导原则:(1).确保C++和C编译器产生兼容的obj文件;(2).将在两种语言下都使用的函数声明为extern “C”;(3).只要可能,用C++写main();(4).总用delete释放new分配的内存;总用free释放malloc分配的内存;(5).将在两种语言间传递的东西限制在用C编译的数据结构的范围内;这些结构的C++版本可以包含非虚成员函数。