《Effective C++》学习

来源:互联网 发布:鱼缸怎么卖的淘宝 编辑:程序博客网 时间:2024/06/05 06:54

本文基础内容来自书籍《Effective C++》,转自博客http://blog.csdn.net/cscmaker/article/details/7553411

条款01  视C++为一个语言联邦

理解C++,必须认识其主要的次语言。共有四个:

C,Object-Orented C++, Template C++, STL

条款02 尽量以const, enum, inline替换#define

     如:#define  VALUE  1,该记号名称可能没有进入记号表,所以当你在使用该变量产生一个编译错误的时候,编译器的错误信息中会提到 1 这个数值,但是没有VALUE,这使得你无法准确的定位到错误。而是用const声明常量就会解决这个问题。

     class GamePlayer{

     static const int num = 5;

     int  scores[num];

};

在旧式编译器中也许不支持上述语法,他们不允许static成员在其声明式上获得初值。如果你的编译器不支持在声明式上赋值,可以再class的实现文件内进行定义。如:

class Test{

 static const int num; //在Test的头文件中声明

};

const int Test::num = 5; //在Test实现文件内

但是在GamePlayer类中,你必须要在声明数组scores时,给他一个确定的数值。这种情况下,可以使用“the enum hack“补偿做法。其理论基础是:“一个属于枚举类型的数值可权充ints来使用”,于是GamePlayer可定义如下:

class GamePlayer{

  enum{num = 5};

  int sorces[num];

};

对于enum hack方法来说,它的行为比较像#define,但是它能够计入记号表,能够避免编译错误不好定位的问题,并且他不像const 常量,取一个const的值是可以的,但是不可以取一个enum的地址。所以当你不想让别人获得一个pointer或者reference指向你的某个整数常量,enum可以帮你实现这个约束。

使用#define实现类似于函数的宏,不会招致函数调用带来的额外的开销。但是使用这种宏经常会出现各种陷阱和麻烦。所以使用inline来代替类似于函数的这种宏。

条款03:尽可能使用const

将某些东西声明为const可帮助编译器侦测出错用法,const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体

编译器强制实施bitwiseconstness,但是你编写程序时应该使用“概念上的常量性”

当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

条款04:确定对象使用前已先被初始化

     读取未初始化的对象会导致不明确的行为,在某些平台上,仅仅只是读取未初始化的值,就可能使得程序终止。

     所以在使用任何类型前都应该首先初始化,无论是内置类型还是自定义类型。对于无任何成员的内置类型,我们需要手工完成初始化,对于自定义类型,初始化的任务落在了构造函数中。另外,还需要了解赋值和初始化的区别。例如:

class Test{

 int  x;

 stringy;

Test(int x, string y){}

};

Test:Test(){

 this->x= x; //这是赋值操作,而不是初始化操作

this->y = y;

}

C++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前。如Test的成员函数的初始化发生在default构造函数调用之前。Test构造函数的最佳写法是,使用所谓的member initialization list替换赋值动作:

Test::Test(int  x,string  y):x(x),y(y)  //这实现了成员变量的初始化。

{

  //do other thing

}

这样使得成员变量在初始化的时候就设置了相对应的值,而不需要在经过一次赋值操作。提高了效率。

C++有着十分固定的“成员初始化次序”。基类总是更早于子类的初始化,而class成员变量的初始化总是以其声明的次序初始化。

C++对于“定义在不同的编译单元内的non-localstatic对象的初始化次序并无明确的定义”。解决这种问题的办法是将每个non-local static对象搬到自己的专属函数内。这些函数返回一个reference,用户调用这些static函数,而不直接涉及到这些对象。这种手法的基础在于:C++保证,函数内的local static对象会在“该函数被调用期间,首次遇到该对象”时被初始化。单例模式使用的就是这种手法。

《Effective C++ 》第 2节  构造/析构/赋值运算符

