《Effective C++》 读后总结

概念汇总:

        local static对象:函数内的static对象。因为它们对函数而言是local。

       non-local static对象:其他static对象。一般指global 对象、定义于namespace作用域内的对象、在class内、以及在file作用域内被声明为static的对象。(所谓static对象,其寿命从被构造出来直到程序结束为止。其使用有作用域限定)

 

P5:

      类的构造函数被声明为explicit,这可阻止它们被用来执行隐式类型转换(implicit type conversions),但它们仍可被用来进行显示类型转换(explicit type conversions)

 

P8:

      lhs代表“left-hand side”左手端,rhs代表“right-hand side”右手端,即分别是左操作数和右操作数。

      将“指向一个T型对象”的指针命名为pt,意思是“pointer to T”。比如 Widget* pw;   //pw="ptr to Widget"

                                   class Airplane;         Airplane* pa;           //pa="ptr to Airplane"

       对于references使用类似的习惯:rw可能是个 reference to Widget,ra则是个reference to Airplane.

       当讨论成员函数时,偶尔会以mf为名。

P14:

       常量定义是通常被放在头文件内(以便被不同的源码含入);

       inline函数被放在头文件内。

P16:

        如果你不想让别人获得一个pointer或reference指向你的某个整数常量,enum可以帮助你事先这个约束。

P17:

       对于单纯常量,最好以const对象或enums替换#defines。

       对于形式函数的宏(macros),最好改用inline函数替换#defines。

P18:const指针

      “左被指,右自身”:如果关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量。迭代器与之相反。

      两个成员函数如果只是常量性(constness)不同,可以被重载。

P22:

      利用C++的一个与const相关的摆动场:mutable(可变的),可以解决常成员函数会改变non-static成员变量的问题。mutable释放掉non-static成员变量的bitwise constness约束。

P27:

      永远在使用对象之前先将它初始化。总是使用成员初始化列表。这样做有时候绝对必要,且又往往比赋值更高效。

      对于无任何成员的内置类型,你必须手工完成此事。至于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。

       C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。因此default构造函数实现初始化成员变量的操作,应该采用member initialization list(成员初始化列表),而不是在函数体内执行assignment操作。这样效率会更高。

P33:

      为内置型对象进行手工初始化,因为C++不保证初始化它们。

      构造函数最好使用成员初始化列表(member initialization list),而不要在构造函数本体内使用赋值操作(assignment)。初始化列表列出的成员变量,其排列次序应该和它们在class中的声明次序相同。

      为免除“跨编译单元之初始化次序”的问题,请以local static对象替换non-local static对象。

P38:

     条款06:“若不想使用编译器自动生成的函数,就该明确拒绝”——————策略是:所有编译器产出的函数都是public的。为了阻止这些函数被创建出来,你可以自行声明它们。通过明确声明一个成员函数,你阻止了编译器暗自创建其专属版本;而令这些函数为private而且故意不去定义它们,使得你得以成功阻止人们调用它。

      具体原因:通过将其声明为private,可以将执行copy constructor或assignment操作的错误从连接期移至编译期。但不是在该类本身,而是在一个专门为了阻止这些函数而设计的base class内。同时,不定义它们,让member函数和friend函数调用它们时,会获得一个连接错误(likage error)。---------从而实现了禁用赋值构造函数和赋值操作!

      应用实例:类的每个实体都是独一无二的,不可能有两个相同的实体。文中的例子是 待售的房子HomeForSale类,函数指赋值构造函数和赋值函数。

 

条款07:为多态基类声明virtual析构函数。

        问题:当derived class 对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义———实际执行时通常发生的是对象的derived成分没被销毁。即“经由base class接口处置derived class对象”的情形。

        解决办法:给base class一个virtual析构函数。此后删除derived class对象就会是你想要的。它会删除整个对象,包括所有derived class成分。

注:

  •        所有STL容器如vector,list,set,tr1:unordered_map等 都不带任何virtual析构函数。因此,派生它们需要注意这个问题。
  •        任何class只要带有virtual函数都确定应该也有一个virtual析构函数。如果class不含virtual函数,通常表示它并不意图被用做一个base class。当class不企图被当作base class,其析构函数不应该为virtual的。       ——可以为抽象类声明一个pure virtual析构函数。但是,必须为这个pure virtual析构函数提供一份定义!(含有pure virtual函数的类为抽象类。抽象类不能实例化,只能作为基类。)    ————P41

