组织和策略问题
0. 不要为小事斤斤计较。(或者说是:知道什么东西不需要标准化)
无需在多个项目或者整个公司范围内强制实施一致的编码格式。只要规定需要规定的事情:不要强制施加个人的喜好或者过时的做法。
C++不应该使用匈牙利命名法。在有智能指针的情况下,单入口单出口可能不是必须的。代码要有自注释性。
1. 在高警告级别下干净地编译代码。
要把警告放在心上:使用你的编译器的最高警告级别。要求干净(没有警告)的构建。理解所有的警告。通过修改你的代码来消除警告,而不是降低警告级别。当单独禁用警告时,尽量在局部范围:#pragma warning (disable: 4101)
2. 使用自动构建系统。
触动单一的按钮:使用一个不需人工干预的构建整个工程的完全自动化(“单动作”)的构建系统。
3. 使用版本控制系统。
好记性不如烂笔头:使用版本控制系统(VCS)。永远不要让文件长时间地签出(check out)。在你通过更新的单元测试后,要经常性地签入(check in)。使得签入的代码不会破坏构建。
4. 投资于代码评审。
评审代码:人多力量大。向他人展示你的代码,阅读其他人的代码。大家都将受益。
设计风格
5. 一个实体一个紧凑的责任(单一职责原则)
一个时刻关注一件事情:赋予每个实体(变量,类,函数,名字空间,模块,库)一个定义明确的责任。随着实体的演化,其责任的作用域也随之加大,但其责任不能发散。
6. 正确性,简单性和清晰性是第一位的。
KISS(Keep It Simple and Stupid,保持软件简单):正确优于快速。简单优于复杂。清晰优于伶俐。安全优于不安全。
不要使用不必要的或者小聪明式的操作符重载。
应该使用命名变量,而不要使用临时变量,这能避免函数声明的二义性。
7. 编码时,知道什么时候和如何考虑可伸缩性。
小心爆炸性的数据增长:不要过早地优化,着眼于渐进的复杂度。对于作用于用户数据的算法,数据处理的复杂度应该是可预测的,最好是不比线性差,避免指数级算法。在证明了优化是有必要而且重要以后(特别是由数据量增加所引起的),应该集中于提高O复杂度,而不是作像少用一个额外的加法那样的优化。
8. 不要过早地进行不成熟的优化。
快马无需加鞭:不成熟的优化(以性能为名,使设计复杂、可读性差)的诱惑很大,但是危害也严重。
优化的第一个原则是:不要去优化。优化的第二个原则(只对专家来说)是:还是不要去优化。再三衡量,而后优化。
记住:让一个正确、清晰的程序更快,比让一个快速的程序正确、清晰,要容易的多。
9. 不要过早地进行不成熟的劣化。
自己要轻松点,代码也是一样:在其他所有的情况都相同的情况下(尤其是代码复杂性和可读性),使用某个有效的设计模式和编码惯用法应该是你信手拈来的事,而且它不能比悲观的候选方法更难于编写。这是在避免没有必要的劣化。
10. 使全局和共享数据最少。
共享会引起争夺:避免共享数据,尤其是全局数据。共享数据会增加耦合性,降低可维护性,而且常常会降低效率。
共享数据会影响单元测试,污染全局空间,不同编译单元的初始化顺序未定,影响多线程环境(最好用通信、序列化)。
11. 隐藏信息。
不要泄密:不要暴露一个提供了抽象的实体的内部信息。公开抽象而非公开数据。
例外:值的聚合(C语言的struct)只是简单的数据集合,数据本身就是接口,没有其他抽象,所以其成员可以公开。
12. 知道什么时候和如何为并行编码。
线程安全:如果应用程序使用了多线程或多进程,就该尽量减少共享对象(参见Item10),而且要安全地共享这些对象。
C++标准对于线程没有任何概念,所以:要有对应平台原语;保证自己实例的非共享、非静态;明确设计并说明需要客户加锁、内部加锁、还是根本无需加锁。
13. 保证资源被对象所拥有。使用显式的的RAII和智能指针。
当你有强力工具时,不要手工去锯:C++的“资源获取既是初始化”(RAII)惯用法是一个强大的正确的资源处理工具。在申请一块原始资源时,立即把它传递给一个属主对象。绝不要在单一语句中申请多个资源。
在使用RAII时,要小心copy构造和赋值行为:禁用、复制、转移还是增加引用计数,要配合对应的RAII类的行为。
编码风格
14. 优先使用编译时和链接时错误,而不是运行时错误。
不要把可以在构建时做的事情推迟到运行时来做:优先编写那些在编译阶段利用编译器来检查不变量的代码,而不是在运行时来检查它们。运行时检查时控制和数据是独立的,也就是说你不能彻底地了解它们。相反,编译时检查是非控制和数据独立的,不会带来运行时开销,而且它还能提供更高等级的可信任度。
15. 优先使用const。
const是好朋友:不可变的值更容易理解,跟踪和解释。所以当变量很敏感时要优先使用常量,当你定义一个值的时候把const作为默认选择。它很安全,在编译时检查,而且它还和C++的类型系统集成在一起。不要轻易去除const限定。
16. 避免使用宏。
C++中:const或者enum定义常量;inline避免函数的展开开销;template指定函数系列;namespace避免名称冲突。使用场景:重复包含头文件的预防;#ifdef和#ifndef.
17. 避免使用奇异数(magic number)。
避免在代码中出现像41或3.14159这样的文字常量。它们不具备自我解释能力,使用诸如符号名或表达式来代替。
18. 尽可能局部化变量的声明。
避免作用域的膨胀,对需求和变量都是一样:变量引入一个状态,你应该处理尽可能少的状态和尽可能短的生命期。
它的名字会污染上下文,甚至名字空间;初始化顺序可能还会造成问题。
19. 始终要初始化变量。
改过自新:在C和C++程序中,未初始化的变量是一个常见的bug来源。在变量定义时进行初始化。
20. 避免长方法(函数)。避免深层的嵌套。
短比长好,平比深好:过于长的函数和嵌套过深的代码块通常是由于没有赋予函数一个单一内聚的职责而引起的,通常可以通过更好的重构来解决。
一些合理的建议:1)单一职责;2)避免代码重复;3)优先短路判断&&来避免if的加深;4)优先使用自动资源控制,少用try;5)STL的算法比自己做的循环好;6)使用多态来替代switch..case或者if。
21. 避免跨编译单元的初始化相关性。
保持(初始化)顺序:在不同编译单元中的名字空间级对象的初始化绝不能相互依赖,因为它们初始化的顺序是未定义的。否则,当你做很小的变更时,会引起不可思议的崩溃,或严重的不可移植性(即使是同一编译器的新的发布)。
22. 最小化定义性依赖,避免循环依赖。
不要过度依赖:当一个前向声明就可以满足要求时,不要去#include一个定义。
不要相互依赖:循环依赖性发生在两个直接或间接相互依赖的时候。一个模块就是一个发布的内聚单元;那些相互依赖的模块并非真的是单独的模块,而是粘在一起组成的一个更大的模块,一个更大的发布单元。它是大型工程的祸根。
当:1)需要知道对象大小;和2)需要命名或者调用成员时候,才需要某个类的完整定义。
但是最好遵循依赖倒置原则:高层不要依赖于底层,二者都应该依赖于抽象。
23. 让头文件自给自足。
各司其职:确保你写的每个头文件都能够编译独立,为此需要包含其内容所依赖的任何头文件(不要包含无用头文件)。
24. 总是写内部#include防护。决不要写外部#include防护。
穿上头文件的保护装:通过对所有的头文件使用具有唯一名字的#include防护,避免无故的多重包含。
函数和操作符
25. 合理地对待通过传值,传(智能)指针或传引用的参数。
合理地确定参数:区分输入,输出和输入/输出参数,区分值参数和引用参数。合理地对待它们。
1)通过值传递:原始类型(char、int)和复制开销比较低的值类型(point,complex<float>)等;2)使用const&对其他类型传递;3)指针和引用的使用:如果参数可选,用指针;否则若是必须的,用引用。
26. 保留被重载操作符的自然语义。
拒绝意外情况:只在适当理由时重载操作符,且要保留自然语义;如果那样做很困难的话,你可能误用操作符重载了。
27. 优先使用算数和赋值操作符的规范形式。
如果有a+b的话,也应有a+=b:在定义二元操作符时,也要提供它们对应的赋值版本,而且要使重复最小,效率最佳。
a@=b和a=a@b有同样的语义,但是前者更高效。为了避免代码重复,使用@=(成员函数)来实现@(非成员)版本。
28. 优先使用++和--操作符的标准形式。优先调用前缀形式。
如果有++c的话,也应该有c++:由于递增和递减操作符都有前缀和后缀形式,而且语义也稍有不同,这样就显得它们有些棘手。自定义的operator++和operator--应与内置操作符行为一致。如果你不需要原始值的话,优先调用前缀版本。
前缀形式返回新值,后缀形式返回的原值,所以后缀形式多创建了一个对象来保存原值。
T& operator++(){ //前缀
//…; Return *this; } const T operator++(int){//后缀
T temp(*this); ++(*this); Return temp; }
29. 考虑用重载来避免隐式类型转换(通常这个类型转换都会导致新的临时对象的产生)。
如无必要勿增对象(奥卡姆剃刀原理,KISS):隐式类型转换提供了语法上的便利,但是当创建临时对象的工作没有必要且适于优化时,你可以提供重载函数,其签名与常见的参数类型精确地匹配,而且不会引起转换动作。
30. 避免去重载&&,||,或,(逗号)。
贤人知道适可而止:编译器会特殊对待内置版本的&&,||和,(逗号)。如果要重载它们,它们就成了普通函数,有非常不同的语义,而且这是引入微妙bug和脆性的一个“可靠的”途径。内置版本:从左到右求值;&&和||是短路求值。
如果重新定义,他们的参数会被处理为:对所有参数求值(a&&b);参数求值顺序不确定。所以不要自定义他们。
31. 不要编写依赖于函数实参评估顺序的代码。
保持(求值)顺序:一个函数的实参的评估顺序没有被指定,所以不要依赖于一个特定的顺序。
类设计和继承
32. 搞清楚你正在编写的类的种类。 了解你自己:有许多不同种类的类。了解你正在编写的类的种类。
33. 优先使用最小型的类,而不是大类。
分而治之:小型类更容易编写,正确,测试和使用。在多数情况下小型类更可能被复用,内聚性更高。优先使用这些包含了简单概念的小型类,而不是那些试图实现很多或者是复杂的概念的大型类。
34. 优先使用组合,而不是继承。(继承的耦合程度仅次于friend )
避免承受继承的负担:继承是仅次于友元的第二紧密的耦合关系。紧耦合要尽可能地避免它。因此,优先使用组合,而不是继承,除非你知道后者真正有益于你的设计。
组合相对的优点:1)对类的依赖小,2)运行期可配置的灵活性;3)编译时隔离;4)适用性:有一些类不能作为基类,但是几乎所有类都可以当成员;5)继承会导致更多的名字隐藏等问题。
公有继承的使用情况:1)使用或者重写虚拟函数;2)访问保护成员;3)关注对象的构造顺序;4)EBO;5)NVI。
35. 避免从那些没有被设计用来当作基类的类派生。
一些人并不想要孩子:那些被用来独立使用的类与基类相比,它们执行着不同的计划(参见Item32)。把独立的类当作基类是一种严重的设计错误,我们应该避免它。要增添行为,优先增加非成员函数,而不是成员函数(参见Item44)。要增加状态,优先使用组合,而不是继承(参见Item34)。避免从具体基类继承。
36. 优先使用抽象的接口。
热爱抽象艺术:抽象接口可以帮你集中于得到一个正确的抽象概念,而不用把它和实现或状态管理细节混在一起。优先设计这样的层次结构,它实现了对抽象概念建模的抽象接口。
遵循DIP的优点:1)不稳定部分(实现)依赖于稳定部分(抽象),更稳定;2)灵活,扩展性;3)模块化。
37. 公有继承就是具有可替换性。继承不是重用,但可以被重用。
Know what:公有继承(is-a关系)可以让一个指向基类的指针或引用去实际指向某个派生类的一个对象,而且既不会破坏代码的正确性也不需要变更既有代码。Know why:不要通过公有继承来重用代码(也就是基类中存在的代码);公有继承是为了被重用(通过那些已经多态地使用了基类对象的既有代码)。
根据Liskov替换原则(LSP):父类的方法都要在子类中实现或者重写,不允许子类出现父类所没有定义的方法。实际上就是说:在使用中,基类可以完全代替子类;所以,如果代码中有了down cast的话,基本上就是没有满足这个原则。
38. 使用安全的改写(overriding)。
在改写一个虚拟函数时,要保持可替换性;要保持基类中函数的前置和后置条件。不要变更虚拟函数参数的默认值。显式地把改写的函数重新声明为virtual(清晰)。谨防隐藏基类中的重载函数。
Class Base{virtual foo(int); virtual foo(int, int); foo(double);}; class Der: public Base{ virtual foo(int){…};}
Der中的foo会把Base中的所有foo隐藏。
39. 考虑让虚拟函数非公有,让公有函数非虚拟。
在基类中变更(特别是在程序库和框架中)的代价是非常高的:让公有函数非虚拟。让虚拟函数私有化或者,如果派生类需要有调用基类版本的能力的话,则为保护。(注意这个建议对析构函数不适用;参见Item50)。
即NVI惯用法,它的优点:1)接口和实现细节(虚拟函数作为钩子)都定义了;2)基类有控制权;3)可变化、扩展
40. 避免提供隐式的转换。
并非所有的变更都是改进:隐式的转换经常是害大于利。在提供隐式的转换前,重新考虑一下你定义的类型,并且优先依赖于显式的转换(explicit构造函数和命名转换函数)。
隐式转换构造函数(单参数,非explicit)与重载机制配合的不好,经常会出现临时对象。所以,最好用explicit关键字构造函数;并使用as_lpct()这样的命名函数作为类型转换函数,而不是operator LPCTSTR()这样的类型转换函数。
41. 让数据成员私有化,除非是在一些更小的聚合体中。(类似于C风格的结构体)
让数据成员私有化。只有在那些聚合了一堆值但不需要封装或提供操作的简单的C风格结构体类型的情况下,才可以把所有数据成员声明为公有的。避免把公有和非公有的数据混合在一起,这往往意味着一个混乱的设计。
42. 不要公开内部数据。
避免返回类的内部数据的句柄(与公开数据成员一样),这样用户就不能不受控制地修改对象所拥有的状态。
Const是浅的(shadow),它控制的指针指向的数据是不会被const的;所以返回const*的函数还是公开了内部的数据。
43. 明智地使用Pimpl。.
克服语言的分离欲望:C++可以让私有成员不可访问,但并不是不可见。考虑使用Pimpl惯用法让私有成员真正不可见,从而实现编译器防火墙和提高信息隐藏。(参见Item11盒Item41)
44. 优先编写非成员非友元的函数。
避免成员的耗费:只要可能,就优先让函数既非成员又非友元;非成员非友元提高了封装性。
决定函数是否为成员:操作符= () -> []必为成员;成员需要一个与左参不同的类型(>>或<<),或需要对左参做类型转换,或能够用类的公用接口单独实现,则做非成员;如果必须有虚拟要求,则做成成员(NVI);其他情况,都做成员。
45. 总是提供配对的new和delete。
每个重载了void* operator new(parms)的类都必须同时提供一个对应的重载void operator delete(void*, parms),这里的parms是一个额外的形参类型列表(第一个总是std::size_t)。数组形式的new[]和delete[]也是一样。
placement new不需要对应的delete,因为它实际上并没有真的分配内存。
46. 如果你提供了任何在类中声明的new,就要提供所有标准形式(plain,placement和nothrow)。
不要隐藏标准形式的new:如果类定义了operator new的任何一种重载形式,就应该提供plain,placement和non-throwing这三种形式的operator new的重载。你如果不提供的话,其他的几个都会被隐藏,而且用户也不可用。
构造,析构和拷贝
47. 以相同的顺序初始化成员变量。
成员变量总是以它们在类定义中被声明的顺序来初始化的;它们在构造函数初始化列表中列出的顺序会被忽略。确保构造函数代码不会胡乱指定一个不同的顺序;这样的目的是为了确保销毁成员的顺序是唯一的而不受客户代码的影响。
48. 优先使用初始化,而不是在构造函数中赋值。
在构造函数中,用初始化替换赋值来设定成员变量,防止不必要的运行时工作。
49. 避免在构造函数和析构函数中调用虚拟函数。
虚拟函数只有在具有虚拟化行为时才是虚拟的:在构造和析构函数中,它们却不是的。甚至,在构造或析构函数中,对未实现的纯虚函数的直接或间接调用都会导致未定义行为。如果你的设计想让虚拟函数从一个基类的构造或析构函数分派到一个派生类中去,你就需要其它的技术了,例如后置构造函数(即:构造完成之后,在调用init()函数)。
50. 让基类的析构函数public和virtual,或者它protected和nonvirtual。
释放还是不释放,这是一个问题:如果要允许从一个指向基类Base的指针来释放内存,那Base的析构函数就必须是public和virtual的。否则,它就应该是protected和nonvirtual的。前提是:使用纯虚拟基类,不要带数据。
编译器生成的析构函数是公有+非虚拟的,所以必须要改变它。
51. 析构,释放单元,和互换操作决不能失败。
它们所尝试的每件事情都应该成功:绝不要从析构函数,资源释放函数(例如:operator delete)或交换(swap)函数中报告错误。特别地,在使用C++标准库时,其析构函数可能抛出一个异常的类型会被直接了当地禁止掉。
这些函数绝对不能失败,因为他们是事务处理中两个关键操作所必须的:提交和撤销。如果连撤销都不能成功……
C++标准:在栈展开期间析构函数异常,将调用terminate,所以不能让析构函数抛出异常。
52. 拷贝和摧毁要一致。
怎么创建怎么清除:如果定义了拷贝构造,拷贝赋值或析构函数中的任意一个,你就需要全部定义这三个函数。
53. 显式地允许或禁止拷贝动作。
有意识地拷贝:明确地选择是使用编译器生成的拷贝构造函数和赋值操作符,还是自己编写,或者是显式地禁止它们。
当然,禁止拷贝构造和拷贝复制意味着他们不能被放入标准STL中的容器。
54. 避免切片现象。在基类中考虑用克隆来替换拷贝。
切片的面包是很好;但切片的对象却不怎么样:对象切片现象是自动的,看不见的,而且可能为令人惊讶的中断带来令人惊奇的多态性的设计。在基类中,可以考虑禁止拷贝构造函数和拷贝赋值操作符,如果用户需要完成多态的(完全的,深的)拷贝的话,可以提供一个虚拟的Clone成员函数。
55. 优先使用赋值的规范形式。
在实现operator=的时候,优先使用规范形式 – 具有特定签名形式且non virtual。并且提供强力异常安全保证。
拷贝构造函数的典型定义应: 传统形式T& operator=(const T&); 或者 更方便的优化形式 T& operator=(T); 返回*this.
56. 只要可行,就应该提供一个不会失败的交换操作(而且要正确)。
Swap既可以无关痛痒,也可以举足轻重:可以考虑提供一个swap函数来有效和准确无误地交换此对象与另一个的内部数据。这样的函数可以很容易地实现一些惯用法,从“通过平滑地移动对象很容易地实现赋值”到“通过提供一个受保护的委托函数来提供强有力的错误安全(容错)的调用代码”。
在这个operator=的实现中,多了一个临时对象,但是提高了安全性。标准做法:
T& operator=(const T& rhs){
T temp(rhs); swap(temp); return *this; } T& operator=( T rhs){
swap(rhs); return *this; }
名字空间和模块
57. 把类型和其非成员函数接口放在同一个名字空间中。
非成员函数也是函数:为了能被正确地调用,被用作一个类类型X的接口的一部分的非成员函数(特别是操作符和助手函数)必须定义在X所在的名字空间中。
公有成员函数和非成员函数都是类的公有接口的一部分。接口的原则是:对于一个类而言,所有在同一个名字空间内提及X和随X一起提供的函数(包括非成员),在逻辑上都是X的一部分,他们共同形成X的接口。
C++被明确的设计为实施接口原则,参数依赖查找(ADL,也叫Koenig查找),要确保:X的对象a能够像使用成员函数一样方便的使用非成员函数接口(比如cout << x)。对于那些以X为参数的、由X的定义提供的非成员函数来说,ADL可以确保他们成员X的一员,就如同X的直接函数一样。
这个规则主要是针对这类接口的方便使用:显然是X的接口一部分,但又是非成员函数。
58. 把类型和函数放到不同的名字空间中,除非你明确地想让他们一起工作。
这样有助于防止名称查找意外:通过把它们放在各自的名字空间中(连同与它们直接相关的非成员函数;参见:Item57),把类型从出于无心的“实参依赖查找”(ADL,通常所说的Koenig查找)中脱离出来,并且鼓励有意的ADL。
59. 不要在头文件或#include指令前放置名字空间using指令。
名字空间using指令是为了方便,提供无二义性的名字管理,而不是让你与其它人相冲突:不要在任何#include指令前写using声明或指令。 推论:不要在头文件中写名字空间级的using声明或指令;作为替换,应该显式地用名字空间来限定所有名称。(第二条规则是从第一条得出的,因为头文件绝不可能知道其后有什么其他头文件的#include指令出现)
简言之:可以而且应该在实现文件中#include之后自由的使用名字空间级的using声明和指令。
60. 避免在不同模块中分配和释放内存。
把事情后推到你发现它们的地方:在一个模块中分配内存,而在另一个不同的模块中释放内存,这样做会因为在那些模块间建立了一个微妙的长距离相关性而使你的程序变得很脆弱。它们必须使用同一个编译器版本,同样的选项flag(特别是debug和NDEBUG)和同样的标准库实现来编译,在实践中,在释放内存时分配内存的模块最好仍在内存中。
因为,库的开发者希望提高库的效率和质量,在下一个版本的内存分配中使用的数据结构和算法会有很大变化。
61. 不要在头文件定义具有链接的实体。
重复会导致膨胀:包括名字空间级的变量或函数在内的具有链接的实体会被分配内存。在头文件中定义如此的实体会引起连接错误或内存浪费。把所有具有链接的实体都放到实现文件中去。
在头文件中声明变量或者函数,用extern(extern对函数声明可有可无);实际的定义放到另外的.cpp里。
所谓具有链接的名字,指它可能与另一个作用域中某个声明所引入的名字表示同一个实体(对象、引用、函数、类型、模板、名字空间或值)。内外链接区别:能否从另一编译单元中引用。无连接指名字所表示的实体不能从作用域之外引用。名字空间级的实体肯定具有内部或者外部链接;局部作用域声明的名字肯定没有链接。
62. 不要让异常跨模块边界传播。
不要把石头扔到邻居家的花园里去:现在还没有被广泛认可的关于C++异常处理的二进制标准。不要让异常在代码的两个地方传播,除非你控制着用于构建的编译器及其选项;否则,模块可能不会支持关于异常传播的兼容性实现。典型地,归结为:不要纵容异常跨模块/子系统边界传播。
异常的传播会根据操作系统、编译器,甚至编译选项而异,所以应该在模块的边界使用catch(…)来防止异常的外播:main函数、无法控制的回调函数、线程边界、模块的接口边界、析构函数(析构函数不能有异常)。
理想情况下,异常应该能够在模块内部顺畅的传播,在跨越模块边界时控制和转化,以供外界使用。
63. 在模块接口中充分使用可移植的类型。
涉及到(模块的)边界时要格外小心:不要让一个类型出现在模块的外部接口中,除非你能保证所有用户都能正确地理解这个类型。使用用户能够理解的最高级别的抽象。
抽象层次越低(File>string>char*),可移植性就越好,但是复杂性也越高。
模板和泛型
64. 明智地混合使用静态和动态多态。
比单纯的部分的总和更多:静态和动态多态是互补的。理解它们的权衡标准,在各自最好的情况下使用它们,并且混合使用它们来达到两全其美。
动态多态性:基于基类/派生关系的统一操作、静态类型检查、动态绑定和隔离编译(因为指针)、二进制接口(vtble)。
静态多态性:基于语法和语义接口的统一操作、静态类型检查、静态绑定(不是分别编译)、效率(被编译期花费了)。
65. 有意地和显式地定制模板。
有意图要优于偶然性,显式要优于隐式:在编写模板的时候,要有意识和正确地提供定制点,而且要清楚地说明它们。在使用模板的时候,要知道模板希望你如何定制它来为你的类型所使用,并恰当地定制模板。
66. 不要特化函数模板。
模板特化只有在它能正确地实行时,它才是有益的:在扩展其他某个人的函数模板(包括:std::swap)的时候,尽量避免去特化它;作为替换,我们可以写一个函数模板的重载函数,并把它放到这个重载函数所用于的类型所在的名字空间中去。(参见:Item57)在你编写自己的函数模板的时候,避免鼓励函数模板自身的直接特化。
67. 不要盲目地编写不通用的代码。
依赖于抽象,而不是细节:使用最最泛型化和抽象的方法来实现功能的一小块。
错误处理和异常
68. 使用断言(assert)来证明内部假设和不变量。
Be assertive!对一个模块的内部假设可以使用assert或等价物来说明(例如:调用者和被调用者由同一个人或团队维护),这个假设必须总为true,否则就代表着程序设计错误(例如:函数调用方发现违反了一个函数的前置条件)。(参见:Item70)要确保断言不会产生副作用。 assert.h, 用assert( I > 10 && “my message”)的形式可以输出错误信息。
69. 形成一个合理的错误处理策略,并且严格地遵守。
在设计前期开发一个实用,一致与合理的错误处理策略,比把它坚持下来。确保它包括:1)标识:哪种情况是错误; 2)严重性;3)检测:哪段代码负责检测错误;4)传播:在各个模块中报告和传播错误通知的机制;5)处理:什么代码负责对错误做些什么;6)报告:错误将如何被记录或通知用户。只在模块边界上改变错误处理机制。
70. 区别错误和无错误(errors and non-errors)。
违约就是一个错误:函数是一个作业单元。因此,函数失效应该被看成一个错误或者其它基于它们对函数的影响的东西。在一个函数f中,当且仅当违反了函数f的前置条件或阻止f满足它的被调用者的任何一个前置条件,完成f自身的任何一个后置条件,或者是重建f负责维护的任何一个不变量时,失效才是一个错误。
特别的在此处我们把内部程序设计错误除外,这是和使用断言相关的一个独立的范畴。
71. 设计和编写错误安全(容错)代码。
许诺,但不能惩罚:在每个函数中,提供最强的安全保证,并不惩罚不需要这种保证的调用代码。至少提供基本保证。
1)确保出错误后程序置于一个有效的状态。这就是基本的保证。注意不变量破坏invariant-destroying(包括不局限于泄漏)。 2)操作的最终状态不是初始状态(如果有错误发生,操作是可以回滚)就是指定的目标状态(如果没有错误发生,操作就是所承诺的),这是强保证。3)操作不可能失败。尽管对大多数函数来说,这是不可能的,但这是诸如交换函数、释放和析构函数所要求的。这是不失败保证。
72. 优先使用异常来报错。
抛出异常吧:优先使用异常来报错,而不是通过错误代码。当异常不能用(参见:Item62)和条件式不是错误的时候,可以使用状态码(例如:返回代码,errno)。当恢复操作不可能或不需要时,可以使用诸如优雅或不优雅的终止等方法。
异常优于错误码:1)异常不能忽略;2)异常自动传播,跨作用域,直到被处理为止;3)异常机制会把错误代码和功能代码分开,逻辑清晰;4)某些函数没有返回值,必须使用异常机制,比如构造函数、操作符等。
C++规范:如果构造函数抛出异常,说明构造对象失败了,该对象的生命周期没有开始过,所以也不必释放。
73. 以传值来抛出异常,以传引用来捕获异常。
适当地了解catch语句:以传值(非指针)方式来抛出异常并通过传引用(通常是const)来捕获异常。这是和异常语义极好地结合。当重新抛出同一异常时,优先使用throw;,而不是throw e;。
如果使用指针,就需要管理内存问题。尤其是,抛出指向stack中对象的指针是不可行的,会造成野指针问题。
74. 适当地报告,处理和解释错误。
在错误被探查到并被标识为错误时报告错误。在最近的一级可以正确处理错误的地方处理和解释各个错误。
75. 避免使用异常规范。
不要为你的函数编写异常规约,除非你不得已(因为其它你不能变更的代码已经介绍过它们了;参见:Exception)
STL:容器
76. 默认情况下使用vector。否则,选择一个适当的容器。
使用“正确(合适)的容器”是很重要的:如果你有一个很好的理由使用某个特定的容器,在你了解你所做的是对的情况下,你可以使用那个容器。So is using vector。
编程时正确、简单和清晰是最主要的。必要时才考虑效率。尽可能编写事务性的、强安全保障的代码。
Vector特征:空间开销小;存取速度快;容器内的相邻对象的内存也相邻;与C语言的内存布局兼容;随机;最快。
77. 用vector和string替代数组。
避免用C风格数组、指针运算和内存管理原语来实现数组抽象。使用vector和string不仅更容易,而且有助于写出更安全和伸缩性的软件来。
使用vector或者string替代c风格数组,原因:可以自动管理内存;接口丰富;与C内存兼容;效率不差;便于优化。
78. 用vector(和string::c_str)和非C++ API交换数据。
vector在转换过程中并没有丢失:vector和string::c_str是你与非C++ API之间通信的通道。但不要认为iterator就是指针;要得到一个由vector<T>::iterator iter引用的元素的地址,应该使用&*iter。
Vector的内存是连续的;string的内存并不保证连续,但是string::c_str返回的是一个连续的内存空间,C风格的。
79. 只把值和智能指针放到容器中。
把值对象存放在容器中:容器总是假定包含的是类似值的类型,包括值类型(直接存取),智能指针和迭代器(iterator)。
80. 优先使用push_back来扩充一个序列。
尽量使用push_back:如果你不需在意插入的位置,优先使用push_back来给一个序列增添一个元素。其他方法则可能是非常慢和不清晰的。
Push_back原理:按照指数级扩大容量,而不是固定增量;因此重新分配内存和复制的次数会越来越少。
81. 优先使用范围操作,少用单元素操作。Prefer range operations to single-element operations.
给序列容器添加元素时,优先使用范围操作(例如:带一对迭代器参数的insert形式),而不是一系列单元素形式的操作的调用。通常调用范围操作的代码易于编写和阅读,而且比显式循环更有效。(参见:Item84)
82. 使用公认的习惯用法来真正地收缩容量和删除元素。
为真正地体现容器的额外能力,可以使用“swap trick”。要真正从一个容器中清除元素,可以使用erase-remove惯用法。
STL:算法
83. 使用安全的(被验证过的)STL实现。
安全第一:使用安全的STL实现,即使它只适用于你的编译器平台中的一个;即使它只还在测试中的预发布版本.
84. 优先调用算法,而不是手写的循环。
明智地使用函数对象:对于每个简单的循环,手写的循环可能是最简单也是最有效的解决方法。但是用算法代替手写的循环可能更具表现性和可维护性,更不容易出错,而且也很有效。
当调用算法时,编写你自己的函数对象来封装你需要的逻辑。避免把参数邦定器和简单的函数对象夹杂在一起。(例如:bind2nd,plus),这往往会降低清晰性.考虑试试[Boost]Lambda程序库,它把编写函数对象的任务自动化了.
85. 使用正确的STL搜索算法。
合适的搜索可能就是STL了:这一点应用于在一个范围内搜索一个特定的值,或者定位所在的位置。要搜索一个未排序的范围,使用find/find_if或count/count_if。要搜索一个排序过的范围,使用lower_bound, upper_bound, equal_range, 或者 (很少) binary_search。(不管binary_search的通用名字,它通常都不是好的选择。)
86. 使用正确的STL排序算法。
排序方式应该恰到好处:理解各个排序算法,适用你所需要的代价最小的算法。
开销从低到高:partition, stable_partition, nth_element, partial_sort, sort, stable_sort.
87. 使谓词成为纯函数。
保持谓词的纯洁性:谓词就是返回true和fallse的函数对象。凭数学感觉,如果一个函数的结果只依赖于它的参数(而不依赖于其他的状态),那么它就是纯的(注意这里的“纯”和纯虚函数没有任何关系)。
88. 优先使用函数对象作为算法和比较器的参数,而不是函数。
对象比函数的适配性更好:对于算法,优先使用函数对象(重载了括号操作符的类)来做参数,而不是函数。关联容器的比较器则必须是函数对象。函数对象适配性好,而且违反直觉的是,它可以产生出比函数更快的代码。
89. 正确地编写函数对象。
成本要低,要可适配:设计拷贝代价低廉的函数对象。只要可能,可以通过从unary_fuction和binary_function派生来生成适应性强的函数对象。
类型安全
90. 避免使用类型转换,优先使用多态。
避免通过对象类型分支来定制行为。通过模板和虚拟函数机制,让类型自己来决定其自身的行为。
通过类型分支是用c++写c或者fortran代码的明显标志。
理想情况下,在程序中添加新特性的时候应该只需要添加新代码,而不需要修改原来代码(开闭原则OCP)。根据抽象来编写代码(依赖倒置原则DIP),在实现时为那些抽象添加各种实现。模板和虚函数为代码和抽象隔离了依赖。
91. 依赖于类型,而不是其表示。
不要去假设对象在内存中是如何表示的,因为它随编译器而不同。让类型自身来决定如何从内存中写入和读取其对象。
C++对类型在内存中的表示方式只有如下的规定:整数是二进制,负整数用二进制补码,普通旧式数据(POD)内存布局与C兼容(成员的顺序与声明顺序一致),int至少2byte。
所谓POD是指:没有虚函数和基类;算术类型、枚举、指针,以及旧式的struct和union。
92. 避免使用reinterpret_cast。
谎言是站不住脚的:不要试图用reinterpret_cast来迫使编译器把一种类型的对象的内存重新解释成一种不同类型的对象。这违背类型安全机制,不可移植,而且reinterpret_cast甚至不能保证转化的成功,也无法保证其他功能。
在某些不太相关的类型间作强制转换,应该是有void*来做中介,不要直接使用reinterpret_cast:
T1* p1 = …; void* p2 = p1; T2* p3 = static_cast<T2*>(p2);
93. 避免对指针使用static_cast。
不能static_cast来转换指向动态对象的指针:使用dynamic_cast、重构、重新设计都是一个安全的替换策略。
94. 避免去除const限定。
去除const限定往往会产生未定义的行为,即使这样做是合法的,它都是一类不良的程序设计风格。
选择const就不要回头,因为编译器可能会把const数据放到只读存储器(ROM)中,去除const会发生内存故障。
95. 不要使用C风格的强制转换。
C风格的强制转换依赖于不同上下文有着不同的(往往还是危险)语义,而这些都隐藏在一个单一的语义后面。用C++风格的强制转换来代替C风格的,这样可以防止意外的错误、清晰、避免了无故增加reinterpret_cast等。
96. 不要对非POD(Plain Old Data)进行memcpy或memcmp操作。
不要使用memcpy和memcmp来拷贝和比较任何较对象,除非他的对象内存布局就是原始布局,没有被增加东西。
Memcpy和memcmp会扰乱类型系统,尤其是面向对象部分。因为POD内存是原始内存,而有多态语义的OO对象却不是:有vptr、allignment、handle或者可以造成dangling的指针(memcpy后会造成两个指针指向同一个内存)。
C++的要点之一是信息隐藏:对象隐藏了数据,并且通过构造函数和赋值函数来把对象的内存进行精确的复制。
97. 不要使用联合来重新解释表示法(实体)。
联合可以被滥用成“没有转换的转换”,写入一个成员而读取另一个成员。这比reinterpret_cast更阴险和难以预测。
98. 不要使用可变参数(…)。
省略号(…)会导致崩溃:它从C中沿袭下来的危险。避免使用可变参数,使用更高级别C++构造和程序库来替代它。
因为:缺乏类型安全;调用者和被调者紧密耦合,需要手动协调,比如写%d;类对象的行为未定义;参数个数未知,所以仍然需要一种交流方式来告知参数个数。
99. 不要使用无效对象。不要使用不安全的函数。
不要使用过期药品:无效对象和历史的但不安全的函数会严重影响程序的“健康”。
失效对象有三种:已销毁对象,包括超出作用域的自动对象和已删除的堆对象;语义失效对象,比如dangling指针;从未有效的对象,比如通过伪造指针(reinterpret_cast)制作的指针,或数组越界的产物。
不要尝试手动调用析构函数obj.~T(),然后再在那里进行placement new,无异于玩火。
100. 不要多态地处理数组。
多态地处理数组是一种严重的类型错误,而编译器可能不会察觉。不要掉到这个陷阱中去。
其根本原因是基类和子类对象的sizeof基本上都是不一样大的,而数组需要使用p+n*sizeof(obj)来定位元素的位置。
第5条 一个实体应该只有一个紧凑的职责
pic1
第9调 避免进行不成熟的劣化
pic2
第14条 宁要编译时和连接时错误,也不要运行时错误
pic3
第17条 避免使用魔数
pic4
第27条 优先使用算术操作符和赋值操作符的标准形式
pic5
第28条 优先使用++和--的标准形式。优先调用前缀形式
pic6
第29条 要避免提供隐式转换
pic7
第44条 优先编写非成员非友元函数
pic8
第46条 如果提供专门的new,应该提供所有标准形式(普通,就地和不抛出)
pic9,10
第49条 避免在构造或析构函数中调用虚函数
第50条 将基类析构函数设为公用且虚拟的,或者保护且非虚拟的
pic11
第53条 显式的启用或禁止复制
pic12
第61条 不要在头文件中定义具有链接的实体
pic13
第62条 理智地结合静态多态性和动态多态性
pic14
pic15
第67条不要无意的编写不同用的代码
pic16
第73条 通过值抛出,通用引用捕获
pic17
第82条 使用公认的惯用法真正的压缩容量,真正的删除元素
pic18
参考:
①:http://blog.csdn.net/aheroofeast/article/details/6525861