条款05: 了解C++默默编写并调用哪些函数

    C++编译器会为一个空类声明一个copy构造函数,一个copy assignment操作符和一个析构函数。此外,如果没有声明构造函数,编译器会为你声明一个default构造函数。所有这些函数都是public inline。

     default构造函数和析构函数用来存放“隐藏幕后”的代码。copy构造函数和copy assignment操作符只是单纯的将来源对象的每一个non-static成员变量拷贝到目标对象。但是要注意三点:

    a:如果打算在一个内含reference成员的class支持赋值操作,你必须自己定义copy assignment操作符。因为C++不允许让reference改指向不同的对象

    b:若类中使用const成员,因为更改const成员是不合法的。所以也需要自已实现copy assignment操作和copy构造函数

    c:如果某个base classes将copy assignment操作符声明为private,编译器将拒绝为其derived classes生成一个copy assignment操作符。

条款06:若不想使用编译器自动生成的函数,就应该明确拒绝

     为了驳回编译器自动提供的机能,可将相应的成员函数声明为private并且不予实现,使用像Uncopyable这样的base class也是一种做法。

    Uncopyable作为base class方法

    class Uncopyable{

      protected:

          Uncopyable(){};

          ~Uncopyable(){};

     private:

        Uncopyable(const Uncopyable&);

      Uncopyable& operator= (const Uncopyable&);

    };

    其他的类private继承Uncopyable

    class HomeForSale: private Uncopyable{

          ............/////////     

};

   这样就不能对HomeForSale使用copy构造函数和copy assignment操作符了

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

     C++明确指出:当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,其结果未有定义,实际上执行时通常发生的是对象的derived成分没有被销毁。只执行了其base class 的析构函数,造成了局部销毁资源的状况。从而造成了内存泄露。

    解决这个问题的做法很简单:给base class一个virtual析构函数。此后删除derived class对象就会如你想要的那般。它会消除整个对象,包括base class成分。如:

   class BaseClass{

    BaseClass(){};

    virtual  ~BaseClass(){ cout<<"~BaseClass()"<<endl;}  

};

  class DrivedClass: public BaseClass{

  DrivedClass(){} 

 ~DrivedClass(){ cout<<"~DrivedClass()"<<endl;}  

};

BaseClass* pBaseClass = new DrivedClass();

delete pBaseClass; //现在不会出现局部删除的现象。

如果class不含virtual函数,通常表示它并不意图被用作一个base class。当class不企图被当做base class,令其析构函数为virtual函数往往是个馊主意。这样做会使得对象会携带某些不必要的信息,用来运行期决定哪一个virtual函数该被调用。 

   classes的设计目的如果不是作为base classes使用,或者不是为了具备多态性质,就不该声明virtual析构函数。

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

       在析构函数中出现异常,会导致程序过早的结束,造成某些资源的泄漏,例如:应该delete某些对象,但是没有执行到。

      a:析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下它们(不传播)或者结束程序(abort)

      b:如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。

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

       base class在构造函数或者析构函数中如果使用vritual函数,那么该base class调用的也是自身的virtual函数,而不会降级为derived class 相应的virtual函数。

      记住这类调用从不下降至derived class。

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

     注意,这只是一个协议,并不一定必须遵循它。然而这分协议被所有内置类型和标准程序库提供的类型如:string ,vector, complex等共同遵守。

条款11:在operator=中处理“自我赋值”

      确保当对象自我赋值时operator=有良好行为,其中技术包括比较“来源对象”和“目标对象”的地址,精心周到的语句顺序,以及copy-and-swap

      确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。

条款12:复制对象时勿忘其每一个成分

      copy函数应该确保复制“对象内的所有成员变量”及“所有base class成分”

     不要尝试以某个copying函数实现另一个copy函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调用。

《Effective C++ 》第 3节  资源管理

