1、视C++为一个语言联邦
对于内置类型而言,pass-by-value通常比pass-by-reference高效。()内置类型在按值传参时,只是将变量的值传递到栈上。 然后被调用函数将值取出后,使用即可。在按引用传参时,需要将被引用的变量的地址压栈, 然后被调用函数首先取出地址,然后再次根据地址寻址获取值。)
C++可分为四个部分:(1)C;(2)Objected-Orientated C++,面向对象的设计;(3)Template C++,泛型编程的部分;(4)STL。
注意:
(1)C++高效编程视情况而变化,取决于你使用C++的哪一部分。
2、尽量以const、enum、inline替换#define
注意:
(1)对于单纯常量,最好以const对象或者enum替换#define(便于调试,类型检查;有时能减少代码量)
(2)对于形似函数的宏(macros),最好改用inline函数替换#define(宏定义易错、难看,内联函数拥有宏的功能,却是一个真正的函数)
3、尽可能的使用const
注意:
(1)将某些东西设置为const能帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体;
(2)编译器强制实行“bitwise contness”,但你编写程序时应该使用“概念上的常量性”(conceptual constness);
(3)当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
4、确定对象使用前已经先被初始化
不同环境下,可能内置类型对象也没有自动初始化。
C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。
基类总是先于派生类被初始化,而类中成员变量总是以其声明的次序被初始化。
注意:
(1)为内置类型对象进行手工初始化,因为C++不保证初始化他们;
(2)构造函数最好使用成员初始化列(member initialization list),而不要在构造函数本体中使用赋值操作(assign)。初始化列列出的成员变量,其排列次序应该和它们在class中声明的次序相同;
(3)为免除“跨编译单元之初始化次序”问题,请以local static对象代替non-local static对象 。(将static对象包裹在一个函数中,这样可以避免在其它单元使用static对象时,该对象还没有初始化,调用函数能确保在第一次调用时初始化)。
5、了解C++默认编写并调用了哪些函数
对于一个空类,编译器会默认声明一个default构造函数、copy构造函数、copy assignment操作符和一个析构函数(最少是这4个),所有这些函数都是public且inline的。
class Empty{}
实质是:
class Empty{
public:
Empty(){...};
Empty(const Empty& rhs){...}
Empty& operator=(const Empty& rhs){...}
~Empty(){...}
}
只有这些函数需要时,它们才会被构造出来。
编译器产生的析构函数是non-virtual的,除非这个class的base class自身声明有virtual析构函数
只有生出的代码合法并且有适当表现机会时,默认生成的copy assignment构造函数才有意义。 (如base类的copy assignment构造函数 就是private,编译器将拒绝为其派生类生成一个copy assignment操作符,此外涉及const成员更改、指针是否更改指向等等操作也会死这样的效果)
注意:
(1)编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符以及析构函数
6、若不想使用编译器自动生成的函数,就应该明确拒绝
两种方法:(1)将copy构造函数、copy assignment运算符函数声明为private(缺点:成员函数和友元函数还是能够使用)
(2)定义一个基类Uncopyable ,它的相关函数声明为private,其派生类相关的函数是无法自动生成的(因为编译器生成版总是会尝试调用其基类的对应兄弟。继承方式随意,基类析构函数不一定需要virtual,因为基类不含数据。)
注意:
(1)为驳回编译器自动(暗自)提供的机能,可将相应的成员函数声明为private并且不予实现。使用像Uncopyable这样的base class也是一种做法。
7、为多态基类声明virtrual析构函数
C++明确指出,当派生类对象经由一个基类指针被删除(意思是多态,用基类指针指向派生类对象),而该基类带着一个non-virtual析构函数,其结果未定义——实际执行时通常发生的是对象的派生类成分没有被销毁,也就是派生类析构函数没有被调用,造成“局部销毁”。
解决方法:给基类一个virtual析构函数。
任何class只要带virtual函数都几乎确定应该也有一个virtual析构函数;如果class不含virtual函数,通常表示它并不意图被用作一个基类,此时令其析构函数为virtual往往是个馊主意。(虚函数的对象有虚函数表指针,占用内存空间;结构破坏,不具有移植性)
只有class内至少有一个virtual函数,才将它声明为virtual析构函数。
一个好的做法是为base classes提供一个pure virtual析构函数的声明(抽象类不能实例化,如果想实例化它,可行的做法是提供pure virtual析构函数一个定义)。
注意:
(1)polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应当拥有一个virtual析构函数;
(2)classes的设计目的如果不是作为base classes使用,或不是为了具备多态性(polymorphically),就不该声明virtual析构函数。
8、别让异常逃离析构函数
通常在析构函数中释放分配的资源,如果销毁多个资源时,发生多个异常,程序执行结果会导致不明确的行为。
解决方法:(1)try...catch捕捉异常,调用std::abort()函数终止程序;(2)记录异常,继续执行。
如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数。因为析构函数吐出异常是危险的,总会带来“过早结束程序”或“发生不明确行为”的危险。
注意:
(1)析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序;
(2)如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。
9、决不在构造函数和析构函数中调用virtual函数
在base class构造期间,virtual函数不是virtual函数!即base class构造期间virtual函数绝不会下降到derived classes阶层。(原因:因为base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时,derived class成员变量尚未初始化。)
在derived class对象的base classes构造期间,对象的类型是base class而不是derived class;同样对于析构函数,进入base class析构函数后对象就成为一个base class对象。
注意:
(1)在构造和析构期间不要调用virtual函数,因为该类调用从不下降至derived class(比起当前执行构造函数和析构函数的那一层)。
10、令operator=返回一个reference to *this
赋值操作符返回左侧对象的引用是为了实现“连锁赋值”!
这个只是协议,方便实现连锁赋值,并不是强制要求。
注意:
(1)令赋值(assignment)操作符返回一个reference to *this。
11、在operator=中处理“自我赋值”
自我赋值发生在对象被赋值给自己时。
注意:
(1)确保当对象自我赋值时operator=有良好的行为。其中技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap;
(2)确定任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,其行为仍然正确。
12、复制对象时勿忘其每一个成分
如果你为class添加一个成员变量,你必须同时修改copying函数以及所有的构造函数(以符合第4条,成员变量使用前初始化)。
一旦你承担起“为derived class撰写copying函数”的重责大任,必须很小心地也复制其base class成分。那些成分往往是private,所以你无权访问它们,你应该让derived class的copying函数调用相应的base class函数。
所以,本条的意思是,当自己编写一个copying函数,请确保:(1)复制所有的local成员变量;(2)调用所有base classes内适当的copying函数。
不应该用copy assignment操作符调用copy构造函数。(这像是在试图构造一个已经存在的对象,某些情况下可以实现,但是某些情况下产生一些严重后果)
反过来,令copying构造函数调用copy assignment操作符,也同样无意义。(构造函数是用来初始化新对象,而copy assignment操作符是只施行于已初始化的对象身上)。
如果要重用相同的代码,最好的方式是新建一个新的成员函数给两者调用。(往往是private的init函数)
注意:
(1)copying函数应该确保复制“对象内的所有成员变量”及“所有base class成分”;
(2)不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数中,并由两个copying函数共同调用。
13、以对象管理资源
注意不要让多个auto_ptr同时指向同一元素。
对于auto_ptr :若通过copy构造函数或copy assignment操作符复制它们,它们会变成null,而复制所得的指针将取得资源的唯一拥有权!
对于tr1::shared_ptr:是引用计数型智慧指针,它们的拷贝行为是正常的拷贝。
注意:
(1)为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源;
(2)两个较长使用的RAII classes分别是tr1::shared_ptr和auto_ptr,前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null.
14、以对象管理资源在资源管理中小心copying行为
注意:
(1)复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为;
(2)普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。不过其他行为也都可能被实现。
15、在资源管理类中提供对原始资源的访问
智能指针(tr1::shared_ptr和auto_ptr)都提供get()成员函数以供获取其原始底部指针(的复件),也重载了指针取值(pointer dereferencing)操作符(operator->和operator*),他们允许隐式转换至底部原始指针。
注意:
(1)APIs往往要求访问原始资源(raw resource),所以每一个RAII class应该提供一个“取得其管理之资源”的办法;
(2)对原始资源的访问可能经由显示转换和隐式转换。一般而言显示转换比较安全,当隐式转换对客户比较方便。
16、成对使用new和delete时要采用相同的形式
使用new时,有两件事发生:(1)内存被分配出来(通过operator new函数);(2)针对此内存会有一个(或多个)构造函数调用。
使用delete时,也会有两件事发生:(1)针对此内存会存在一个(或多个)析构函数被调用;(2)内存被释放。
尽量不要对数组形式做typedefs动作,因为这样有可能导致delete模糊。
注意:
(1)如果你在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果你在new表达式中不使用[],那么一定不要在相应的delete表达式中使用[]。
17、以独立的语句将newed对象置入智能指针
同一条语句内的参数(临时函数调用)执行顺序在C++中是没有规定的,所以容易造成意想不到的后果,因此最好采用分离语句,这样次序就是固定的。
注意:
(1)以独立语句将newed对象存储于(置于)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
18、让接口容易被正确使用,不易被误用
tr1::shared_ptr可以使用“专属删除器”。
注意:
(1)好的接口很容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质;
(2)“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容;
(3)“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任;
(4)tr1::shared_ptr支持定制型删除器(custom deleter)。这可防范DLL问题,可被用来自动解除互斥锁等等。
19、设计class犹如设计type
设计新的class,就是设计新的type,需要考虑一些问题
20、宁以pass-by-reference-to-const替换pass-by-value
pass-by-reference-to-const高效的原因:没有任何构造函数和析构函数被调用,因为没有任何新的对象被创建。(const是重要的,防止其被不经意修改)。
reference往往是以指针实现出来,因此pass-by-reference通常意味着真正传递的是指针。
对象小并不意味着其copy构造函数不昂贵。
注意:
(1)尽量以pass-by-referencee-to-const替换pass-by-value,前者通常比较高效,并可避免切割问题。
(2)以上规则并不适合内置类型,以及STL的迭代器和函数类型。对它们而言,pass-by-value往往比较适当。
21、必须返回对象时,别妄想返回其reference
注意:
(1)绝不要返回pointer或reference指向一个lock stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而可能需要多个这样的对象。
22、将成员变量声明为private
使用函数可以让你对成员变量的处理有更精确的控制;更重要的是封装(方便以后修改)。
protected成员变量的封装性是不是高过public成员变量?(否)
protected成员变量就像public成员变量一样缺乏封装性。
注意:
(1)切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性;
(2)protected并不比public更具封装性。(因为都没封装性)
23、宁以non-member、non-friend替换member函数
对于成员变量,越多函数可以访问它,那数据的封装性就越低,意味着我们改动时,影响到的代码就越多。因此推荐使用non-member non-friend函数。
此外,有利于扩展、分成多个头文件
注意:
(1)宁可拿non-member non-friend函数替换member函数,这样就可以增加封装性、包裹弹性和机能扩充性。
24、若所有的参数皆需要类型转换,请为此采用non-member函数
只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者,而this对象的隐喻参数,绝不是隐式转换的合格参与者。
无论何时,能够避免friend函数就避免。
注意:
(1)如果你需要为某个函数的所有参数(包括this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。
25、考虑写一个不抛出异常的swap函数
注意:
(1)当std::swap对你的类型效率不高时,提供一个swap成员函数,并确保这个函数不抛出异常;
(2)如果你提供一个member swap,也该提供一个non-member swap用来调用前者,对于classes(而非templates),也请特化std::swap
(3)调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命令空间资格修饰”;
(4)为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
26、尽可能延后变量定义式的出现时间
不止应该延后变量的定义,直到非得使用该变量的前一刻为止,甚至应该尝试延后这份定义直到能够给它初值实参为止。这样,不仅能够避免构造(和析构)非必要对象,还可以避免无意义的default构造行为。
注意:
(1)尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序的效率。
27、尽量少做转型动作
转型(casts)破坏了类型系统,可能导致任何种类的麻烦,甚至有些非常晦涩。
旧式转型:(1)(T)expression; (2)T(expression)
新式转型:(1)const_cast:通常被用来将对象的常量性移除(C++中唯一有此能力的转型);
(2)static_cast:用来强迫隐式转换,可以进行多种转换,但无法将const转为non-const(只有const_cast才能办到);
(3)dynamic_cast:主要用来执行“安全向下转型”,也就是用来决定某对象是否归属继承体系中的某个类型。(唯一无法被旧式转型执行的动作,可能耗费重大运行成本);
(4)reinterpret_cast:意图执行低级转换,实际动作及结果可能取决于编译器。
注意:
(1)如果可以,尽量避免转型,特别是在注重代码效率的代码中避免dynamic_casts。如果有个设计需要转型动作,试着发展无需转型的替代设计。
(2)如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调出该函数,而不需将转型放进他们自己的代码内;
(3)宁可使用C++-style(新式)转型,不要使用旧式转型。前者很容易辨识出来,而且也比较有着分门别类的职掌。
28、避免返回handles指向对象内部成分
注意:
(1)避免返回handles(包括引用、指针、迭代器)指向对象内部。遵守这个条款可以增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降到最低。
29、为“异常安全”而努力是值得的
注意:
(1)异常安全函数,即使发生异常也不会泄漏资源或允许任何数据结构败坏。这样的函数区分为三种可能的保证:基本型、强烈型、不抛异常型。
(2)“强烈保证”往往能以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义;
(3)函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
30、透彻了解inlining的里里外外
注意:
(1)将大多数inlining限制在小型、被频繁调用的函数身上。这可使日后的调试过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化;
(2)不要只因为function templates出现在头文件中,就将他们声明为inline。
31、将文件间的编译依存关系降至最低
如果使用object references或object pointer可以完成任务,就不要使用objects;如果可以,尽量以class声明式代替class定义式为声明式和定义式提供不同的头文件。
注意:
避免大量依赖性编译的解决方案就是:在头文件中用class声明外来类,用指针或引用代替变量的声明;在cpp文件中包含外来类的头文件。(只要在头文件中尽量减少include别的头文件,就可以做到尽量的减少依赖关系。)
(1)支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和Interface classes;
(2)程序库头文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。
32、确定你的public继承塑模出ls-a关系
virtual函数意味着“接口必须被继承”;non-virtual函数意味着“接口和实现都必须被继承”。
public inheritance(公开继承)意味着“is-a”(是一种)的关系。
派生类对象都是基类对象,反之不成立。意味着凡是需要基类对象的地方,可以使用派生类对象(注意这只对public继承才成立),而需要使用派生类对象的地方却不能使用基类对象。基类更具一般化,而派生类更具特殊化。
注意:
(1)“public继承”意味着is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
33、避免遮掩继承而来的名称
在Derived class中使用using Base::mf1会遮蔽Derived class中关于mf1所有的东西,使用Base class里的东西(并且public)。
转交函数会暗自成为inline。
注意:
(1)derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此;
(2)为了让被遮掩的名称再重见天日,可以使用using声明式或转交函数。
34、区分接口继承和实现继承
声明一个pure virtual函数的目的是为了让derived classes只继承函数接口;生成简朴的impure virtual函数的目的,是为了让derived classes继承该函数的接口和缺省实现;声明non-virtual函数的目的是为了令derived classes继承函数的接口及一份强制性实现。
注意:
(1)接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口;
(2)pure virtual函数只具体指定接口继承;
(3)简朴的(非纯)impure virtual函数具体指定接口继承和缺省实现继承;
(4)non-virtual函数具体指定接口继承及强制性实现继承。
35、考虑virtual函数以外的选择 --没看懂
注意:
(1)virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
(2)将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
(3)tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
36、绝不重新定义继承而来的non-virtual函数
适用于基类对象的每一件事,也适用于派生类对象,因为每个派生类对象都是一个基类对象 ;如果“每个派生类对象都是一个基类对象”不为真,那么就不应该使用public形式继承。
注意:
(1)绝对不要重新定义继承而来的non-virtual函数。
37、绝不重新定义继承而来的缺省参数值
virtual函数系动态绑定,而缺省参数值却是静态绑定。(原因是运行期效率,动态绑定时在编译期更慢且更复杂)
所谓静态类型,就是它在程序中被声明时所采用的类型;所谓动态类型,是指目前所指对象的类型。
为了不更改缺省参数值造成——代码重复,而且代码还有相依性——解决方案是:更改virtual函数设计,采用公共的non-virtual函数调用private的virtual函数。
注意:
(1)绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-你唯一应该覆写的东西-却是动态绑定。
38、通过复合塑模出has-a或“根据某物实现出”
复合包括:分层、内含、集合和内嵌。
注意:
(1)复合的意义和public继承完全不同;
(2)在应用域,复合意味has-a(有一个)。在实现域,复合意味着is-implemented-in-terms-of(根据某物实现出)。 --更像是“配接器”
39、明智而审慎的使用private继承
如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象;由private base class继承而来的所有成员,在derived class中都会变成private属性。
private继承意味着只有实现部分被继承,而接口部分被略去。
private继承意味着is-implemented-in-term-of(根据某物实现出)-----和复合一样:尽可能使用复合,必要时才使用private继承。
private继承时,派生类是可以使用基类中的public/protected成员,而派生类对象却不行(没有接口)!!!!
private继承有一个好处:空白基类优化(单继承时存在)
当你面对“并不存在is-a关系”的两个classes,其中一个需要访问另一个的protected成员,或需要重新定义一 个或多个virtual函数,private继承极有可能成为正统的设计策略。
注意:
(1)private继承意味is-implemented-in-term-of(根据某物实现出)。它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
(2)和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
40、明智而审慎地使用多重继承
使用virtual继承的那些classes说产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base classes的成员变量时,也比访问non-virtual base classes的成员变量速度慢——得为virtual继承付出代价。此外支配“virtual base classes初始化”的规则也比non-virtual bases的情况远为复杂且不直观。
使用忠告:(1)非必要不使用virtual bases;(2)如果你必须使用virtual base classes,尽可能避免在其中放置数据。
注意:
(1)多重继承比单一继承复杂。它可能导致新的歧义性,以及对virtual继承的需要;
(2)virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况;
(3)多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private 继承某个协助实现的class”的两相组合。
41、了解隐式接口和编译期多态
隐式接口仅仅是由一组有效表达式构成,表达式自身可能看起来很复杂,但它们要求的约束条件一般而言相当直接而明确。
注意:
(1)classes和templates都支持接口(interfaces)和多态(polymorphism);
(2)对classes而言接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期;
(3)对templates参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过templates具现化和函数重载解析发生于编译器。
42、了解typename的双重意义
从C++的角度看,声明templates参数时,不论使用关键字class或typename,意义完全相同;然而class和typename并不总是等价的,有时一定要使用typename。
任何时候当你想要在templates中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字typename;但是typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可以在member initialization list(成员初值列)中作为base class修饰词。
typename相关规则在不同的编译器上有不同的实践。
注意:
(1)声明templates参数时,前缀关键字class和typename可互换。
(2)请使用关键字typename标识嵌套从属类型名称,但不得在base class lists(基类列)或member initialization list(成员初值列)内以它作为base class修饰符。
43、学习处理模板化基类内的名称
base class是模板时,未特化前,派生类无法正常调用基类的成员(即编译器不会进入基类中搜索),这是因为基类在等待参数,还未特化(此时还是类模板),不是具体的类(模板类)。
要想解决这个问题,有三个解决方案:(1)在base class函数调用动作之前加上this->(这种方式是假设基类模板的所有特化版本都会包含该调用成员函数,如果最后特化的版本不包含,则编译报错);(2)使用using声明式using base<T>::func(原理和第一条一样); (3)明白指出调用的函数是位于base class内,base<T>::func()(原理和第一条一样)。
注意:
(1)可在derived class templates内通过“this->”指涉base class templates内的成员名称,或藉由一个明白写出的“base class资格修饰符”完成。
44、将与参数无关的代码抽离template
class templates的成员函数只有被使用时才会被暗中具现化。
使用templates可能会导致代码膨胀。
注意:
(1)Templates生成多个classes和多个函数,所以任何templates代码都不该与某个造成膨胀的templates参数产生相依关系
(2)因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换templates参数;
(3)因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
45、运用成员函数模板接收所有兼容类型
在class内声明泛化copy构造函数(模板成员函数)并不会阻止编译器生成它们自己的copy构造函数(非模板成员函数),赋值(assignment)也一样。
注意:
(1)请使用成员函数模板生成“可接受所有兼容类型”的函数;
(2)如果你声明成员模板用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
46、需要类型转换时请为模板定义非成员函数
因为在templates实参推导过程中从不将隐式类型转换函数纳入考虑。
注意:
(1)当我们编写一个class templates,而它所提供之“与此templates相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class templates内部的friend函数”。
47、请使用traits classes表现类型信息
编译器条件语句:重载
注意:
(1)Traits classes使得“类型相关信息”在编译期可用。它们以templates和“templates特化”完成实现;
(2)整合重载技术后,traits classes有可能在编译期对类型执行if...else测试。
48、认识template元编程
模板元编程(TMP):编写templates-based C++程序并执行编译期的过程。即以C++写成、执行于C++编译器内的程序。
TMP循环效果是藉由递归完成的。TMP循环并不涉及递归函数的调用,而是涉及“递归模板具现化”。
TMP选择判断效果:可由函数重载实现。
注意:
(1)模板元编程可将工作由运行期移往编译期,因而得以实现早期侦测和更高的执行效率;
(2)TMP可被用来生成“基于政策选择组合”的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。
49、了解new-handler的行为
当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
一个良好的new-handler函数应该做以下事情:(1)让更多内存可被使用;(2)安装另一个new-handler;(3)卸厨new-handler,抛出异常;(4)抛出bad_alloc(或派生bad-alloc)异常,这个异常不会被operator new捕捉,因而传播到内存申请处;(5)不返回,通常调用abort或exit。
static成员必须在类外定义,除非它们是const 整数型。
C++不支持class专属new-handler,但是可以通过代码自己实现。
注意:
(1)set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用;
(2)Nothrow new是一个颇为局限的工具,因为它只适用于内存分配;后继的构造函数调用还是可能抛出异常。
50、了解new和delete的合理替换时机
替换编译器提供的operator new或者operator delete的目的:(1)用来检测运行上的错误;(2)为了强化效能,使用特制版本;(3)为了搜集使用上的统计数据。
C++规则:所有operator new都应该内含一个循环,反复调用new-handler函数;operator new返回的指针都要有适当的对齐(取决于数据类型,malloc实现)。
注意:
(1)有许多理由需要写一个自定的new和delete,包括改善性能、对heap运用错误进行调试、搜集heap使用信息等。
51、编写new和delete时需固守常规
operator new实际上不只一次尝试分配内存,并在每次失败后调用new-handler函数。只有当new-handler函数指针是null时,operator new才会抛出异常。
如果即将被删除的对象派生自某个base class而后者缺乏virtual析构函数,那么C++传给operator delete的size_t数值可能不正确。也就是如果你的base classes遗漏virtual析构函数,operator delete可能无法正确运行。
注意:
(1)operator new应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存需求,就该调用new-handler。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”;
(2)operator delete应该在收到null指针时不做任何事,class专属版本则还应该处理“比正确大小更大的(错误)申请”。
52、写了placement new也要写placement delete
new操作共有两个函数被调用:(1)用以分配内存的operator new;(2)构造函数。
placement new:带额外参数的operator new,也就是,如果operator new接受参数除了一定会有的那个size_t之外还有其他,这便是所谓的placement new。
如果内存分配成功,而构造函数抛出异常,运行期系统有责任取消operator new的分配并回复旧观,具体做法是,运行期系统寻找“参数个数和类型都与operator new相同”的某个operator delete,如果找到,就调用,如果找不到,就什么也不做。
规则:如果一个带额外参数的operator new没有“带相同额外参数”的对应版operator delete,那么当new的内存分配动作需要取消并恢复旧观时就没有operator delete会被调用。
此外,正常使用delete操作时调用的是正常形式的operator delete,而非其placement版本。这意味着,我们必须同时提供一个正常的operator delete(用于构造期间无任何异常被抛出)和一个placement delete版本(用以构造期间有异常被抛出)。
此外在继承体系中注意派生类中定义任何类型的placement new都会遮掩基类中的定义,所以如果不想遮掩,又需要有自己的placement new定义,请用using等方式取得基类相应的函数。
注意:
(1)当你编写一个placement operator new,请确定也写出了对应的placement operator delete、如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏;
(2)当你声明了placement new和placement delete,请确定不要无意识(非故意)地遮掩他们的正常版本。
53、请不要忽视编译器的警告
注意:
(1)严肃对待编译器发出的警告信息。努力在你的编译器的最高(最严)警告级别下争取“无任何警告”的荣誉;
(2)不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度并不相同。一旦移植到另一个编译器上,你原本依赖的警告信息有可能消失。
54、让自己熟悉包括TR1在内的标准程序库
注意:
(1)C++标准程序库的主要机能由STL、iostreams、locales组成,并包含C99标准程序库;
(2)TR1添加了智能指针(如tr1::shared_ptr)、一般化函数指针(tr1::function)、hash-based容器、正则表达式以及另外10个组件的支持;
(3)TR1自身只是一份规范,为获得TR1提供的好处,你需要一份实物。一个好的实物来源是Boost。
55、让自己熟悉Boost
注意:
(1)Boost是一个社群,也是一个网站。致力于免费、源码开放、同僚复审的C++程序库开发。Boost在C++标准化过程中扮演深具影响力的角色;
(2)Boost提供许多TR1组件实现品,以及许多其它程序库。