class Uncopyable { protected: // allow constructor and destructor for derived object Uncopyable() {} ~Uncopyable() {} private: Uncopyable(const Uncopyable&); //avoid copying Uncopyable& operator=(const Uncopyable&); 07.声明多态基类析构函数为虚函数 包含虚函数的类需要额外的信息来实现虚函数:vptr(virtual table pointer)指向一个由函数指针构成的数组,称为 vtbl(virtual table),每个有虚函数的类都有一个相应的 vtbl 析构顺序:先父类再子类,构造函数的调用顺序相反 带有多态性质的基类应声明一个虚析构函数 如果一个类带有任何虚函数,就声明一个虚析构函数 类的设计目的不是作为基类使用,或者不是为了多态性,不应该声明虚析构函数 08.别让异常逃离析构函数 确保对象自我赋值时,operator= 行为良好,包括比较源对象和目标对象的地址、精心周到的语句顺序(先复制源对象,再执行删除),以及icopy-and-swap 确定任何函数如果操作一个以上的对象,而其中多个对象时同一个对象时,行为仍然正确 12.复制对象的所有部分 拷贝构造函数和拷贝赋值操作符都是 copying 函数 copying 函数应该确保复制“对象内的所有成员变量”和“所有基类成分” 不要尝试以某个 copying 函数实现另一个 copying 函数,应该将相同的东西抽象成一个函数,二者都调用这个函数 3.资源管理 为防止内存泄漏,建议使用 RAII(Resource Acquisition Is Initialization,资源取得时机就是初始化时机) 对象,它们在构造函数中获得资源并在析构函数中释放资源 常用的 RAII 类是 shared_ptr 和 auto_ptr。前者的拷贝行为比较直观,后者的复制动作会转移资源的所有权:shared_ptr 有引用计数,但是无法打破环装引用 参考智能指针一文 14.在资源管理类中小心复制行为 复制 RAII 对象必须一并复制它锁管理的资源,所以资源的 copying 行为决定 RAII 对象的 copying 行为 一般情况下,RAII 类的 copying 行为是:阻止 copying、实行引用计数法 15.在资源管理类中提供对原始资源的访问 APIs 往往要求访问原始资源,所以每一个 RAII 类应该提供一个接口可以获得其管理的资源 对原始资源的访问可以是显示转换或隐式转换:一般显示转换比较安全,隐式转换对客户比较方便 16.在对应的 new 和 delete 采用相同形式 “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容 “阻止误用”的办法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任 shared_ptr 支持自定义删除器,可以防止 DLL 问题,可被用来自动解除互斥锁 19.把类设计看作类型设计 对象的初始化和对象的赋值该有什么样的差别:区分构造函数和赋值操作符的行为 新类型的对象如果以值传递,意味着什么:取决于拷贝构造函数 什么是新类型的“合法值”:确定需要做的错误检查工作 新类型需要配合某个继承图系吗:受继承类的约束,如果允许被继承,析构函数是否为虚函数 新类型需要什么样的转换:显示类型转换和隐式类型转换 什么样的操作符和函数对此新类型是合理的:确定需要声明的函数,哪些是成员函数,哪些不是成员函数 谁该调用新类型的成员:确定成员的属性(public/protected/private),也确定类之间的关系(所属,友元) 什么是新类型的未声明接口 新类型有多一般化:是否需要定义一个模板类 真的需要一个新类型吗:是否可以为已有类添加非成员函数或模板来实现 20.常量引用传递优于值传递 值传递效率低,而且可能造成对象切割(slicing):值传递一个衍生类对象时,如果函数声明的是基类,那么调用的是基类的拷贝构造函数 C++ 编译器底层使用指针实现,不同情形使用不同的方式 内置类型(如 int)采用值传递 STL 的迭代器和函数对象使用值传递 其他的采用常量引用传递 21.必须返回对象时,不要返回引用 绝不要返回指针或引用指向一个 local stack 对象 绝不要返回引用指向一个 heap-allocated 对象 绝不要返回指针或引用指向一个 local static 对象而有可能同时需要多个这样的对象 22.声明数据成员为私有的 提供一个 public swap 成员函数,在函数内高效地置换两个对象值 在类或模板所在的命名空间提供一个非成员的 swap 函数,在函数内调用上述 swap 函数 如果正在编写一个类或类模板,让该类特化 std::swap,另其调用上述的 swap 函数 如果调用 swap,确定包含using std::swap,然后不加任何 namespace 修饰符,直接调用 swap,编译器就会查找适当的 swap 函数并调用 警告:成员函数 swap 不可抛出异常 const_cast<T>( expression )用来移除对象的常量性,唯一可以实现这个目的的 C++ 风格的转换操作符 dynamic_cast<T>( expression )用于执行“安全向下转换”,用于确定某对象是否归属继承体系中的某个类型,可能耗费重大运行成本,唯一一个 C 风格无法实现的转换操作 reinterpret_cast<T>( expression )意图执行低级转换,实际动作和结果可能取决于编译器,即不可移植 static_cast<T>( expression )用于强迫隐式转换,例如 non-const 转换为 const,或者 int 转 double 等 倾向使用 C++ 风格的转换操作,不要使用 C 风格的转换 易被辨识,因而得以简化查找类型被破坏的过程 各转换工作有各自的局限,便于编译器诊断错误的运用 如果可以,尽量避免转换操作,特别是在注重效率的代码中避免 dynamic_cast,如果有需要,尝试改成无需转换的设计 使用类型安全容器,确定是哪种衍生类或基类 将虚函数放在父类,然后添加空实现 如果必须转换,试着用函数封装,可以调用函数,而无需将转换操作引入代码 28.避免返回指向对象内部的句柄 避免返回 handles(包括引用、指针、迭代器)指向对象内部。一遍增加封装性,帮助 const 成员函数的行为像个 const,并将发生 dangling handles 的可能性降至最低 29.努力写异常安全的代码 异常安全函数即使发生议程也不会内存泄漏或破坏任何数据结构。这样的函数分为三种可能的保证:基本型、强烈型、不抛异常型 “强烈保证”往往以 copy-and-swap 实现,但“强烈保证”并非对所有函数都可实现或具备现实意义 函数提供的“异常安全保证”通常最高只等于其调用的各个函数的“异常安全保证”中的最弱者 30.了解内联的细节 pimply idiom(pointer to implementation):将一个类分为两个,一个提供接口,一个负责实现接口,前者在类内包含一个后者的 shared_ptr,做到“接口与实现分离” 使用接口类、衍生类和工厂模式进行实现 分离的关键在于“声明的依存性”替换“定义的依存性”:让头文件尽可能自我满足,万一做不到,则使用前置声明 尽量使用对象引用或对象指针,而不是对象:可以在头文件中使用前置声明 尽量使用 class 声明式而不是 class 定义式 为声明式和定义式提供不同的头文件 程序头文件应该以“完全且仅有声明式”的形式存在 6.继承与面向对象设计 如果继承基类并加上重载函数,又希望重新定义或覆盖其中一部分,必须为那些原本会被隐藏的名称引入一个 using 声明式,否则继承的名称会被隐藏 为了让隐藏的名称仍然可见,可使用 using 声明式或 forwarding 函数 内置的 forwarding 函数的另一个用途是为那些不支持 using 声明式的编译器而用 34.区分接口继承和实现继承 接口继承和实现继承不同。在 public 继承时,衍生类会继承基类的接口,即成员函数 声明纯虚函数的目的是让衍生类只继承函数接口 声明非纯虚函数的目的是让衍生类继承该函数的接口和缺省实现 声明非虚函数的目的是让衍生类继承函数的接口和一份强制性实现 35.考虑虚函数的替代 使用 non-virtual interface(NVI)手法,是 Template Method 设计模式的一种特殊形式。以 public non-virtual 成员函数包裹较低访问性的虚函数 将虚函数替换为“函数指针成员变量”。是 Strategy 设计模式的一种分解表现形式 以 function 成员变量替换虚函数,因而允许使用任何可调用实体(callable entities)搭配一个兼容与需求的签名式。这也是 Strategy 设计模式的某种形式 将继承体系内的虚函数替换为另一继承体系的虚函数。这是 Strategy 设计模式的传统实现手法 将功能从成员函数移到类外部,缺点是非成员函数无法访问类的 non-public 成员 function 对象的行为就像一般函数指针。这样的对象可接纳“与给定的目标签名式兼容”的所有可调用实体 36.绝不重定义继承的非虚函数 动态类型可在程序执行过程中改变 可以使用 NVI 手法:另基类内的一个 public 非虚函数调用 private 虚函数,后者可被衍生类重新定义。让非虚函数知道缺省参数,虚函数负责真正的工作 38.通过组合对”has-a”或”is-implemented-in-terms-of”建模 复合是类型间的一种关系,当某种类型的对象内包含其他类型的对象,就是复合关系 在应用域,复合意味着 has-a(有一个)。在实现域,复合以为着 is-implemented-in-terms-of(根据某物实现出) 39.慎重使用私有继承 private 继承意味着 is-implemented-in-terms-of。通常比复合的级别低,但是当衍生类需要访问基类的 protected 成员,或需要重新定义继承而来的虚函数时,private 继承是合理的 private 继承时,编译器不会自动将一个衍生类对象转换为一个基类对象 由 private 继承而来的所有成员,在衍生类中都是 private 属性 private 继承是一种实现技术,意味着只有实现部分被继承,接口部分应忽略 与复合相比,private 继承可以使得空白基类最优化(EBO, empty base optimization)。对致力于“对象尺寸最小化”的程序库开发者比较重要 尽可能使用复合,必要时采用 private 继承 当想要访问一个类的 protected 成员,或需要重新定义该类的一个或多个虚函数 当空间更加重要,衍生类的基类可以不包含任何 non-static 成员变量 “独立(非附属)”对象的大小一定不为零,不适用于单一继承(多重继承不可以)衍生类对象的基类 40.慎重使用多重继承 虚继承:防止多重继承时,基类之间又有基类,从而上层的基类的成员变量被父类复制 虚继承的类产生的对象体积更大,访问虚基类的成员变量速度慢,增加初始化(及赋值)的复杂度 如果虚基类不带任何数据,是具有使用价值的情况 多重继承比单一继承复杂,可能导致新的歧义性,以及对虚继承的需要 多重继承的用途:涉及“public 继承某个接口类”和“private 继承某个协助实现的类” 7.模板与泛型编程 类和模板都支持接口和多态 对类而言接口是显式的,以函数签名为中心。多态则是通过虚函数发生于运行期 对模板参数而言,接口是隐式的,基于有效表达式。多态则是通过模板具体化和函数重载解析,发生于编译期 42.理解 typename 的双重定义 模板生成多个类和多个函数,所以任何模板代码都不该与某个造成膨胀的模板参数产生依赖关系 因非类型模板参数造成的代码膨胀,往往可以消除,做法是以函数参数或类成员变量替换模板参数 因类型参数造成的代码膨胀,往往可以降低,做法是让带有完全相同二进制表示的具体类型实现共享代码 45.使用成员函数模板来接受“所有兼容类型” 具有基类-衍生类关系的两个类型分别具体化某个模板,生成的两个结构并不带有基类-衍生类关系 使用成员函数模板生成“可接受所有兼容类型”的函数 如果声明成员模板用于“泛化拷贝构造”或“泛化赋值操作”,必须声明正常的拷贝构造函数和拷贝赋值操作符 声明泛化拷贝构造函数和拷贝赋值操作符,不会阻止编译器生成默认的拷贝构造函数和拷贝赋值操作符 46.需要类型转化时在模板内定义非成员函数 input 迭代器:只能向前移动,一次异步,只可读取(不能修改)所指的东西,且只能读取一次。模仿了指向输入文件的读指针。如 C++ 的 istream_iterator output 迭代器:只能向前移动,一次一步,只可修改所指的东西,且只能修改一次。模仿了指向输出文件的写指针。如 C++ 的 ostream_iterator input 和 output 迭代器都只适合“单步操作算法(one-pass algorithms)” forward 迭代器:既能完成上述两种迭代器的工作,且可以读或写所指对象一次以上。使得可以实施“多步操作算法(multi-pass algorithms)”。如单向链表的迭代器 bidirectional 迭代器:既能完成 forward 迭代器的工作,还支持向后移动。STL 的 list/set/multiset/map/multimap 迭代器就属于这一分类 random access 迭代器:可以执行“迭代器运算”,即可以在常量时间内向前或向后跳跃任意距离。如 array/vector/deque/string 提供的都是随机访问迭代器 如何设计一个 traits 类 确认若干希望将来可取得的类型相关信息。例如迭代器希望取得分类(category) 为该信息选择一个名词。如迭代器是 iterator_category 提供一个模板和一组特化版本,其中包含希望支持的类型相关信息 traits 类的名称常以”traits”结束 如何使用一个 traits 类 建立一组重载函数(类似劳工)或函数模板,彼此间的差异只在于各自的 traits 参数。令每个函数实现与其接受的 traits 信息相对应 建立一个控制函数(类似工头)或函数模板,调用上述的函数并传递 traits 类所提供的信息 traits 类使得“类型相关信息”在编译期可用。它们以模板和一组“模板特化”完成实现 整合重载技术后,traits 类可在编译期对类型执行 if…else 测试 48. 认识模板元编程 让某些事情更容易 可将工作从运行期转移到编译期。使得原本在运行期才可以侦测的错误在编译期被找到 TMP 的 C++ 程序在每一方面可能更加高效:较小的可执行文件、较短的运行期、较少的内存需求 缺点:导致编译时间变长 TMP 主要是函数式语言,可以达到的目的 确保度量单位正确:在编译期确保程序所有度量单位的组合是正确的 优化矩阵运算:使用 expression template,可能会消除中间计算生成的临时对象并合并循环 可生成用户自定义设计模式的实现品。设计模式如 Strategy/Observer/Visitor 等都可以多种方式实现 语法不直观 支持工具不充分,如没有调试器 8.定制 new 和 delete new和delete只适合分配单一对象;new []和delete []用来分配数组 STL 容器所使用的 heap 内存是由容器所拥有的分配器对象(allocator objects)管理,而不是 new 和 delete 管理 49.理解 new-handler 的行为 可以用是set_new_handler设置该函数 参数是个指针,指向 new 无法分配足够内存时该调用的函数 返回值是个指针,指向set_new_handler被调用之前正在执行的 new_handler 函数 new_handler 是个 typedef,定义一个指针指向函数,函数没有参数也没有返回值 设计良好的 new-handler 函数 让更多内存可被使用:程序一开始执行就分配一大块内存,而后第一次调用 new-handler,将该内存释放给程序使用 设置另一个 new-handler:如果已知哪个 new-handler 可以获得更多可用内存,调用时设置该 new-handler 替换自己。比如令 new-handler 修改“会影响 new-handler 行为”的静态数据、命名空间数据或全局数据 取消设置 new-handler:即将 null 指针传给set_new_handler,内存分配不成功时就会抛异常 抛出 bad_alloc 或派生自 bad_alloc 的异常:该异常不会被 new 操作捕获,但会传播给请求内存的代码 不返回:通常调用 abort 或 exit nothrow new是一个有局限性的工具,因为它只适用于内存分配;后续的构造函数调用还是可能抛出异常 50.理解何时替换 new 和 delete 有意义 检测运用上的错误:自定义 new 操作,可超额分配内存,以额外空间放置特定的 byte patterns(即签名,signature)。对应的 delete 操作可以检查上述签名是否原封不动,若否表示在分配区的某个声生命时间点发生了 overrun(写入点在分配区块尾端之后) 或 underrun(写入点在分配区块起点之前)。此时 delete 可以日志记录该时间和发生错误的指针 强化效能:编译器的 new 和 delete 无法解决碎片问题,导致程序可能无法申请大区块内存。通常来说这种自定制的性能更好 收集使用上的统计数据:先收集软件如何使用动态内存,包括分配区块的大小分布、寿命分布、分配和释放的次序(FIFO/LIFO/随机)、任何时刻内存分配上限 增加分配和释放的速度:当定制型分配器专门针对某特定类型的对象设计时,往往比泛用型分配器更快 降低缺省内存管理器带来的空间额外开销:泛用型内存管理器往往使用更多内存 弥补缺省分配器中的非最佳对齐:缺省的分配器一般是 4 字节对齐,但是对于 x86 最好是 8 字节对齐 将相关对象成簇集中:将往往被一起使用某个数据结构放在一起创建,可以减少 page fault 的错误 获得非传统的行为:比如添加数据初始化工作 51.写 new 和 delete 时遵循惯例 如果自己实现一个 placement operator new,也要写出对应的 placement operator delete。否则会发生隐蔽时断时续的内存泄漏 当声明 placement new 和 placement delete,确定不要无意识地遮掩它们的正常版本 9.杂项讨论 严肃对待编译器发出的警告信息。努力在编译器的最高(最严苛)警告级别下争取“无任何警告” 不要过度依赖编译器的报警能力,因为不同的编译器对待事情的态度不相同。一旦移植到另一个编译器上,原本依赖的警告信息有可能消失 54.熟悉包括 TR1 在内的标准库
using std::swap
const_cast<T>( expression )
dynamic_cast<T>( expression )
reinterpret_cast<T>( expression )
static_cast<T>( expression )
new
delete
new []
delete []
set_new_handler
nothrow new