条款13:以对象管理资源

          为防止资源泄漏,请使用RAII(Resource Acquisition Is Initialliztion资源取得时机便是初始化时机)对象,它们在构造函数中获得资源并在析构函数中释放资源。

          两个常被使用的RAII classes分别时tr1::shared_ptr(引用计数型智慧指针RCSP)和auto_ptr(标准库中的智能指针)。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使他指向NULL。

条款14:在资源管理类中小心copying行为

         复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。(例如:常见的一个错误就是,对象A中存在对另外一个对象B的引用,此时使用默认的copy函数复制该对象A为C,那么在C析构的时候会将B删除掉,A在析构的时候就会出错)

         普遍而常见的RAII class copying行为是:抑制copying, 施行引用计数法(shared_ptr类似方法)。不过其他行为也可能被实现。

条款15:在资源管理类中提供对原始资源的访问

         APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。(例如:shared_ptr,auto_ptr重载了指针取值操作符( -> 和 *),或者提供一个get()函数,返回一个原始指针)

       对原始资源的访问可能经由显示转换或者隐式转换。一般而言显式转换比较安全,但是隐式转换对客户比较方便。

条款16:成对使用new和delete时要采取相同的形式

          int* p = new int[5];  delete p;  //这样是错误的,只删除了第一个元素的内存空间。

          如果你在new表达式中使用[],必须在相应的delete表达式中也使用[],如果你在new表达式中不使用[],也一定不要在delete中使用[]

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

          例如:函数 void processWidget(std::tr1::shared_ptr<Widget> pw, int priority);在具体调用的时候使用如下方式:

                      processWidget(std::tr1::shared_ptr<Widget> (new Widget), priority);该函数中在调用该函数之前需要做三件事情:

                       A :调用 priority         B:执行 new Widget      C: 调用tr1::shared_ptr构造函数

                      此时,如果这样对函数进行调用,会有资源泄漏的可能性。因为C++编译器对于这三个事情的执行顺序没有严格的规定。如果 new在第一个位置执行,priority在第二个执行,此时在执行第二个出错的时候,new出来的资源就无法放入智能指针中,也就造成了资源泄漏。

           以独立的语句将newed对象存储于智能指针中,如果不这样做一旦抛出异常,就有可能导致资源泄漏。

《Effective C++ 》第 4节  设计与声明

条款18:让接口容易被正确使用,不易被误用

         好的接口很容易被正确使用,不容易被误用。

         “促使正确使用”的办法包括接口的一致性,以及内置类型的行为兼容。

         “阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。

          tr1::shared_ptr支持定制型删除器,这可防范DLL问题(跨DLL之new/delete成对运用),可被用来自动解除互斥锁。但是这种智能指针在防范用户出现错误的同时,增加了系统的开销,boost库中shared_ptr的大小是原始指针的两倍大。所以需要综合考虑其执行成本。

条款19:设计class犹如设计type

          如何设计高效的classes,首先需要了解你面对的问题,几乎每一个class都要求你面对以下提问:

         新type的对象应该如何被创建和销毁?涉及到构造函数和析构函数以及内存分配函数,释放函数的设计

         对象的初始化和对象的赋值该有什么样的差别?这个答案决定你的构造函数和赋值操作符的行为,以及期间的差异。

        新type的对象如果被passed by value,意味着什么?copy构造函数用来定义一个type的pass by value该如何实现

        什么是新type的“合法值”?对class的成员变量而言,通常只有某些数值集是有效的。