条款08:别让异常逃离析构函数

  •        析构函数绝对不要吐出异常。如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或结束程序(abort())。
  •         如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。

条款09:绝不在构造和析构过程中调用virtual函数

       在派生类的构造和析构期间不要调用virtual函数,因为这类调用从不下降至derived class。——————按照构造函数中变量属性的初始化顺序,应该是先构造基类的成员变量,然后再顺序构造派生类的成员变量;析构函数的析构顺序相反。因此,在派生类的构造函数执行期间,只调用基类的virtual函数。

条款10:令 operator= 返回一个reference to *this。

       理由是:为了实现“连锁赋值”,赋值操作符必须返回一个reference指向操作符的左侧实参。这样,返回值也必须是引用了。

            int x,y,z;

           x=y=z=13;                 //赋值连锁形式

P60:copy cstr和Operator=

     如果你发现你的copy构造函数和copy assignment操作符有相近的代码,消除重复代码的做法是,建立一个新的成员函数给两者调用。这样的函数往往是private,而且常被命名为init

条款13:以对象管理资源。——针对动态分配内存

     原因:避免new出来的内存,因为过早返回或出现异常 而 没有delete的情况。

      把资源放进对象内,我们便可倚赖C++的“析构函数自动调用机制”确保资源被释放。

     “资源取得时机便是初始化时机”(RAII):我们总是在获得一笔资源后于同一语句内以它初始化某个管理对象。有时候获得的资源被拿来赋值(而非初始化)某个对象。由此,可以实现在构造函数中获得资源并在析构函数中释放资源。

注意:

       auto_ptr和tr1::shared_ptr两者都在其析构函数内做delete而不是delete[]动作。因此,在动态分配而得的array身上使用auto_ptr或tr1::shared_ptr是个馊主意。

      Boost库中的boost::scoped_array 和 boost::shared_array_classes提供了针对动态分配数组的内存管理行为

P68:deep copying

     deep copying行为:某些标准字符串类型是由“指向heap内存”之指针构成(那内存被用来存放字符串的组成字符)。这种字符串对象内含一个指针指向一块heap内存。当这样一个字符串对象被复制,无论指针或其所指内存都会被制作出一个复检。

P72:智能指针之转换函数

      1. 是否该提供一个explicit转换函数(例如shared_ptr中的get成员函数)将RAII class转换为其底部资源,或是应该提供implicit转换?

      答案主要取决于RAII class被设计执行的特定工作,以及它被使用的情况。通常是显示转换函数如get是更安全的,因为它将“非故意之类型转换”的可能性最小化了。从而很好的防止了内存资源泄露

      2.APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。

P75:new typedef定义的类型

考虑下面这个typedef:

       typedef std::string AddressLines[4];              //每个人的地址有4行,每行是一个string

   由于AddressLines是个数组,如果这样使用new:

       std::string* pal=new AddressLines;               //注意,"new AddressLines"返回一个string*,就像“new string[4]”一样

   那就匹配“数组形式”的delete:

       delete pal;                               //行为未有定义

       delete []pal;                             //很好

条款17:以独立语句将newed对象置于智能指针

       资源被创建   ——   经由 "new Widget" 创建资源

       资源转换为资源管理对象  ——  使用智能指针来管理被创建的资源

   问题场景:

        在 “资源被创建” 和 “资源被转换为资源管理对象” 两个时间点之间有可能发生异常干扰。 即,在使用 以智能指针作为形参之一的函数时,C++不是以特定次序完成函数参数的核算的,也就是:资源被创建  和 资源被转换为资源管理对象 两个动作发生的时间不连续,期间可能会发生另外的实参核算,但是它们的先后顺序是一定的。因此,可能在 发生另外的实参核算的过程中发生异常,从而导致了heap分配出来的内存被遗失,导致内存资源泄露。

    因此,需要以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。

第四章——设计与声明 Designs and Declarations

