第二章 构造/析构/赋值运算

    几乎你写的每一个class 都会有一个或者多个构造函数,一个析构函数,一个copy assignment操作符。这是类的一些基本操作。


条款5:了解C++默默并调用那些函数。

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



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

   由于C++的类是默认生成复制构造函数和=操作函数,有时候会导致不必要的函数复制行为,我们不希望出现下面这种情况:

    

 C++ Code 
1
2
3
4
HomeForSale h1;
HomeForSale h2;
HomeForSale h3(h1);   //不希望调用复制
h1 = h2;    //不希望复制

         方法一是将copy构造函数和copy assignment操作函数进行私有化,即把这些函数定义为private。
具体实现如下所示:
    
 C++ Code 
1
2
3
4
5
6
7
8
9
class HomeForSale
{
public:
    ...
private:
    ...
    HomeForSale( const HomeForSale &);         //只有声明。
    HomeForSale & operator( const HomeForSale &);
};
      这种做法不是绝对的安全,因为member函数和friend函数还是可以访问private函数的。


      方法二:为了有效解决上面的问题,我们可以定义一个base class来解决这个问题。而集成这个base class不再声明copy构造函数或者copy assign操作符。

具体代码如下:

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
class UnCopyable
{
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:     //私有的copy 构造函数和私有的copy assignment操作符函数
    Uncopyable( const UnCopyable &);
    Uncopyable & operator=( const UnCopyable &);
};

class HomeForSale:  private UnCopyable   //私有继承
{
};
    这种方法能行得通的原因是:任何操作,只要尝试拷贝HomeForSale对象,编译器便试着生成一个copy构造函数和一个copy assignment 操作符,这些函数的“编译器生成版”会尝试调用其base class 的对应的兄弟,那些调用会被编译器拒绝,因为其base class 的拷贝函数是private的。多么巧妙的一种方法啊。

   UnCopyable class 的实现和运用颇为微妙,包括不一定得public继承它,以及U年copyable 的析构函数一定不能是Virtual。

    

请记住:

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


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

问题来源:我们先来看例子。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
class TimeKeeper   //base class
{
public:
 TimeKeeper();
 ~TimeKeeper();
};
class AtomicClock: public Timekeeper{...};   //原子钟
class WaterClock: public TimeKeeper{...};   //水表

TimeKeeper *ptk=getTimeKeeper();   //从TimeKeeper继承体系获得一个动态分配对象
...                         //运用之
delete ptk;                //释放它,避免资源泄漏

      在上面的工厂模式中,我们设计工厂函数式,返回指针指向一个计时对象,工厂函数会返回一个基类指针,指向新生成的derived clas对象。为了遵守工厂函数的规矩,被getTimekeeper()返回的对象必须位于heap。因此为了避免泄漏内存和其他资源,将工厂函数返回的每一个对象适当的delete 很重要。

     这样问题就会出现来了。问题出在getTimeKeeper返回的指针指向一个derived class对象,而那个对象却经由一个base class指针被删除,而目前的base class 有个non-Virtual 析构函数。这就带来了灾难,因为C++明白指出,当derived class 对象经由一个base class 指针被删除,而该base class 带着一个non-Virtual 析构函数,其结果是未定义的--实际导致derived class 没有被销毁,而导致内存泄漏。

      针对上诉问题,提出解法一:

      

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
class TimeKeeper   //base class
{
public:
 TimeKeeper();
  virtual ~TimeKeeper();
};
class AtomicClock: public Timekeeper{...};   //原子钟
class WaterClock: public TimeKeeper{...};   //水表

TimeKeeper *ptk=getTimeKeeper();   //从TimeKeeper继承体系获得一个动态分配对象
...                         //运用之
delete ptk;                //释放它,避免资源泄漏

      像TimeKeeper这样的base classes除了析构函数之外通常还有其他Virtual 函数,因为Virtual 函数的目的是允许derived class 的实现得以客制化。任何class 只要带有Virtual 函数都几乎确定应该有一个Virtual 析构函数。

    ///这条建议并不适用于所有的类。。。

      我们来看看下面两个例子。

 C++ Code 
