随笔-19  评论-2  文章-0  trackbacks-0

========================
Effective C++   构造、析构、赋值运算
书作者:Scott Meyers
原笔记作者:Justin
========================


Item 5、6   :C++默认编写的函数
-----------------------------------------
 tag: 默认拷贝函数  copy assignment
 
 如果没有定义类的构造函数/拷贝函数/拷贝构造函数/析构函数,编译器会自作主张帮你定义这些函数,而且还是public外加inline的。(估计这是最冷的冷饭了)
 帮写拷贝函数时仅限于帮你处理一下诸如int的内嵌类型(build-in type)以及自身就有拷贝函数的类对象,再复杂一点的就搞不定了。

 有时候需要禁止某个类对象的拷贝操作,就要避免以上函数的自动生成,需要自己定义这些函数,并且有以下特性
 private
 只是声明,不给予实现(give no implementation)
 当然要是还是觉得麻烦,干脆直接继承Boost的noncopyable(自己写一个类似的也可以),就可以不费吹灰之力的获得不能拷贝的特性。

Item 7  :为多态基类声明virtual析构函数
--------------------------------------------
 tag: 多态(polymorphism) 虚函数  virtual function  析构函数

 ·polymorphic base classes 应将析构函数定义为虚函数(virtual function)
 ·当class内至少含一个virtual函数时,才声明virtual析构函数。
 
 否则在通过基类指针析构一个子类对象的时候,因为没有虚表(V Table)对析构函数的指引,对象的基类部分是被毁了,对象的子类部分却没办法得到析构(没有了虚表中的说明它根本不知道有子类的析构函数来析构子类部分!),从而导致内存泄漏。

 virtual函数实现:
  要实现virtual,vptr(virtual table pointer)指针指向一个向函数指针构成的数组,称为vtbl(virtual table);
  带有virtual函数的class都有一个对应的vtbl,对象调用virtual函数时,实际被调用的函数取决于该对象的vptr所指的那个vtbl(在其中寻找适当的函数指针)
 
 假设基类指针在析构时要做的事情是奉命救人:指针啊指针,XX地发大水了,你赶紧去把张三一家人(子类对象)救出来。
 指针拿着指令(析构函数)就找到了张三(张三家家长,子类对象的基类部分):张三是吧,你一家人都在这里了吧,跟我走吧。(析构吧,回空余内存里去吧@#¥%)
 如果张三已经没有其他亲戚了,他就是张三家的全部人(这个对象其实就是基类对象)。很简单,直接跟着指针走就完事了。
 如果张三还有个小弟叫张三疯(张三家除张三外的一部分,即子类对象的子类部分),张三就必须拿着族谱(V Table)跟指针说哎呀这是我弟啊,把他也析构了吧,张三家对您感恩不尽……也很简单,一并带走就完成任务了(完整析构/释放了整个对象)
 如果,张三没了族谱,记不得有个张三疯弟弟,傻乎乎的跟了指针走,大家也以为张三一家都被救出来了。灾区里就只剩下张三疯一个人在疯跑了(memory leak)

 另一方面,如果不准备把某个类用作多态中的基类,就没必要定义析构函数为虚函数。这样除了增加代码大小开销外(因为需要空间存放虚表),没有任何好处。(这个倒是很多书上没说过的)

 如果张三家本来就只有张三一个人,他就没必要还要带着个空白的族谱了。逃命的时候能扔就扔嘛。


 最后提到的一点和抽象类有关。抽象类是有纯虚函数的类,只能作为接口,不能有自己的对象。在设计软件框架的时候会有需要用到抽象类的时候。可是如果有这么一个类,需要设计为抽象类,但是却找不到一个成员函数可以拉去做纯虚函数,那么这个时候就把析构函数定义为纯虚函数好了。只是同时还要给它一个空的实现。

 class::~class() {}


 

Item 8 :别让异常逃离析构函数
-------------------------------------------
 tag:异常 try catch  析构函数
 
 ·在析构函数中不能抛出异常。 若被析构函数调用的函数可能会抛出异常,析构函数应该捕捉任何异常,并吞下(不传播)或结束程序。
 ·若一个操作可能出现异常,客户需要对这个异常作出反应。class应提供一个普通函数来执行该函数,而不是在析构函数内调用。

 异常不像函数调用,一旦抛出(throw),就回不来了。
 如果在析构函数中抛出了异常,有一部分释放/摧毁对象成员的操作可能就无法进行。因为某个异常,导致了所在的析构函数无法完成全部析构任务。

 可是要是真的需要对析构动作中出现的错误/异常进行处理咋办?书中自有解决方案:从差的到好的。

 在析构函数内布置catch,一旦发生异常就会被捕获,然后简单调用std::abort自杀
  点评:干脆是干脆了,但是这样猝死会不会有点太突然?
 也是在函数内布置catch,但是遇到异常就把它直接吃到肚子里(大师语:Swallow the exception)。
  点评:一般不该这样处理,毕竟发生了不好的事。但如果真的想要程序继续带伤上阵,也确定这样不会有问题,那也不会有人有意见。
  
 除了在函数里布置catch,并采用以上任一方法,另外实现一个可供用户调用的函数接口,用来处理这些有可能出错的析构工作。
  点评:大师给予这个方案高度的评价,因为这样不但有以上两种方法的效果,还给用户一个参与处理异常的机会(调用接口函数)。如果用户没有(或者忘记)用该函数处理析构的动作,那么析构函数也会自觉挑起这个任务。而这个时候如果还出问题,用户也没有什么理由来责怪了:是你自己不想要管的!



Item 9 :不在构造和析构过程中调用 virtual 函数
-----------------------------------------------------
 tag:
 
 拥有虚函数的类就有虚表,虚表可能会引发子类相应虚函数的调用,在这些调用中有可能对某些子类对象成员的访问。
 在构造一个子类对象时:   开始构造父类部分 -> 完成父类部分并开始构造子类部分 -> 完成子类部分    (完成整个构造工作)
 析构一个子类对象的时:  开始析构子类部分 -> 子类析构完毕并开始析构父类部分 -> 完成析构父类部分(完成整个析构工作)

 在构造函数的第一步,子类对象成员还不存在,调用虚函数有可能会访问不存在的子类成员;
 哪怕到了第二步,因为子类对象还没有完全构造完毕,此时调用虚函数也是危险的。事实上在整个构造过程中,该对象都被视作一个父类对象。
 反过来也是同样道理,在析构函数的第一步,子类成员已经开始销毁,不能调用虚函数;到了第二步,整个子类部分都没有了,更不能用虚函数了。
 而在整个析构过程中,该对象也是被看作是父类对象的。
 
 确保虚函数不会在对象构造/析构过程中被调用:

 方法之一就是用参数传递代替虚函数机制。
 把可能被构造函数调用的虚函数改成非虚函数,然后让父类的构造函数将需要的信息/数据通过参数传递给子类构造函数。
  Class Parent
  {
   public :
      Parent();
      Parent( const  std:: string &  WordsFromChild){
          DoStuff(WordsFromChild);
          // ..
      };
      void  DoStuff( const  std:: string &  WordsFromChild);
  }   
    Class Child :  public  Parent
    {
  public :
      Child( /**/ /* some parameters here */ ) : Parent(TellParent( /**/ /* some parameters here */ )) {
         // ..
      };
  private :
      static  std:: string &  TellParent( /**/ /* some parameters here */ );
    }
     也许看到这里会想:要是TellParent()中访问了未被初始化的子类成员呢?那不也是一样有问题么?
     注意,这就是为什么前面有个static限定的原因。因为静态函数可以在对象的初始化阶段就开始工作,更详细的描述看这里。
 子类的虚表在子类的构造函数时生成,所以在父类的构造函数中调用虚函数使用的是父类的版本。
 子类和父类对象都会有自己的虚表,里面安置的是自己版本的虚函数实现。



Item 10-12 :  拷贝运算符
--------------------------
 tag:assignment operator(赋值运算符)  自我赋值  copying函数
 
 ·令赋值(assignment)操作符返回一个reference to *this。 可实现(a=b=c)。
 ·处理自我赋值
 ·构造函数用来初始化新对象,而 assignment操作符只施行于已初始化对象身上。
 ·copying函数应该确保复制“对象内所有成员变量”和“所有 base class成分”
 
 1、
  在函数入口检查是否属于自拷贝(例如:检查指针是否指向同一片内存),如果是,啥也不干直接返回。否则属于正常情况的拷贝。
  这样解决了self-assignment-unsafe的问题,但是没能避免exception-unsafe。
  
 2、
  第二种方法比较简单,只是整理一下指令的顺序。但是却同时解决了自赋值和抛出异常带来的问题。继续无耻的抄写代码一段: 

   Widget&  Widget::operator=(const Widget& rhs)
   {
      Bitmap *pOrig = pb;            // remember original pb
      pb = new Bitmap(*rhs.pb);      // make pb point to a copy of *pb
      delete pOrig;                  // delete the original pb
      return *this;
   }

  这样的做法在解决以上两个问题的同时却也降低了执行的效率:不论什么情况,这个赋值函数都要创建一个新的Bitmap对象。
  第一种方法的额外支出:判断语句必然地引入了程序的分支(branch),于是指令的预取(prefetch)、缓冲(caching)、流水线处理(pipelining)的效率就会被降低。
 3、
  Copy And Swap。改赋值为交换。

   void swap(Widget& rhs);   //交换*this 和rhs的数据;

   Widget&  Widget::operator=(const Widget& ths)
   {
    Widget temp(ths);  //为rhs数据制作一份副本
    swap(temp);    //将*this数据和上述副本的数据交换。
    return *this;
   }
   
   Widget&  Widget::operator=(Widget rhs)    // rhs is a copy of the object
   {                                        // passed in — note pass by val
      swap(rhs);                             // swap *this's data with the copy's
      return *this;
   }利用参数传值,隐性的构造了一个Widget对象。然后将新对象和本对象中的数据成员交换,达到为本对象赋值的效果。
  新的临时对象在跳出函数后自动销毁。刚才说的两个unsafe,都不会存在。
  这样开销较大了,无论什么时候都要构造新的对象。用swap来完成赋值的做法有点逻辑混淆。但这样做很有可能让编译器生成更有效率的代码。

 ---------------------------
  如何保证在赋值/拷贝的时候能够将所有的成员完整拷贝过去?
 对于简单的数据成员,编译器自动生成的拷贝函数可以保证一个不漏都帮你拷贝;
 如果是比较复杂的成员(比如说指向一片内存空间的指针),编译器就没有足够的智商把这些成员拷贝到另外一个对象中去了。

 在增加类成员以后记得更新拷贝函数,以免拷贝不完全。
 子类的拷贝函数把自己的成员都拷贝了,但是却漏了把父类对象的成员拷贝到新的对象中。 在子类的拷贝函数中调用父类的拷贝函数

   Widget&  Widget:: operator   =  (Widget src) 
  {
     swap(src);                                // copy-and-swap
     WidgetParent:: operator   =  (src);       // invoking the parent's copy assignment operator
     return   * this ;
  }

  最后的最后,通常来说在拷贝函数和拷贝构造函数中的实现大多相同,
 不要在拷贝函数中调用拷贝构造函数或者反之。如果真的需要避免代码的重复,大可定义一个私有的函数来负责前面两者相同的部分。

posted on 2010-03-15 22:44 Euan 阅读(464) 评论(0)  编辑 收藏 引用 所属分类: C/C++

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理