条款21:必须返回对象时,别妄想返回其reference。

       问题1:绝不要返回pointer 或 reference 指向一个 local stack 对象;

       问题2:返回reference 指向一个 heap-allocated 对象(new出来的)。比如在operator重载的连续多层调用时,容易造成资源泄露。实例,重载 operator* ,函数返回值是new出来的heap-allocated对象,出错代码:w=x*y*z。

       问题3:返回 pointer 或 reference 指向一个 local static 对象。因为有可能一行代码同时返回多个这样的对象,然而local static对象只有一个。由于返回值都是reference,导致调用端看到的用于都是 static对象的“现值”,就造成不期望的结果。常见场景是两个对象进行比较,比如: bool operator== (const Rational& lhs, const Rational& rhs)。

注意:条款4已经为“在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。

条款4: 明确对象被使用前已先被初始化。

      需要明白的是:读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你的程序终止运行。更可能的情况是读入一些“半随机”bits,污染了正在进行读取动作的那个对象,最终导致不可测知的程序行为,以及许多令人不愉快的调试过程。

       最佳处理办法就是:永远在使用对象之前先将它初始化。如果是非内置型对象,总是使用 成员初始化列表。      

1)对于C++内置型对象,需要进行手工初始化,因为C++不保证初始化它们。实例:int x=0;

2)对于内置类型以外的任何其他东西,初始化责任落在构造函数(constructors)身上。规则很简单:确保每一个构造函数都将对象的每一个成员初始化,并且初始化的顺序应该和它们在class中的声明次序相同。值得注意的是:C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。因此,构造函数应该使用 成员初始化列表(member initialization list) 达到初始化的目的,而不是赋值操作(assignment)。甚至当你想要default构造一个成员变量,你都可以使用 成员初始化列表,只要指定无物作为初始化实参即可(前提是各成员变量都有自己的默认构造函数)。

3)为免除“跨编译单元之初始化次序”问题,请以local static 对象替换non-local static对象。注:这里就有一份“为单线程环境中合理返回reference指向一个local static对象”的设计实例。

条款22:将成员变量声明为private

       被广泛使用的classes是最需要封装的一个族群,因为它们最能够从“改采用一个较佳实现版本”中获益。(这里的“封装”指的是将成员变量声明为private)原因是,某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。也就是类中的成员变量封装得越好,当你想取消这个变量的时候,需要改变的代码量越少,尤其是对于被广泛使用的基类。

     成员变量应该是private,因为如果它们不是,就有无限量的函数可以访问它们,它们也就毫无封装性。

      切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。

条款23:宁以non-member、non-friend替换member函数

       面向对象守则要求数据应该尽可能被封装。如果某些东西被封装,它就不再可见。越多东西被封装,越少人可以看到它,我们就有越大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的那些人和事物。因此,他使我们能够改变事物而只影响有限客户。

       我们可以通过计算能够访问该数据的函数数量,来量测数据的封装性。越多函数可访问它,数据的封装性就越低。

       因此,如果对于同一个功能函数,如果它可以被声明为member函数或一个non-member,non-friend函数,那么我们应该选择后者,因为它保证了数据的封装性。C++中,比较自然的做法是让该函数声明为一个non-member函数,并且与对应类位于相同namespace内。 这里需要注意的是,这个功能函数可以被声明为另一个类的member函数。

       namespace与classes不同,前者可以跨越多个源码文件,而后者不能。即在不同头文件可以定义相同的namespace,只要它的内部成员命名没有冲突。典型实例就是C++标准程序库的组织方式,它有数十个头文件,每个头文件都声明了std的某些机能。这意味着客户可以轻松扩展这一组便利函数,他们需要做的就是添加更多non-member non-friend函数到此命名空间内,新函数就像其他旧有的便利函数那样可用且整合为一体。

       降低编译依存性!!!条款31

      总结:宁可拿non-member non-friend函数替换member函数。这样做可以增加封装性、包裹弹性(packaging flexibility,即函数内部有更大的弹性做相关工作)和机能扩充性(函数会被声明在相同的namespace内)。

 

 

 

 

 

 

你可能感兴趣的:(C++语法,c++)