1
2
3
4
5
6
7
8
class Point  //一个二维空间点
{
public:
  Point( int xCoord, int yCoord);
  ~Point();
private:
   int x,y;
}
      这个例子告诉我们,当一个class不企图被当做base class,不应该令其析构函数为Virtual。原因在于虚表指针比较浪费空间。

    欲实现出Virtual函数,对象必须携带某些信息,主要用来在运行期决定哪一个Virtual函数该被调用,这份信息通常是由一个所谓的vptr(virtual table pointer)指针指出。vptr指向一个由函数指针构成的数组,称为vtbl(virtual table),每个带有Virtual函数的class 都有一个相应的vtbl。

      因此很多人的使用心得是:只有当class 内含至少一个Virtual函数,才为它声明Virtual析构函数。

      还有另外一个例子;

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
class SpecialString: public std::string
{
...
};
SpecialString *pss= new SpecialString( "nihao");
std::string *ps;
...
ps=pss;
...
delete ps;  //未定义,现实中*ps的SpecialString资源会泄漏
            //因为SpecialString析构函数没有被调用
         这个例子告诉我们,不要尝试继承STL容器如vector、list、set、tr1::unordered_map,因为这些类都是不带Virtual析构函数的class。

       最后我们来看看一个抽象类的例子。所谓的抽象类既是令class 带一个pure Virtual 析构函数,所谓的抽象类也就是不能被实体化的class。由于抽象类总是企图被当做一个base class来用,而有由于base class应该有个Virtual析构函数。为了你希望他称为抽象的那个class 声明一个pure Virtual 析构函数,下面是一个例子;

     

 C++ Code 
1
2
3
4
5
6
7
8
class AWOW
{
public:
    virtual ~AWOW()= 0;   //声明pure Virtual析构函数。
}

AWOW::~AWOW()
{} //pure Virtual 析构函数的定义
  

     你必须为这个pure Virtual 析构函数提供一份定义。

     因为析构函数的运行方式是,最深层派生的那个class 其析构函数最先被调用,然后是其每一个base class 的析构函数被调用。


       根据上面的例子我们可以做出总结了:

请记住:

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

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


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

      析构函数中抛出异常会导致程序无法销毁内存空间中的对象,这样会导致内存泄漏等问题。但是我们的析构函数必须执行一个动作,而该动作可能会在失败时抛出异常。假设你使用一个class、 负责数据库连接:
 
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class DBConn    //这个class用来管理DBConnection 对象
{
public:
    ... 
    ~DBConn()    //确保数据库连接总是被关闭。
    {
     db.close();
    }
private:
   DBConnection db;
};
//因此用户可以写出如下的代码:
DBConn dbc(DBConnection::create());
//开启一个区块block 建立DBConnection对象并交给DBConn对象管理
//通过DBConn的接口,使用DBConnection对象在区块结束点,DBConn 对象被销毁,因为自动调用close()
       问题在于,如果该调用导致异常,DBConn析构函数会传播异常,也就是允许它离开这个析构函数,那就会造成问题,难以驾驭。
     如何解决这个问题呢:
    解法一:
   如果close 抛出异常就结束程序。通常通过调用abort完成。
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
DBConn::~DBConn()
{
     try
    {
        db.close();
    }
     catch(...)
    {
         //制作运转记录,记下对close的调用失败
        std::abort();
    }
}
    调用abort可以抢先制“不明确行为”为死地。
 
解法二:
       吞下因调用close而发生的异常:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
DBConn::~DBConn()
{
     try
    {
        db.close();
    }
     catch(...)
    {
         //制作运转记录,记下对close的调用失败
    }
}

     这种做法的好处在于,程序可以继续运行。程序必须能够可靠地执行,即使在遭遇并忽略一个错误之后。
问题是两者都无法对“导致close抛出异常”的情况作出反应。
      解法二:
    下面还有另外一种比较有优势的解法就是,采用双重保险对异常进行处理。马上上代码:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class DBConn
{
public:
    ...
     void close()   //提供给客户使用的新函数
    {
        db.close();
        closed =  true;
    }
    ~DBConn()
    {
         if(!closed)
        {
             try
            {
                db.close();   //关闭连接
            }
             catch(..)
            {
                 //制作运转记录,记下对close的调用失败
                 //关闭失败,结束程序或者吞下异常。
            }
        }
    }
private:
    DBConnection db;
     bool closed;
}
   把调用close的责任从DBconn析构函数上移到DBConn客户手上,上面采用了双保险的操作。

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