条款20:宁以pass-by-reference-to-const替换pass-by-value

          例如:bool validateStudent(Student s);

                      Student plato;

                      当调用此函数的时候,把plato当作函数的参数,此时首先Student的构造函数会被调用,以plato为蓝本将s初始化,同样,在函数返回时,会将s销毁。因此对于该函数而言,参数传递的成本是:一次copy构造函数的调用,一次析构函数的调用。当然,当Student类中还存在其他对象的话,那这个代价就不只一次copy,一次析构了。

         所以:尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。

                     以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。

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

          绝不要返回pointer或者reference指向一个local  stack对象,或者返回reference指向一个heap-allocated对象,或者返回pointer或者reference指向一个local static对象而有可能同时需要多个这样的对象。

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

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

       protected并不比public更具封装性。

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

          如果某些东西被封装,它就不再可见,愈多东西被封装,愈少人可以看到它。而愈少人看到它,我们就有愈大的弹性去改变它,因为我们的改变仅仅直接影响看到改变的那些人事物。因此,愈多东西被封装,我们改变那些东西的能力也就愈大。这就是我们首先推崇封装的原因:他使我们能够改变事务而只影响有限客户。

         现在考虑对象内的数据。愈少代码可以看到数据,愈多的数据可被封装,而我们也就愈能自由的 改变对象数据。

         条款这样做可以增加封装性,包裹弹性和机能扩充性。

条款24:若所有参数皆需类型转换,请为此采用non-member函数

           只有当参数被列于参数列内,这个参数才是隐式类型转换的合格参与者。地位相当于“被调用之成员函数所隶属的那个对象”-----即this对象------的那个隐喻对象,绝不是隐式转换的合格参数者。

            如:const Rational operator* (const Rational& rhs) const; ///成员函数,

                   result = 2* oneHalf;     //result , oneHalf都是Rational对象,此时变成  2.operator*(oneHalf)。错误!2不能隐式转换成Rational对象!

                   const Rational operator*(const Rational& rhs1, const Rational& rhs2); //non-member函数

                  此时使用  result = 2* oneHalf  //   2可以被隐式转换成Rational对象,前提是构造函数是non-explicit

条款25:考虑写出一个不抛出异常的swap函数

         当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。 

         如果你提供一个member swap,也该提供一个non-memebr swap用来调用前者。对于classes,也请特化std::swap

         调用swap时应针对std::swap使用using 声明式,然后调用swap并且不带任何“命名控件资格修饰”

         为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。  

《Effective C++ 》第 5 节  实现

条款26:尽可能延后变量定义式的出现时间

条款27:尽量少做转型动作

          C++提供了四种新式的转型

          A:const_cast通常被用来将对象的常量性转除。它也是唯一有此能力的C++ style转型操作符。

          B:dynamic_cast主要用来执行“安全向下转型”,也就是用来决定某些对象是否归属于集成体系中的某个类型。它是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。

          C:reinterpret_cast意图执行低级转型,实际动作可能取决于编译器,这也就表示它并不可移植。

          D:static_cast用来强迫隐式转换,例如将non-const对象转换成cosnt对象,或者将int转换成double等等。

          请记住:

          如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_casts,如果有个设计需要转型,试着发展无需转型的替代设计。

          如果转型是必要的,试着将它隐藏于某个函数背后。客户随后可以调用该函数,而不需要将转型放入自己的代码中。

          宁以C++ style转型,不要使用旧式转型,前者很容易辨识出来,而且也比较有着分别门类的职掌。

条款28:避免返回handles指向对象内部成分

          不论这所谓的handle是个指针或者迭代器或者reference,也不论这个handle是否为const,也不论那个返回handle的成员函数是否为const。这里唯一关键的是,有个handle被传出去了,一旦如此你就是暴露在“handle比其所指对象更长寿”的风险下。

条款29:为“异常安全”而努力是值得的

         异常安全函数提供以下三个保证之一:

         基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。没有任何对象或数据结构会因此而败坏,所有对象都处于一种内部前后一致的状态。

         强烈保证:如果异常被抛出,程序状态不改变。调用这样的函数需要有这样的认知:如果函数成功,就是完全成功,如果函数失败,程序会恢复到“调用函数之前”的状态。

         不抛掷保证:承诺绝不抛出异常,因为他们总能够完成他们原先承诺的功能。作用于内置类型身上的所有操作都提供nothrow保证。

          "强烈保证"往往能够以copy-and-swap实现出来,但是“强烈保证”并非对所有函数都可以实现或者具备现实意义。

          函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。

