《Effective C++》读书笔记(二) 构造/析构/赋值运算 (第一部分)

来源:互联网 发布:如何在淘宝卖二手货 编辑:程序博客网 时间:2024/05/17 23:56

构造/析构/赋值运算

Constructors,Destructors,and Assignment Operators

 

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

              Know what functions C++ silently writes and calls.

       当我们写下一个empty class时,不包含任何我们声明的constructors,destructors,以及copy assignment操作符时,就像这样:

class Empty {};

         看起来什么都没做,其实编译器已经帮我们体贴地做了不少。就像这样:

class Empty{public:     Empty() {...}                               //default构造函数     Empty(const Empty& rhs) {...}               //copy构造函数     ~Empty() {...}                              //析构函数     Empty& operator=(const Empty& rhs) {...}    //copy assignment 操作符};

        default构造函数和析构函数主要是给编译器一个地方用来放置“藏身幕后”的代码,像是调用base classes和non-static成员变量的构造函数和析构函数。而且,编译器默认生成的destructors是个non-virtual,除非这个class的基类(base class)自身声明有virtual destructors。

        当然了,如果我们主动地去声明了constructors,destructors,以及copy assignment操作符,那么编译器就会带领它的default构造函数、copy构造函数、析构函数和copy assignment操作符退居二线,也就没必要再去自动创建,来遮蔽我们主动声明的版本了。

 

☆编译器可以暗自为class创建default构造函数、copy构造函数、copy assignment操作符,以及析构函数。

 

 

 

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

             Explicit disallow the use of compiler-generated functions you do not want.

       书上有个例子:地产商中介卖的是房子,一个中介软件系统自然而然想有个class来描述待售房屋。而房屋是独一无二的。就像这样~

class HomeForSale {...}

       为这房屋去创建一个副本显然不合适。也就是说,各种蓄谋对此房屋进行copy的行为,都应该以失败收场,理想情况应该是这样:

HomeForSale h1;HomeForSale h2;HomeForSale h3(h1);       //企图复制h1----不该通过编译h2=h1;                    //还是企图复制h1----也不该通过编译

        然而。理想很丰满,现实很骨感。依据上边所说的条款5,就算我们不去声明,只要有人想去调用它们,编译器还是会去帮这个忙。再说清楚些,如果不声明copy构造函数或copy assignment操作符,编译器会帮忙生成一份默认的;如果声明了,所写的class还是支持copying。而在这个例子的目标,是去阻止copying!

 

      解决之道很简单:将copy构造函数和copy assignment操作符声明在private中并且故意不实现。也就是“将成员函数声明为private而且故意不实现它们”的伎俩。就算member函数和friend函数还是能调用private函数,也会发生连接错误(linkage error)。就像这样:

class HomeForSale{public:    ...private:    ...    HomeForSale(const HomeForSale&);               //只有声明    HomeForSale& operator=(const HomeForSale&);};

        不过有一种更好的方法,就是把连接期错误移到编译期,越早侦测到错误越好。只要任何人——甚至member函数或friend函数去尝试复制HomeForSale对象,编译器就会试着生成一个copy构造函数和一个copy assignment操作符,而编译器生成版又会去尝试调用其base class的对应兄弟,最后只会被拒绝。因为base class的copy函数不是public或protected,而是private。

class Uncopyable{protected:                  //允许derived对象构造或析构    Uncopyable() {}    ~Uncopyable() {}private:    Uncopyable(const Uncopyable&);        //但阻止copying    Uncopyable& operator=(const Uncopyable&);};...class HomeForSale:private Uncopyable{    ...    //class不再声明copy构造函数或copy assignment操作符};



 

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

 

 

 

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

              Declare destructors virtual in polymorphic base classes.

        当derived class对象经由一个base class指针被删除,而该base class带着一个non-virtual析构函数,实际执行时通常发生的是对象的derived成分没被销毁。形如这样的classes:

class TimeKeeper{public:    TimeKeeper();    ~TimeKeeper();    ...};class AtomicClock: public TimeKeeper {...};class WaterClock:  public TimeKeeper {...};class WristWatch:  public TimeKeeper {...};

        消除这个问题的做法很简单:给base class一个virtual析构函数,之后删除derived class对象就会是我们期望的。对,它会销毁整个对象,包括所有derived class成分,就像这样:

class TimeKeeper{public:    TimeKeeper();    virtual ~TimeKeeper();    ...};TimeKeeper* ptk=getTimeKeeper();...delete ptk;

        析构函数的运作方式是:最深层派生(most derived)的那个class其析构函数最先被调用,然后是其每一个base class的析构函数被调用。

        声明virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。欲实现出virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个virtual函数该被调用。主要是由vptr(virtual table point)指针指出。如果滥用virtual函数,就会相应增加vptr的使用,也就会引起空间的浪费;并且class不含virtual函数时,通常表示它并不意图被用作一个base class。当class不企图被当做base class时,令其析构函数为virtual往往是个馊主意。比如std::string和STL容器就不被设计作为base classes使用,其中内部的是个non-virtual函数,如果将其当成base class,会因为derived成分没被销毁而导致行为不明确。

        无端地将所有classes的析构函数都声明为virtual,就像从未声明它们为virtual一样,都是错误的。

 

 

 

☆polymorphic(带多态性质的)base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数。

☆Classes的设计目的如果不是作为base classes使用,或不是为了具备多态行(polymorphically),就不该声明为virtual函数。

 

 

 

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

              Prevent exceptions from leaving destructors.

        C++并不禁止析构函数吐出异常,但它不鼓励这样做。在析构函数内吐出异常,也就是说,让异常逃离了析构函数,就很容易引起不明确行为。

        有两种方法可以避免这种问题。第一种是让析构函数记录内部函数的调用失败,然后不传播或者强行结束程序。

DBConn::~DBConn(){    try {db.close(); }    catch (...)    {         制作运转记录,记下对close的调用失败;    }}
DBConn::~DBConn(){    try {db.close(); }    catch (...)    {         制作运转记录,记下对close的调用失败;         std::abort();    }}
        但如果析构函数内部的close()抛出异常呢?

       第二种方法是重新设计DBConn接口,使其客户有机会对可能出现的问题进行反应。

class DBConn{public:    ...    void close()    {         db.close();         closed=true;    }~DBConn{    if (!closed)    {       try       {          db.close();       }       catch (...)       {             制作运转记录,记下对close的调用失败;             ...       }    }}private:    DBConnection db;    bool closed;};
        如果某个操作可能在失败时抛出异常,而又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以为的某个函数。由客户自己调用close并不会对他们带来负担,而是给他们一个处理错误的机会,否则他们没机会相应。




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

☆如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构函数中)执行该操作。


 

参考文献:

  《Effective C++》3rd   Scott Meyers著,侯捷译

原创粉丝点击