条款9:绝不在构造和析构过程中调用Virtual函数。

   在构造函数下调用虚函数如下所示,会发生意想不到的事情。

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// never_call_vir.cpp :
#include  "stdafx.h"
#include <iostream>
using  namespace std;
class Base    //基类
{
public:
    Base()
    {
        print();   //构造函数调用虚函数
    }
     virtual  void print()   //虚函数
    {
        cout <<  "Base()~print()" << endl;
    }
};

class Derived:  public Base   //派生类
{
public:
    Derived()
    {
        cout <<  "Derived()" << endl;   //构造函数
    }
     virtual  void print()
    {
        cout <<  "Derived()~print()" << endl;   //重构虚函数
    }
};


int _tmain( int argc, _TCHAR *argv[])
{
    Derived der;   //调用派生类函数
     return  0;
}
        首先有个基类base,其次Derived是其子类,print虚函数覆盖了父类函数。那么当 Derived   der创建的时候,会调用哪个print呢?下面上输出结果:

         

           从生成的结果可以看到,基类Base的print。基类型构造期间virtual函数绝对不会下降到派生类型阶层,也就是说virtual函数并不是virtual的。由于base class构造函数的执行更早于derived class构造函数,当base class构造函数执行时derived class的成员变量尚未初始化。如果此期间调用的virtual函数下降至derived class阶层,要知道derived class的函数几乎必然取用local成员变量,而那些成员变量尚未初始化。这将是一张通往不明确行为和彻夜调试大会串的直达车票。“要求使用对象内部尚未初始化的成分”是危险的代名词,所以C++不让你走这条路。

            其实还有比上述理由根本的原因:在derived class对象的base class构造期间,对象的类型是base class而不是derived class。不只virtual函数会被编译器解析至(resolve to)base class,若使用运行期类型信息(runtime type information,例如dynamic_cast(见条款27)和typeid),也会把对象视为base class类型。本例之中,当derived 构造函数正执行起来打算初始化“derived 对象内的base class成分”时,该对象的类型是base 。那是每一个C++成分(见条款1)的态度,而这样的对待是合理的:这个对象呢你的“derived 专属成分”尚未初始化,所以面对它们,最安全的做法就是视它们不存在。对象在derived class构造函数开始执行前不会成为一个derived class对象。

        相同道理也适用于析构函数。一旦derived class析构函数开始执行,对象内的derived class成员变量便呈现未定义值,所以C++的任何部分包括virtual函数,dynamic_casts等等也就那么看待它。

      但是假设这样的调用virtual函数放在non-virtual函数里,而non-virtual函数则在构造函数和析构函数里调用,那么编译器很难检测到这样的情况。假设X定义的是pure virtual,,但运行时则会crash(如果你很愿意看的话)。

如下代码,就会出现问题

      
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// never_call_vir.cpp : 定义控制台应用程序的入口点。
#include  "stdafx.h"
#include <iostream>
using  namespace std;
class Base
{
public:
    Base()
    {
        Init();
    }
     void Init( void)
    {
        Print();
    }
     virtual  void Print( voidconst =  0;   //纯虚函数
};

class Derived :  public Base
{
public:
     virtual  void Print( voidconst
    {
        cout <<  "Derived Print" << endl;
    }
};

int _tmain( int argc, _TCHAR *argv[])
{
    Derived der;
     return  0;
}


从图中可以看到,在pure virtual 函数被调用的时候,程序是会终止的。

所以确定你的构造函数和析构函数都没有(在对象被创建和销毁期间)调用virtual函数,而它们调用的所有函数也都服从同一约束


解决办法一种是在类 Transaction内将logTransaction函数改为non-virtual,然后要求继承类构造函数传递必要信息给Transaction构造函数,而后那个构造函数便可安全调用non-virtual logTransaction。代码如下:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Transatcion
{
public:
     explicit Transaction(cosnt std::string &logInfo);
     void logTransaction( const std::string &logInfo)  const//non-virtual
};

Transaction::Transaction( const std::string &logInfo)
{
    logTransaction(logInfo);                                 //non-virtual调用
}


class BuyTransaction:  public Transaction
{
public:
    BuyTransaction(parameters)
        : Transaction(createLogString(parameters)) {}         //将log信息传递给基类构造函数
private:
     static std::string createLogString(parameters);
};
请记住:
      在构造和析构期间不要调用Virtual函数,因为这类调用从不下降至derived class(比起当前执行构造函数和析构函数的那一层)

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

