《Effective C++》

  1. 尽可能用const和inline而不用#defineios

    尽可能用编译器而不用预处理。程序员

  2. 尽可能用而不用<stdio.h>算法

    scanf和printf颇有用,但不是类型安全的,并且没有扩展性。
    on the other hand,①有些iostream的操做实现起来要比相应的C stream效率要低,但不是对全部的iostream而言,而是一些特殊的实现;②在标准化的过程当中,iostream库在底层作了不少修改,因此对那些要求最大可移植性的应用程序来讲,会发现不一样的厂商遵循标准的程度也不一样;③iostream库的类有构造函数而<stdio.h>里的函数没有,在某些涉及到静态对象初始化顺序的时候,若是能够确认不会带来隐患,用标准C库更加简单实用。编程

  3. 尽可能用new和delete而不用malloc和free数组

    malloc和free(及其变体)产生问题的缘由是由于它们太简单:它们不知道构造函数和析构函数。缓存

  4. 尽可能使用C++风格的注释安全

  5. 对应的new和delete要采用相同的形式数据结构

    用new的时候会发生两件事:首先,内存会被分配,而后,为分配的内存调用一个或多个构造函数;用delete的时候,也有两件事发生,首先,为将被释放的内存调用一个或多个析构函数,而后释放内存。对于delete来讲有这样一个重要的问题:内存中有多少个对象要被删除?答案决定了有多少个析构函数将被调用。
    这个问题简单说来就是,要被删除的指针指向的是单个对象呢,仍是对象数组?这只有你来告诉delete。若是你在delete时没有用括号,delete就会认为指向的是单个对象;不然,它就会认为指向的是一个数组:
    string *stringPtr1 = new string;
    string *stringPtr2 = new string[100];
    ……
    delete stringPtr1;
    delete[] stringPtr2;
    若是在stringPtr1前加了[]和stringPtr2前没有加[],结果都是不可预测的。解决这类问题的规则:若是你调用new时用了[],那么调用delete时也要用[],若是调用new时没有用[],那么调用delete时也不要用[].函数

  6. 析构函数里对指针成员调用delete性能

    若是在析构函数里没有删除指针,它不会表现出很明显的外部症状。相反,它可能只是表现为一点微小的内存泄露,而且不断增加,最后吞噬了你的内存空间,致使程序夭折。
    删除空指针是安全的(由于它什么也没作)。
    固然对本条款的使用也不要绝对,好比,你不会用delete去删除一个没有用new来初始化的指针,并且,就像用智能指针对象时不用劳你来删除同样,你也永远不会去删除一个传递给你的指针。换句话说,除非类成员最初用了new,不然是不用在析构函数里用delete的。

  7. 预先准备好内存不够的状况

  8. 写operator new和operator delete时要遵循常规

    要有正确的返回值;可用内存不够时要调用出错处理函数;处理好0字节内存请求的状况;还要避免不当心隐藏了标准形式的new。
    处理零字节请求的技巧在于把它做为请求一个字节来处理。

  9. 避免隐藏标准形式的new

    在类里定义了一个称为“operator new”的函数后,会不经意地阻止了对标准new 的访问。条款50 解释了为何会这样,这里咱们更关心的是如何想个办法避免这个问题。一个办法是在类里写一个支持标准 new 调用方式的operator new,它和标准new 作一样的事。这能够用一个高效的内联函数来封装实现。另外一种方法是为每个增长到 operator new 的参数提供缺省值(见条款24)。

  10. 若是写了operator new就要同时写operator delete

    为何有必要写本身的operator new和operator delete?为了效率。缺省的operator new和operator delete具备很是好的通用性,它的这种灵活性也使得在某种特定的场合下,能够进一步改善它的性能。尤为在那些须要动态的分配大量的但很小的对象的应用程序里,状况更是如此。
    若是写了一个本身的内存分配程序,就要同时写一个释放程序。

  11. 为须要动态分配内存的类声明一个拷贝构造函数和一个赋值操做符

    用delete去删除一个已经被删除的指针,其结果是不可预测的。
    只要类里有指针时,就要写本身版本的拷贝构造函数和赋值操做符函数。在这些函数里,你能够拷贝那些被指向的数据结构,从而使每一个对象都有本身的拷贝;或者你能够采用某种引用计数机制(见条款 M29)去跟踪当前有多少个对象指向某个数据结构。
    对于有些类,当实现拷贝构造函数和赋值操做符很是麻烦的时候,特别是能够确信程序中不会作拷贝和赋值操做的时候,去实现它们就会相对来讲有点得不偿失。照本条款的建议去作:能够只声明这些函数(声明为private 成员)而不去定义(实现)它们。这就防止了会有人去调用它们,也防止了编译器去生成它们。关于这个俏皮的小技巧的细节,参见条款27。

  12. 尽可能使用初始化而不要在构造函数里赋值

    从纯实际应用的角度来看,有些状况下必须用初始化。特别是 const 和引用数据成员只能用初始化,不能被赋值。
    对有基类的对象来讲,基类的成员初始化和构造函数体的执行发生在派生类的成员初始化和构造函数体的执行以前。
    经过成员初始化列表来进行初始化老是合法的,效率也毫不低于在构造函数体里赋值,它只会更高效。另外,它简化了对类的维护(见条款M32),由于若是一个数据成员之后被修改为了必须使用成员初始化列表的某种数据类型,那么,什么都不用改变。

  13. 初始化列表中成员列出的顺序和它们在类中声明的顺序相同

    类成员是按照它们在类里被声明的顺序进行初始化的,和它们在成员初始化列表中列出的顺序没一点关系。

  14. 肯定基类有虚析构函数

    当经过基类的指针去删除派生类的对象,而基类又没有虚析构函数时,结果将是不可肯定的。
    虚函数的目的是让派生类去定制本身的行为(见条款36),因此几乎全部的基类都包含虚函数。若是某个类不包含虚函数,那通常是表示它将不做为一个基类来使用。当一个类不许备做为基类使用时,使析构函数为虚通常是个坏主意。
    实现虚函数须要对象附带一些额外信息,以使对象在运行时能够肯定该调用哪一个虚函数。对大多数编译器来讲,这个额外信息的具体形式是一个称为vptr(虚函数表指针)的指针。vptr 指向的是一个称为vtbl(虚函数表)的函数指针数组。每一个有虚函数的类都附带有一个vtbl。当对一个对象的某个虚函数进行请求调用时,实际被调用的函数是根据指向vtbl 的vptr 在vtbl 里找到相应的函数指针来肯定的。
    若是声明虚析构函数为inline,将会避免调用它们时产生的开销,但编译器仍是必然会在什么地方产生一个此函数的拷贝。

  15. 让operator=返回*this的引用

    C++程序员常常犯的一个错误是让operator=返回void,这好象没什么不合理的,但它妨碍了连续(链式)赋值操做,因此不要这样作。
    另外一个常犯的错误是让 operator=返回一个const 对象的引用。
    当定义本身的赋值运算符时,必须返回赋值运算符左边参数的引用,*this。若是不这样作,就会致使不能连续赋值,或致使调用时的隐式类型转换不能进行,或两种状况同时发生。

  16. 在operator=中对全部数据成员赋值

    只要想对赋值过程的某一个部分进行控制,就必须负责作赋值过程当中全部的事。
    派生类的赋值运算符也必须处理它的基类成员的赋值。

  17. 在operator=中检查给本身赋值的状况

    在赋值运算符中要特别注意可能出现别名的状况,其理由基于两点。其中之一是效率。若是能够在赋值运算符函数体的首部检测到是给本身赋值,就能够当即返回,从而能够节省大量的工做,不然必须去实现整个赋值操做。另外一个更重要的缘由是保证正确性。一个赋值运算符必须首先释放掉一个对象的资源(去掉旧值),而后根据新值分配新的资源。在本身给本身赋值的状况下,释放旧的资源将是灾难性的,由于在分配新的资源时会须要旧的资源。

  18. 争取使类的接口完整而且最小

    归纳起来就是说,无故地在接口里增长函数不是没有代价的,因此在增长一个新函数时要仔细考虑:它所带来的方便性(只有在接口完整的前提下才应该考虑增长一个新函数以提供方便性)是否超过它所带来的额外代价,如复杂性,可读性,可维护性和编译时间等。

  19. 分清成员函数、非成员函数和友元函数

    成员函数和非成员函数最大的区别在于成员函数能够是虚拟的而非成员函数不行。因此,若是有个函数必须进行动态绑定(见条款38),就要采用虚拟函数,而虚拟函数一定是某个类的成员函数。
    explicit 构造函数不能用于隐式转换。
    假设 f 是想正确声明的函数,C 是和它相关的类:虚函数必须是成员函数。若是 f 必须是虚函数,就让它成为C 的成员函数。operator>>和operator<<毫不能是成员函数。若是f 是operator>>或operator<<,让f 成为非成员函数。若是f 还须要访问C 的非公有成员,让f 成为C 的友元函数。只有非成员函数对最左边的参数进行类型转换。若是 f 须要对最左边的参数进行类型转换,让f 成为非成员函数。若是f 还须要访问C 的非公有成员,让f 成为C 的友元函数。其它状况下都声明为成员函数。若是以上状况都不是,让 f 成为C 的成员函数。

  20. 避免public接口出现数据成员

    在public 接口里放上数据成员无异于自找麻烦,因此要把数据成员安全地隐藏在与功能分离的高墙后。

  21. 尽量使用const

    char p = "Hello"; // 非const 指针,非const 数据
    const char *p = "Hello"; // 非const 指针, const 数据
    char * const p = "Hello"; // const 指针,非const 数据
    const char * const p = "Hello"; // const 指针,const 数据
    语法并不是看起来那么变化无穷。通常来讲,你能够在头脑里画一条垂直线穿过指针声明中的星号(
    )位置,若是const 出如今线的左边,指针指向的数据为常量;若是const 出如今线的右边,指针自己为常量;若是const 在线的两边都出现,两者都是常量。
    一个好的用户自定义类型的特征是,它会避免那种没道理的与固定类型不兼容的行为。

  22. 尽可能用传引用而不用传值

    C 语言中,什么都是经过传值来实现的,C++继承了这一传统并将它做为默认方式。除非明确指定,函数的形参老是经过“实参的拷贝”来初始化的,函数的调用者获得的也是函数返回值的拷贝。
    传递引用是个很好的作法,但它会致使自身的复杂性,最大的一个问题就是别名问题,这在条款17 进行了讨论。另外,更重要的是,有时不能用引用来传递对象,参见条款23。最后要说的是,引用几乎都是经过指针来实现的,因此经过引用传递对象其实是传递指针。所以,若是是一个很小的对象——例如int— — 传值实际上会比传引用更高效。

  23. 必须返回一个对象时不要试图返回一个引用

    引用只是一个名字,一个其它某个已经存在的对象的名字。不管什么时候看到一个引用的声明,就要当即问本身:它的另外一个名字是什么呢?由于它必然还有另一个什么名字(见条款M1)。

  24. 在函数重载和设定参数缺省值间慎重选择

    答案取决于另外两个问题。第一,确实有那么一个值能够做为缺省吗?第二,要用到多少种算法?通常来讲,若是能够选择一个合适的缺省值而且只是用到一种算法,就使用缺省参数(参见条款38)。不然,就使用函数重载。

  25. 避免对指针和数字类型重载

  26. 小心潜在的二义性

    C++认为潜在的二义性不是一种错误。多继承(见条款43)充满了潜在二义性的可能。最常发生的一种状况是当一个派生类从多个基类继承了相同的成员名时。

  27. 若是不想使用隐式生成的函数就要显式的禁止它

    方法是声明这个函数(operator=),并使之为private。显式地声明一个成员函数,就防止了编译器去自动生成它的版本;使函数为private,就防止了别人去调用它。
    可是,这个方法还不是很安全,成员函数和友元函数仍是能够调用私有函数,除非——若是你够聪明的话——不去定义(实现)这个函数。这样,当无心间调用了这个函数时,程序在连接时就会报错。
    这适用于条款45 所介绍的每个编译器自动生成的函数。实际应用中,你会发现赋值和拷贝构造函数具备行为上的类似性(见条款11 和16),这意味着几乎任什么时候候当你想禁止它们其中的一个时,就也要禁止另一个。

  28. 划分全局名字空间

    名字空间带来的最大的好处之一在于:潜在的二义不会形成错误(参见条款26)。因此,从多个不一样的名字空间引入同一个符号名不会形成冲突(假如确实真的从不使用这个符号的话)。

  29. 避免返回内部数据的句柄

    对于 const 成员函数来讲,返回句柄是不明智的,由于它会破坏数据抽象。对于非const 成员函数来讲,返回句柄会带来麻烦,特别是涉及到临时对象时。句柄就象指针同样,能够是悬浮(dangle)的。因此必定要象避免悬浮的指针那样,尽可能避免悬浮的句柄。

  30. 避免这样的成员函数:其返回值是指向成员的非const 指针或引用,但成员的访问级比这个函数要低

  31. 千万不要返回局部对象的引用,也不要返回函数内部用new 初始化的指针的引用

    先看第一种状况:返回一个局部对象的引用。它的问题在于,局部对象 ----- 顾名思义 ---- 仅仅是局部的。也就是说,局部对象是在被定义时建立,在离开生命空间时被销毁的。所谓生命空间,是指它们所在的函数体。当函数返回时,程序的控制离开了这个空间,因此函数内部全部的局部对象被自动销毁。所以,若是返回局部对象的引用,那个局部对象其实已经在函数调用者使用它以前被销毁了。当想提升程序的效率而使函数的结果经过引用而不是值返回时,这个问题就会出现。
    写一个返回废弃指针的函数无异于坐等内存泄漏的来临。

  32. 尽量的推迟变量的定义

    推迟变量定义能够提升程序的效率,加强程序的条理性,还能够减小对变量含义的注释。

  33. 明智的使用内联

    在一台内存有限的计算机里,过度地使用内联所产生的程序会由于有太大的体积而致使可用空间不够。即便可使用虚拟内存,内联形成的代码膨胀也可能会致使不合理的页面调度行为(系统颠簸),这将使你的程序运行慢得象在爬。过多的内联还会下降指令高速缓存的命中率,从而使取指令的速度下降,由于从主存取指令固然比从缓存要慢。
    另外一方面,若是内联函数体很是短,编译器为这个函数体生成的代码就会真的比为函数调用生成的代码要小许多。若是是这种状况,内联这个函数将会确实带来更小的目标代码和更高的缓存命中率!
    一个给定的内联函数是否真的被内联取决于所用的编译器的具体实现。幸运的是,大多数编译器均可以设置诊断级,当声明为内联的函数实际上没有被内联时,编译器就会为你发出警告信息。

  34. 将文件间的编译依赖性降到最低

    C++的类定义中不只包含接口规范,还有很多实现细节。
    若是可使用对象的引用和指针,就要避免使用对象自己。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则须要类型定义的参与。
    尽量使用类的声明,而不使用类的定义。由于在声明一个函数时,若是用到某个类,是绝对不须要这个类的定义的,即便函数是经过传值来传递和返回这个类。
    不要在头文件中再(经过#include 指令)包含其它头文件,除非缺乏了它们就不能编译。相反,要一个一个地声明所须要的类,让使用这个头文件的用户本身(经过#include 指令)去包含其它的头文件,以使用户代码最终得以经过编译。

  35. 使公有继承体现“是一个”的含义

    C++面向对象编程中一条重要的规则是:公有继承意味着 "是一个" 。

  36. 区分接口继承和实现继承

    纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中从新声明,并且它们在抽象类中每每没有定义。
    定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口。声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现。声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。

  37. 毫不要从新定义继承而来的非虚函数

    若是写类 D 时从新定义了从类B 继承而来的非虚函数mf,D 的对象就可能表现出精神分裂症般的异常行为。也就是说,D 的对象在mf 被调用时,行为有可能象B,也有可能象D,决定因素和对象自己没有一点关系,而是取决于指向它的指针所声明的类型。引用也会和指针同样表现出这样的异常行为。
    任何条件下都要禁止从新定义继承而来的非虚函数。

38.毫不要从新定义继承而来的缺省参数值

虚函数是动态绑定而缺省参数值是静态绑定的。这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。
若是缺省参数值被动态绑定,编译器就必须想办法为虚函数在运行时肯定合适的缺省值,这将比如今采用的在编译阶段肯定缺省值的机制更慢更复杂。作出这种选择是想求得速度上的提升和实现上的简便,因此你们如今才能感觉获得程序运行的高效;固然,若是忽视了本条款的建议,就会带来混乱。

39.避免“向下转换”继承层次

这种类型的转换 ---- 从一个基类指针到一个派生类指针 ---- 被称为 "向下转换",由于它向下转换了继承的层次结构。在刚看到的例子中,向下转换碰巧能够工做;但正以下面即将看到的,它将给从此的维护人员带来恶梦。
"向下转换" 能够经过几种方法来消除。最好的方法是将这种转换用虚函数调用来代替,同时,它可能对有些类不适用,因此要使这些类的每一个虚函数成为一个空操做。第二个方法是增强类型约束,使得指针的声明类型和你所知道的真的指针类型之间没有出入。为了消除向下转换,不管费多大工夫都是值得的,由于向下转换难看、容易致使错误,并且使得代码难于理解、升级和维护(参见条款M32)。

40.经过分层来体现“有一个”或“用···来实现”

使某个类的对象成为另外一个类的数据成员,从而实现将一个类构筑在另外一个类之上,这一过程称为 "分层"(Layering)。"分层" 这一术语有不少同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。条款35 解释了公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用...来实现"。
当经过分层使两个类产生联系时,实际上在两个类之间创建了编译时的依赖关系。

41.区分继承和模板

模板类的特色:行为不依赖于类型。
当对象的类型不影响类中函数的行为时,就要使用模板来生成这样一组类。当对象的类型影响类中函数的行为时,就要使用继承来获得这样一组类。

42.明智的使用私有继承

这为咱们引出了私有继承的含义:私有继承意味着 "用...来实现"。若是使类D 私有继承于类B,这样作是由于你想利用类B 中已经存在的某些代码,而不是由于类型B 的对象和类型D 的对象之间有什么概念上的关系。于是,私有继承纯粹是一种实现技术。
私有继承意味着 "用...来实现" 这一事实会给程序员带来一点混淆,由于条款40 指出,"分层" 也具备相同的含义。怎么在两者之间进行选择呢?答案很简单:尽量地使用分层,必须时才使用私有继承。

43.明智的使用多继承

44.说你想说的;理解你所说的

45.弄清C++在幕后为你所写、所调用的函数

至于拷贝构造函数和赋值运算符,官方的规则是:缺省拷贝构造函数(赋值运算符)对类的非静态数据成员进行 "以成员为单位的" 逐一拷贝构造(赋值)。即,若是m 是类C 中类型为T 的非静态数据成员,而且C 没有声明拷贝构造函数(赋值运算符),m 将会经过类型T 的拷贝构造函数(赋值运算符)被拷贝构造(赋值)---- 若是T 有拷贝构造函数(赋值运算符)的话。若是没有,规则递归应用到m 的数据成员,直至找到一个拷贝构造函数(赋值运算符)或固定类型(例如,int,double,指针,等)为止。默认状况下,固定类型的对象拷贝构造(赋值)时是从源对象到目标对象的 "逐位" 拷贝。对于从别的类继承而来的类来讲,这条规则适用于继承层次结构中的每一层,因此,用户自定义的构造函数和赋值运算符不管在哪一层被声明,都会被调用。

46.宁肯编译和连接时出错,也不要运行时出错

将检查从运行时转移到编译或连接时一直是值得努力的目标,只要实际可行,就要追求这一目标。这样作的奖赏是,程序会更小,更快,更可靠。

47.确保非局部静态对象在使用前被初始化

非局部静态对象指的是这样的对象:

定义在全局或名字空间范围内(例如:theFileSystem 和tempDir),
在一个类中被声明为static,或,
在一个文件范围被定义为static。
对于不一样被编译单元中的非局部静态对象,你必定不但愿本身的程序行为依赖于它们的初始化顺序,由于你没法控制这种顺序。让我再重复一遍:你绝对没法控制不一样被编译单元中非局部静态对象的初始化顺序。
很天然地想知道,为何没法控制?
这是由于,肯定非局部静态对象初始化的 " 正确" 顺序很困难,很是困难,极其困难。即便在它最普通的形式下 ---- 多个被编译单元,多个经过隐式模板实例化所生成的非局部静态对象(隐式模板实例化时,它们自己可能都会产生这样的问题) ---- 不只不可能肯定正确的初始化顺序,每每连找一个能够肯定正确顺序的特殊状况都不值得。

48.重视编译器警告

49.熟悉标准库

标准库提供了下列高效的实现:vector(就象动态可扩充的数组),list(双链表),queue, stack,deque,map,set和bitset。唉,居然没有hash table(虽然不少制造商做为扩充提供),但多少能够做为补偿的一点是, string 是容器。这很重要,由于它意味着对容器所作的任何操做(见下文)对string 也适用。

50.提升对C++的认识

相关文章
相关标签/搜索