1 C++语言联邦
- C 内置类型传值更快
- C with class 构造析构 封装 继承 多态 动态绑定 传递常引用更快
- Template 模板元编程
- STL 容器 迭代器 算法 函数对象 基于C指针 传值更快
2 const enum inline constexpr 代替 #define
- 出错时,编译器可以在符号表中找到变量名
- 简单的宏替换可能导致数据段中存在多分常量拷贝
- const string a("str") 替换 const char *const a = "str"
- (新式编译器)类中静态变量在类内声明时初始化,可以不在定义式中初始化
- #define 无命名空间概念,无法定义类内部常量
- enum定义的常量无法取地址,更像#define。可用于定义数组长度而const不可以
- 优秀的编译器不会为const变量分配空间,除非取其地址或引用
- inline取代宏函数,且可指定作用域;const enum取代#define常量,其中enum不可取地址
- #define用于头文件保护仍很有用
3 尽可能用const
- 与指针、引用的结合
- const修饰的迭代器 const_iterator 迭代器本身为const const xxx<yyy>::iterator 指向的值为const
- const与函数(返回值 参数 函数本身)
- const成员函数 为了确认该成员函数可作用于const对象(c++建议传递常引用,故只有const成员函数可以执行)
- 成员函数是否为const可相互重载,对于const对象自动调用const版本的成员函数
- 函数返回值为内置类型时,不可被修改,纵使可修改,修改的是返回对象的副本而不是其本身;为内置类型的引用时可被修改;
- const成员函数,编译器以bitwise的观点编译,此类函数不可更改对象内的任何非静态成员,但却可以通过其返回的引用对对象进行修改,故真正的const函数返回值应该同样用const修饰
- 程序员应以逻辑的观点看待const成员函数,利用mutable使const函数可以修改被其修饰的成员变量(这些变化不会对调用对象的客户产生影响)
- 对于相同逻辑而需同时实现const与非const版本的函数,可使用非const版本调用const版本,并将const属性转除。具体做法是将*this强转为const类型然后调用const版本函数,再对返回值const_cast
4 确定对象被使用前已先被初始化
- 通常C part of C++初始化语句会招致运行期成本,不保证初始化,non C part of C++一般会初始化。 eg. array与 vector
- 内置类型最好均进行手动初始化。自定义类型初始化由构造函数负责:应对每个成员变量初始化
- 构造函数内赋值与初始化列表(在进入构造函数之前进行,效率更高,顺序与变量声明顺序一致,初始化必须有次序性时尤为注意)
- 在对象构造时,首先会调用默认构造函数对内部成员进行初始化,若在构造函数内重新赋值赋值,则默认构造的执行被覆盖而没有任何意义,但是采用初始化列表时,各成员对象均直接利用设定的初始值进行构造
- 对于对象内属于基本内置类型的成员变量,赋值与初始化列表开销相同,但仍建议使用初始化列表,尤其对于const 及 引用对象,无法通过赋值进行初始化。
- 当成员变量过多,且对象本身有多个构造函数时,可以初始化开销较小的成员初始化过程写入成private函数,供多个构造函数调用。对于初始化时需要读取文件或数据库时尤其适用
- C++对于定义于不同编译单元的静态变量初始化没有明确规定次序,因而一个对象依赖于另一个静态对象时,后则可能还未按时初始化->催生了单例模式
- 任何一种non - const - static对象在多线程下均有麻烦。应在单线程节点通过refrence-returning函数全部初始化
5 了解C++默默编写并调用了哪些函数
- C++会在必要(被调用)时为class生成默认构造函数、析构函数、拷贝构造函数、赋值函数;均为public & inline。其中 析构函数non-virtual ,除非基类的构造函数为虚
- 编译器生成的拷贝构造与赋值函数行为一致,只是将成员变量简单赋值
- 某些情况下编译器拒绝生成拷贝构造与赋值函数(eg.1有成员为引用类型或const(两者均初始化后不可被赋值),2基类中拷贝构造或赋值函数为private,因为生成的函数必要需要调用基类的拷贝函数)
6 若不想使用编译器自动生成的函数,就该明确拒绝
- 拒绝方式一:显示声明拷贝构造/赋值函数为private 且 不实现函数体 可在链接时发现错误
- 拒绝方式二:声明空的基类(析构函数可不虚),只声明拷贝构造/赋值函数为private 且 不实现函数体,然后令需要限制拷贝构造/赋值函数的类对其private继承 可在编译期发现错误
7 为多态基类声明virtual析构函数
- 任何class只要带有virtual函数就该有一个virtual析构函数,因为该class必将作为base class
- 若class内不含virtual函数,意味着其不想被继承,则不应该有virtual析构(会增加内存开销,影响与C-struct的一致性)
- 对于析构函数非虚的class,不该将其作为base class而继承它(string类 及 STL容器类,c++并未提供final in java及sealed in C# 来限制这些类被继承)
- C++防止类被实例化,可以将析构函数定义为纯虚,但最好再将此析构实现,以免其派生类析构时发生链接错误
- 对于为了实现多态性质的基类,一定需要virtual析构;也有不为多态的基类(eg. 6中方法2),它们不需要虚析构
8 别让异常逃离析构函数
- C++不喜欢析构函数吐出异常(不处理异常),这会导致异常传播而造成不确定行为
- 对于可能产生异常的操作应封装出来供客户捕捉,同时应在析构中进行处理(捕捉错误甚至结束程序),以防客户未显式处理
9 绝不在构造和析构函数中调用virtual函数
- 某base class的析构、构造中调用virtual函数,则在其派生类构造时,首先进行base class的构造,此时的该对象的类型应算做base class,则调用的virtual版本为基类版本(否则将可能使用尚未初始化的派生类中的成员),若viritual纯虚则引发链接错误。析构函数同理
- 析构或析构函数通过调用non-virtual函数调用到virtual函数将使问题更加隐蔽,应注意。
- base class的构造析构需要调用派生类中特异的方法时,可以通过派生类的初始化列表向基类中传入参数,即在构造期间无法从base class调用到派生类中的方法,但是可以从派生类向base class传递参数以实现base class调用方法的特异性
10 令 operator= 返回一个reference to *this
- operator= operator+= 按惯例最好返回reference to *this,以便实现连续赋值
11 在operator=中实现"自我赋值"
- a[i] = a[j] *px = *py均可能时自我赋值行为,当operator=需申请内存以拷贝赋值对象时可能造成内存泄漏或意外销毁对象
- 利用 this == &py 判断是否自我赋值可以避免自我赋值错误,但是仅为自我赋值安全,为实现异常安全(申请内存过程可能出错)
- 对内部指针进行备份的巧妙设计可以避免内存泄漏或意外销毁对象,自我赋值安全又异常安全
- 更好的做法是将生成被拷贝对象的副本,并将此副本与拷贝对象交换(swap),基于此,可以设计operator=传入的参数为对象类型(制作拷贝构造函数时,参数为对象类型时会导致无限递归,事实上g++无法编译通过),而非其引用,这样在参数传递过程中自然进行了一次对被拷贝对象的复制,可生成效率较高的代码,但可能造成模糊的语义。在实际应用中应做适当取舍
12 复制对象时勿忘其每一个成分
- 编写拷贝构造函数或赋值函数时,应该 1复制所有local成员变量;2 调用base classes内的适当拷贝函数
- 拷贝构造函数与赋值函数往往具有相同的行为,令二者相互调用以避免代码重复的做法是不可取的,恰当的方法应该是将赋值行为放入private函数,来供二者同时调用
13 以对象管理资源
- 把资源放进对象内,便可以依赖C++析构函数自动调用机制确保资源被释放。基于此想法产生了智能指针auto_ptr及智慧指针shared_ptr
- 注意使用管理对象时,获得资源后应立刻放入管理对象;管理对象将运用析构函数保证资源被释放
- std::auto_ptr<Object> p(Object a).用智能指针这个管理对象保存需要使用的对象,待代码区块结束后,智能指针将自动delete a。为了保证delete只进行一次,必须保证同一对象只能被一个智能指针引用,这导致了智能指针之间赋值时,新指针将指向需求的对象,而原指针将被置为null
- std:tr1::shared_ptr<Object>(Object a)作为智能指针的升级,通过计数引用次数来销毁对象,不再有怪异的赋值行为。当对于环装引用,shared_ptr将无法释放资源
- auto_ptr及shared_ptr均不存在delete[]版本,因为vector及string可应付动态分配数组的需求
14 在资源管理类中小心coping行为
- 资源获得时机便是初始化时机,基于此的资源管理对象在构造时获取资源,在离开区块自动析构时释放资源
- 对于资源管理对象的赋值行为,可以1 禁止复制; 2对底层资源使用引用计数法(share_ptr)比较常用; 3 复制底部资源 4转移底部资源的拥有权(auto_ptr)
- share_ptr可以指定删除器,在引用计数为0时执行删除器
15 在资源管理类中提供对原始资源的访问
- auto_ptr和shared_ptr均提供get()以获取其内部资源。此二者还可以使用->及*直接访问资源的内部成员
- 自定义资源管理类需要提供客户访问原始资源的方法。包括显示访问(eg. get()方法)及隐式访问(operator type() 方式,在需要type类型时,自动调用此对象的type()将此对象转换为type类型)
- 显示访问比较啰嗦,但是安全;隐式访问调用时比较自然,但是容易造成错误比如在进行赋值操作时可能引起多个资源管理对象引用同一个底层资源的情况
16 在成对使用new与delete时要采取相同形式
- new -> delete new [] ->delete[]
- 当使用typedef int newtype[4] 时, newtype * a = new newtype,必须delete[] a。不建议此种类型的typedef
17 以独立语句将newed对象置入智能指针
- processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority()) 由于new Widget priority share_ptr执行顺序不确定(不同于Java C#),可能导致智能指针不能正确托管对象。
- 故组好将托管动作作为独立语句
18 让接口容易被正确使用,不易被误用
- 保持接口的一致性,设计类使其行为与内置类型一致
- shared_ptr支持定制型删除器,可防范DLL问题,可被用来自动解除互斥锁
19 设计class犹如设计type
- 考虑对象的创建与销毁,涉及构造析构、operator new operator new[] operator delete operator delete[]
- 考虑对象拷贝与构造时的差异,拷贝时传参 by value or by const reference
- 成员变量的合法值,做适当限制以减少异常
- 考虑class的继承关系,继承自别人,或将被别人继承(保持析构virtual)
- 考虑与其他类型的转换T1->T2,可以在T1中增加隐式转换函数operator T2,或者在T2中增加单参数构造函数(explicit or implicit)
- 考虑class应该具有的操作,对于不该存在的操作声明private
- 考虑成员变量和函数该如何访问,以确定 public private protected friends
- 未声明的借口???
- 并非定义单独类而是需要一个家族时,考虑是否该定义一个template class
- 新类只是为了拓展已有基类的功能,考虑是否可以定义一个或多个non-member函数或templates
20 宁以pass-by-reference-to-const 替换pass-by-value
- 以pass-by-reference-to-const 替换pass-by-value更高效切避免类型切割问题
- 对于内置类型 STL迭代器和函数对象,不适用以上规则
21 必须返回对象时,别妄想返回其reference
- reference只是个代表既有对象的名称,看到reference时立即想到其固有名称是什么!!!
- 任何函数,不可以返回local变量的reference;返回heap中的对象也不合理,1用户可能忘记delete;2对于a*b*c则必然内存泄漏;返回static变量更不合理,非线程安全!且 a*b 与c*d将用于相同;
- 可能有些编译器优化,返回reference to local 时 延迟对local变量的销毁???
22 将成员变量声明为private
- 保持了接口的一致性(都使用函数);可以对成员变量的访问权限做控制,读写操作可被侦测,做出相应动作;可以修改内部算法而不用改变客户程序;
- 不封装意味着不可改变;protected同private,并不比private更具封装性
23 宁以non-member、non-friend替换member函数
- 面向对象的核心是增强对成员变量的封装,而成员函数越少,则封装性越强。内部数据修改后,只影响有限的客户
- 在可实现相同功能的前提下,non-member及non-friend函数未增加成员函数,因而具有更高的封装性
- non-member及non-friend函数可以是其他类的成员
- C++的做法是,将non-member及non-friend函数放置于其所操作对象相同的命名空间中,并将不同功能的non-member放置于不同的头文件中。这样针对不同的用户需求只需添加相应的头文件
24 若所有参数皆需类型转换,请为此采用non-member函数
- 建立数值类型class时,提供隐式类型转换是合理的
- 如果需要为某个函数的所有参数(包括this指针)进行类型转换,那么这个函数必须是non-member
25 考虑写出一个不抛异常的swap函数
- 编写的class调用标准swap不耗费时间则没啥事,万一耗费时间(新class采用pimpl手法,数据由内部指针指领),可以对std下的swap特化成新class专用版本。此版本通常需要调用新class的成员函数,具体swap操作由成员函数调用标准swap交换指针完成swap操作
- STL容器也采用了上述手法,即为自己特化全局的swap,特化的版本仍属于std命名空间
- 当新编写的class为template class时,无法需对全局swap偏特化,但c++不支持模板函数偏特化。只能对swap进行重载,但是重载函数不同于特化,重载相当于重新编写了一个函数,故无法加入std标准库,所以最好将重载版本的swap放入新class同一命名空间下。
- 以上已解决template class的swap特化问题,同时适用于non-template class。但对于non-template class最好实现std下的特化版本,因为对于std::swap()这样的调用方式,非std命名空间下的swap将不会被采用
26 尽可能延后变量定义式的出现时间
- 尽可能延后变量定义,甚至到此变量有初值时再定义。1因为代码逻辑可能跳过预先定义的变量,而使它的出现没有任何意义 2 利用初值构造变量可以省去一次默认构造
- 以上做法在改善程序效率的同时可增加程序的清晰度
27 尽量少做转型动作
- 编译器面对转型语句,并非只是改变对数据类型的认识而什么都不做。编译器为转型语句生成代码(eg1. int与double在计算机内部的表示并不相同 eg2.base* p = &derivedO,实际这两个地址可能有偏移,取决于编译器实现)
- static_cast<base>(*this).op(),并非base::op(),前者只是将*this转为临时基类对象并操作(由于是类型转型,非指针转型,会切除派生类的特性,故即使op为虚函数调用的也是base的版本),并不会影响this中的成员(如果是static_cast<base*>(this).op()则会影响this中成员,此时注意,若op为virtual则会递归调用!,若非virtual则可以实现想要的功能)
- 处理需要向下转型的方法1使用容器保存派生类的指针,其迭代器类型即为派生类;2在base中实现需要在drived中调用的函数virtual版本
- 使用c++提供的转型函数;避免使用dynamic_cast;转型无法避免则塞入函数避免向客户暴露
28 避免返回handles指向对象内部成分
- 即使以const方式返回内部对象的引用,也有造成悬垂引用的风险(对象已销毁,而客户仍保留有对其内部变量的引用)
29 为"异常安全"而努力是值得的
- 异常安全函数需提供以3个保证之一:1基本承诺,如果异常抛出,程序应处于一个合理的但却不确定的状态;2强烈保证,如果抛出异常,程序必须处于调用之前的状态;3不抛异常保证,作用于内置类型的操作均可保证不抛出异常
- 强烈保证除了利用对象管理资源外往往需要copy-and-swap,即拷贝需要修改的数据,在完全完成所有修改操作后,再将新数据通过不抛异常的swap置换入原对象。此方法意味着较低的效率,实际运用时需要取舍
- 函数提供的异常安全保证,取决于启动安全保证最弱的函数;内部有多个函数挑用时强烈保证往往难以达成,因为其中未抛异常的函数将难以恢复到调用前的状态。故代码需要调用老式非异常安全代码时,将无奈地无法保证异常安全
30 透彻理解inlining的里里外外
- inline函数除免除调用成本外,还有利于编译器对inline调用函数的优化,因为编译器最优化机制通常用来浓缩不含函数调用的代码
- 过度使用inline会使代码膨胀,降低高速缓存的命中率;如果inline函数小到不及函数调用的开销,则上述弊端并不存在
- 大多数编译器在编译器inline,少部分在连接器inline,.NET CLI可以再运行期inline
- 大多数编译器拒绝为大函数inline并生成警告,内含virtual调用的函数无法inline,因为virtual需要在运行期才能确定
- inline函数可能也会被编译器生成outline版本,当有通过指针调用inline函数时(人为的或者编译器生成构造析构函数的outline版本)
- 构造与析构往往不该inline,因为编译器实际为构造析构添加了许多额外代码(构造析构基类与成员变量)
- inline函数无法通过库替换的方式升级;inline函数无法进行调试;
- 鉴于inline的优势与缺点,应该在必要的时刻慎重inlining
31 将文件间的编译依存关系降至最低
- C++class的定义式不只包含接口还可以包含对象,这其实暴露了所包含对象过多的细节(需要include被包含对象的定义以确定其确切大小便于开辟空间,而成员函数的参数及返回值类型即使是对象,也并不需要包含其定义式),这就使得class定义式与include的定义式产生依赖
- 以声明的依存性替换定义的依存性。做到 1 成员变量能用指针或引用定义尽量这么用 2尽量以类的声明式替换定义式 3 制作库时提供声明与定义两个头文件,前者交给客户,后者自用
- 将实现与接口分离,两种具体的实现途径1在类定义中数据成员仅包含指针指向具体实现类(其中包含了真正的数据成员)的一个智能指针对象,具体实现类拥有与主类中完全一样的方法。主类方法的实现依靠直接调用实现类的方法; 2 创建接口类,由库继承并实现接口(对接口具现化),客户通过接口类内部的静态方法获取动态的具现化类对象,并通过接口类的指针操控具现化对象(此即为工厂模式)
- 程序库头文件应该以完全且仅有声明式的形式存在
32 确定你的public继承塑模出is-a关系
- public继承意味is-a,适用于base classes身上的每一件事情一定适用于derived classes身上,因为每一个derived class对象也都是一个base class对象
33 避免遮掩继承而来的名称
- 派生类中的名称会遮掩基类中的名称(不比对具体类型向量,只关注符号),但是在public继承下要满足is-a,必然不允许派生类遮掩基类
- 在public继承下,同一函数有多个成员函数重载版本时,派生类中的一个同命函数会将其全部遮掩,为了暴露基类的成员函数,应该 using base::symbol
- 在private继承下,为了暴露基类成员函数应该使用转交函数(派生类内定义同名函数并以inline方式调用基类版本)
34 区分接口继承和实现继承
- pure virtual函数用于接口继承;impure virtual函数用于接口继承及缺省实现;non virtual函数用于即可继承及强制实现继承
35 考虑virtual函数以外的其他选择
- 选择一:NVI(non-virtual interface,是一种template method设计模式)手法即将virtual函数作为private而被public的non-virtual方法调用,这种外覆式(wrapper)的手法可以在virtual函数前后做一些手脚
- 选择二:借由函数指针实现的策略模式,即对象初始化时传入函数指针,用来实现对象行为的类virtual化;优点 1 相同对象也可以有不同的行为 2对象的行为可以在运行期改变。缺点 需要对象弱化封装以完成外部函数的功能。(优缺点同以下选择)
- 选择三:借由tr1::function<>。是对选择二的增强,可以将任何满足tr1::function函数模板的对象作为类virtual函数使用。可以是1函数指针(选择二);2 函数对象;3其他类的成员函数(借由tr1::bind)
- 选择四:经典策略模式:在原有对象的继承体系之外,重新建立一套策略继承体系。原有体系内包含策略体系的基类指针。这样以实现原有对象的类virtual功能。
36 绝不重新定义继承而来的non-virtual函数
- 从使用角度:重写non-virtual函数,会导致同一个对象以 base或drived的方式引用时会有不同的行为,这是奇怪的。
- 从理论角度:drived is-a base,34 non virtual函数用于即可继承及强制实现继承, 重写non-virtual不合此用意
37 绝不重新定义继承而来的缺省参数值
- virtual函数时动态绑定的即其行为随具体引用的对象而改变,virtual函数的缺省参数却是静态绑定(C++为运行效率而为)其行为仅与引用本身的类型有关。
- 缺省参数不同时,会导致以指向派生类的基类指针调用virtual函数时,执行了派生类的函数体,却使用了基类的缺省参数。
- 必须做到基类派生类缺省参数相同。为防止修改也必须同步进行等麻烦事,建议使用NVI手法,基类的non-virtual函数使用缺省参数并调用private virtual函数(参数不缺省并由non-virtual传入)
38 通过复合塑模除has-a或“根据某物实现出”
- 复合可以塑造has-a以及利用xxx实现两种语义。前者针对同属应用域的对象间的复合,后者用于应用域对象内复合了实现域对象
39 明智而审慎地使用private继承
- private继承的意义仅限于实现层面,不具备类似public继承的is-a关系,只是为了以继承的方式实现38中利用xxx实现的语义,意味着部分实现被继承,而接口被略去
- 需要利用xxx实现的语义时,当派生类需要访问基类的protected成员或者需要实现virtual函数时或者不愿意增加派生类的大小时可以选择private继承;但选择复合更容易理解,且可以模拟java/C#中禁止重新定义virtual函数
40 明智而审慎地使用多重继承
- 多重继承可能继承到相同的符号symbol,编译器在首先进行的最佳匹配中对这些完全一样的匹配无法做取舍,在进行取用时必须指明符号的域(所在命名空间)
- 多重继承可能使派生类中存在多份同一基类的成分,不希望存在此情况则基类的直接派生类需要以virtual方式继承。这种做法会使基类以指针方式被派生类引用,会造成初始化时派生对象的额外开销;访问基类成员迟缓;产生的对象体积偏大
- virtual base class不带任何数据时最具实用价值
- 多重继承典型情况,public继承接口类以约束派生类的行为,同时private继承某个工具类使帮助实现派生类的功能(见39)
41 了解隐式接口和编译期多态
- class 面向对象编程 template 模板元编程 都支持接口和多态
- 对class而言,其接口由函数签名确定(显式),其动态性由virtual函数在运行期体现
- 对template而言,其接口基于有效表达式(隐式),其动态性由模板中类型的替换在编译期体现
42 了解typename的双重意义
- 任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置写上typename
- 但不得在基类列或成员初值列内以它作为base class修饰符
43 学习处理模板化基类内的名称
- 当我们从object oriented C++ 跨进template C++继承旧不像以前那般畅行无阻了
- 继承于模板类的派生类内无法直接调用继承自基类的函数,因为模板类的特化版本可能不含该函数。解决方式1利用this->调用继承自模板基类的函数;2 using 声明模板基类中需要被派生类调用的方法;3 利用模板基类+限定符指明被调用函数的作用域(方法3不推荐,因为对于virtual函数将关闭其virtual特性)
- 在template代码中重复是隐晦的,必须去设想当template被具现化多次时可能发生的重复
- 非类型参数模板往往可以消除,以带有非类型参数的模板类继承仅含类型参数的模板类,并将非类型参数作为普通参数传入模板基类。可以降低代码重复;可以非类型模板参数确定对象类型,在保证派生对象不是相同类型的同时将非类型参数提前传入,有助于编译器优化。
- 类型参数而造成的膨胀亦可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码
45 运用成员函数模板接受所有兼容类型
- 比如智能指针实际为内含指针的模板类,当对象指针间具有继承关系时,它们对应的智能指针间却无法进行转型。此时可以声明泛化拷贝构造函数(非explicit以便隐式赋值)。为防止可以使任意模板间可以互相转换(比如基类转型派生类),可在初始化列表中将类内的裸指针进行拷贝赋值,若转型不合法可在编译器报错
- 泛化构造函数在具现化时若 两者模板类相同,则退化成为了正常的拷贝函数。为了全面的控制赋值/赋值行为,在声明泛化拷贝构造/泛化赋值函数的同时应该声明正常的拷贝构造/赋值函数
- 24中参数均需类型转换时,建议定义非friend非成员的函数。当类型为模板类时,非friend非成员的函数因不能根据需要类型转换的参数进行具现化,进而对参数进行隐式转换(先有鸡先有蛋的问题);故将非成员函数定义为friend,这样friend函数可以同模板类一同具现化,进而可以对参数进行隐式转换。(具体方法体可以定义在类中,可以定义于类外的模板函数中并由friend函数调用来实现具体所需功能)
- 想要实现迭代器,首先迭代器有不同特性,在实现advance方法时根据特性的不同实现方式有别,这就要求迭代器携带有“类型信息”,但是内置指针也是一种迭代器(却无法将额外信息注入其中)。
- 所以定义一个模板类(traits 榨汁机),可以具现化为一组因迭代器类型不同而不同的类型,在这些类内部typedef各自的迭代器类型,这样可以针对内置指针类型进行特化,以将typedef信息打入内置指针类型对应的模板类中。
- 在具体实现算法时,将上述模板类内typedef的类型作为参入放入一组预先定义好的重载算法中,就可以在编译期根据迭代器的不同而选择不同的算法实现
- 模板元编程可以将工作由运行期移往编译器,因而得以实现错误侦测及更高的执行效率
- 模板元编程可以用来生成“基于政策选择组合”的客户定制代码,可以用来避免生成对某些特殊类型并不适合的代码
- operator new 用于申请内存;new operator 用于初始化对象,内部会调用operator new
- set_new_handler被设置后,operator new异常则会被执行
- 使类拥有专属的new_handler: 类中重载operator new函数,首先调用全局set_new_handler装入专属new_handler,然后调用全局operator new,退出前记得在类析构时还原全局new_handler。在此可以使用对象来管理被替换除的原有new_handler,以便局部管理对象在退出重载的operator new函数时,自动析构以达到自动还原全局new_handler的目的。
- 以上做法可进一步加强:制作一个模板基类。基类中重载operator以便子类继承,同时模板可以保证内部存储new_handler的对象的唯一性(对于每一个需要特异化new_handler的类而言) 即 class A :public BaseHandler<A> 怪异的循环模板模式或者叫 DO IT FOR ME
- 要获取现有的new_handler,必须set_new_handler以寄出原handler
- 自定义new delete的好处 检测运行错误,统计内存使用信息,提升处理效率,降低额外开销,自定义对齐模式,为相关对象统一分配内存,获得非传统行为
- C++规定即使new 0bytes,也必须返回有效指针。operator new 中必须有一个无线循环,申请内存失败后 或者调用new_handler释放空闲内存;或者将new_handler置空(抛出异常);或者抛出异常bad_alloc;或者直接终止程序,才有可能退出循环
- operator new被重载过的类作为基类时,其派生类会调用基类的operator new版本,导致申请的内存大小仅适合基类,这时需要在operator new内部做适当判断,发现申请的内存大小大于sizeof(base)时调用 全局operator new申请内存
- 重载operator new[]时无法确定 需要的元素个数,并且这块内存可能有额外的用于记录元素个数的开销,所以参数size并不等于元素个数*sizeof(base)
- C++保证删除null指针永远安全,在重载delete时一定检查输入是否为null;当new针对sizeof(base)做处理时,delete也该有相应动作。此外,若基类析构不为虚时,其派生类使用基类的operator delete时会被传入错误的size,这是基类析构为virtual的又一个原因
- placement new 广义上指重载的operator new带有除 size之外的其他参数。狭义上的placement new指带有的额外参数是一块内存的首地址,返回的内存将从中划分
- C++自带全局的operator new有
void* operator new(std::size_t) throw(std::bad_alloc)
void* operator new(std::size_t, void*)throw()
void* operator new(std::size_t, const std::nothorw_t&) throw()
自定义operator 会掩盖以上函数,需要自定义类中全局版本可用 使用using(见33);简单做法是需要类中声明所有正常形式的new及相应的delete。觉不能无意识地掩盖全局版本
- 声明placement new 必须有相同参数的placement delete,这个版本仅用于 成功placement new后的构造函数出错时,由C++系统自动调用释放内存(否则内存泄漏)。此外还需要有正常版本的delete,以便用户手动释放对象
53 不要轻忽编译器的警告
54 让自己熟悉包括TR1在内的标准程序库
- C++标准程序库 STL iostreams locales C99标准程序库
- TR1添加了 智能智能 一般化函数指针 hash容器 正则表达式 及另外10个组件
- Boost