  为了实现“连锁赋值”,赋值操作符必须返回一个reference to *this指向操作符的左侧实参。
int x,y,z;
x=y=z;
代码实现如下所示:
 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Widget
{
public:
    ...
    Widget &  operator=( const Widget &rhs)
    {
     ...
      return * this;   //返回左侧对象
    }
    Widget &  operator+=( const Widget &rhs)   //这个协议适用于+=、-=,*=等等。
    {
     ...
      return * this;
    }
    Widget &  operator=( int rhs)
    {
     ...
      return * this
    }
}
   
请记住:
   令赋值(assignment)操作符返回一个reference to *this.

条款11 在operator=中处理‘自我赋值’

 在自我赋值中可能会发生对象被自己赋值给自己的情况,因此需要规避这种问题。这种自我赋值的情况会导致资源的消耗,还带来诸多不安全的因素。

      解决方案是:证同测试。如下所示:
 C++ Code 
1
2
3
4
5
6
7
Widget& Widget:: operator=( const Widget & rhs)
{
  if( this==&rhs)  return * this;   //证同测试 如果是自我赋值,就不做任何事
  delete pb;
 pb= new Bitmap(*rhs.pb);
  return * this;
}

这个版本的赋值函数基本上是可以接受的,但不见的是安全的,因为当new产生异常时pb依然是个不确定的指针。


第三个版本:

 C++ Code 
1
2
3
4
5
6
7
Widget &Widget:: operator=( const Widget &rhn)
{
    bitmap *pOrig = pb;
    pb =  new bitmap(*rhn.pb);
     delete pOrig;
     return  this;
}

这个函数在开始时用pOrig记录了pb,当new没有异常时我们在把Pb原来的指向空间释放掉,从而提高了安全性。

实现赋值函数还有另外一个思想,即copy and swap技术

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
class Widget
{
     void swap( const Widget &rhs);
}

Widget &Widget:: operator=( const Widget &rhs)
{
    Widget temp(rhs);   //防止改变rhs
    swap(temp);
     return * this;
}
当然我们也可以by value 传递参数
 C++ Code 
1
2
3
4
5
6
Widget& Widget:: operator=( const Widget rhs)  //按值传递是实参的一个copy   
{
    swap(temp);
     return * this;

}

请记住:
    确保当对象自我赋值是operator=有良好行为。其中技术包括比较“来源对象”和目标对象的地址,精心周到的语句顺序,以及copy-and-swap。
      确保任何函数如果操作一个以上的对象,而起哄多个对象时同一个对象时,其行为仍然正确。


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

1.在类里涉及到对象拷贝的函数要复制类里每一个成分copy构造函数以及copy assignment操作符

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
class Test
{
private:
    string name;

public:
    Test( const Test &rhs): name(rhs.name) {}
    Test & operator = ( const Test &rhs)
    {
        name = rhs.name;
         return  this;
    }
};

     这样如果类里再添加一个成员变量时,就得需要修改这两个函数,不然新成员不会被拷贝的新对象中,而且
编译器一般不会报错

2.在继承中,注意base类的拷贝

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
class derve:  public Test
{
private:
    string derveStr;
public:
    derve( const derve &rhs): derveStr(rhs.derveStr) {}
    derve & operator = ( const derve &rhs)
    {
        derveStr = rhs.derveStr;
         return  this;
          
    }
};
         虽然继承类里看到了两个拷贝函数,但忘了base类的拷贝,继承类对象里也包含了base类的一份副本,所以继承类对象拷贝时也需要拷贝base的副本,如果继承类里没有调用base 的拷贝函数,会使用default的拷贝函数,如上面的继承类,拷贝时使用的是base类的default拷贝函数,则不能做到拷贝每个成分,在定义继承类的时候可以调用base类的自定义拷贝函数

      

 C++ Code 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class derve:  public Test
{
private:
      string derveStr;
public:
      derve( const derve &rhs): derveStr(rhs.derveStr), Test(rhs) {}
      derve & operator = ( const derve &rhs)
      
    {
            Test:: operator = (rhs);
            derveStr = rhs.derveStr;
            return  this;
          
    }
};

3.如果两个拷贝函数有重复的代码,可以定义一个成员函数(如init()),将重复的代码放到这个成员函数中,然后拷贝函数再调用这个init()函数,这两个拷贝函数不要互相调用,这样不太好,应该把相同代码放到第三方函数中


请记住:

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

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

   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值