条款30:透彻的了街inlining的里里外外

           将大多数inlining限制在小型,被频繁调用的函数身上。这可使日后的调式过程和二进制升级更容易,也可使潜在的代码膨胀问题最小化,使程序的速度提升机会最大化。

           不要因为funciton templates出现在头文件,就将他们声明为inline

条款31:将文件间的编译依存关系降至最低

         Interface classes类似java和.net的interfaces,但是C++的interface classes并不需要负担java和.net的interface所要负担的责任。举个例子:java和.net都不允许在interface内实现成员变量和成员函数,但是C++不禁止这两样东西。C++这种更为巨大的弹性有其用途。

        支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes和interface classes

        程序库文件应该以“完全且仅有声明式”的形式存在。这种做法不论是否涉及templates都适用。


条款32:确定你的public继承塑模出is-a关系

          public继承  意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class 对象。

条款33:避免遮掩继承而来的名称

          derived classes内的名称会遮掩base classes内的名称,在public继承下从来没有人希望如此。

          为了让被遮掩的名称在再见天日,可使用using声明式或者转交函数。

条款34:区分接口继承与实现继承

           接口继承与实现继承不同,在public继承之下,derived classes总是继承base classes的接口。

           pure virtual函数只具体制定接口继承

           简朴的impure virtual函数具体制定接口继承及缺省实现继承

           non-virtual函数具体指定接口继承以及强制性实现继承

条款35:考虑virtual函数以外的其他选择

          藉由non-virtual interface手法实现 template method模式 

         这种流派主张virtual函数应该几乎总是private。较好的设计是使用一个non-virtual的public成员函数作为对外的接口,并调用一个private virtual函数进行实际工作。 

         藉由Function Pointers实现strategy模式

         这种方法主张类中接受一个函数指针,指向要执行的动作。从而实现不同的策略。

         藉由tr1::function完成strategy模式

         这种方法不再使用函数指针,而是改用一个类型为tr1::function的对象。

         定义式:typedef std::tr1::function<int (const GameCharacter&)>HealthCalcFunc;

         这个签名代表的函数“接受一个reference指向const GameCharacter,并且返回int”。这个tr1::function类型产生的对象可以持有任何与签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换成const GameCharacter&,而其返回类型可被隐式转换为int。(这种方法没有明白,还要继续研究。)

条款36:绝不重新定义继承而来的non-virtual函数 

条款37:绝不重新定义继承而来的缺省参数值

            virtual函数是动态绑定,而non-virtual则是静态绑定,缺省参数值是静态绑定。

           静态绑定就是他在程序中被声明时所采用的类型,动态类型是指“目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。

           例如:class Shape{

                         public:

                               virtual void draw(ShapeColor color =Red)const = 0;

                       }

                      class Rectangle:public Shape{

                       public:

                          virtual void draw(ShapeColor color =Green) const ;

                       }

                      Shape* pr = new Rectangle();

                      pr->draw();        //此时默认的参数不是Green而是Red,因为,缺省参数值静态绑定!

条款38:通过复合塑模出has-a或者“根据某物实现出”

          复合的意义和public继承完全不同

          在应用领域中,复合意味着has-a。在实现领域中,复合意味着is-implemented-in-terms-of(根据某物实现出),例如通过list实现set。

条款39:明智而审慎地使用private继承

          Private继承意味is-implemented-in-terms of。它通常比复合的级别低。但是当derived class需要访问protected base class的成员,或者需要重新定义继承而来的virtual函数时,这样设计是合理的。

         和复合不同,private继承可以造成empty base最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。

条款40:明智而审慎地使用多重继承

         多重继承比单一继承复杂,它可能导致新的歧义性,以及对virtual继承的需要。

        virtual继承会增加大小,速度,初始化复杂度等成本。如果virtual base classes不带任何数据,将是最具实用价值的情况

        多重继承的确有正当用途,其中一个情节涉及“public 继承某个Interface class”和“private继承某个协助实现的class”的两相组合。


原创粉丝点击