读书笔记:Effective C++ 炒冷饭 - Item 9 开幕谢幕勿用虚函数

读书笔记:Effective C++ 炒冷饭 - Item 9 开幕谢幕勿用虚函数

[原创文章欢迎转载,但请保留作者信息]

Justin 于 2009-11-08

其实是说不要在构造/析构函数里调用虚函数。

道理也是不难但也不那么明显:
虚函数 拥有虚函数的类就有虚表,虚表可能会引发子类相应虚函数的调用,在这些调用中有可能对某些子类对象成员的访问。
在构造一个子类对象的时候可以分成这几步:开始构造父类部分 -> 完成父类部分并开始构造子类部分 -> 完成子类部分(完成整个构造工作)
析构一个子类对象的时候:开始析构子类部分 -> 子类析构完毕并开始析构父类部分 -> 完成析构父类部分(完成整个析构工作)

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

大师说的是道理,我大概只能联想。

一个模特的工作是穿上服装拍照。拍照这个技术活可以看作是这个对象的一个虚函数。在女模的一天之始,美丽动人的她需要先起床(基类构造),习惯裸睡的她还需要到更衣室去穿衣服(子类构造)。然后她对摄影师说“准备好啦”(构造完毕),就开始一天的工作:被拍。到了晚上女模回家(开始析构),先要卸掉身上的束缚(子类析构),最后上床(基类析构),当然,是不是一个人上床不属于我联想的范围。
作为拍照这个虚函数来说,女模睡觉的时候是不能做的,这个时候她还没有摆姿势的意识;女模起床以后也是不能拍的,因为衣服还没穿上(OK……我承认这个例子不是很好);穿好衣服,她说“准备好啦”,拍照就可以开始了。同样道理,最迟你也只能在她晚上说拜拜之前拍完,到回家开始脱衣服睡觉觉之后,你就要等到明天再工作了。

然而问题往往没有那么简单明了:很多时候一个构造/析构函数里会调用一些普通的函数,而这些函数又可能间接地调用了虚函数。在这种情况下,傻大粗的编译器不会报错。到了灾难发生时只有程序员可怜兮兮地抓臭虫……
那么有没有办法能够确保虚函数不会在对象构造/析构过程中被调用呢?别看我,大师开讲了:

方法之一就是用参数传递代替虚函数机制。把可能被构造函数调用的虚函数改成非虚函数,然后让父类的构造函数将需要的信息/数据通过参数传递给子类构造函数。对于析构函数,做法也类似。码了那么多字,要不,来段代码消化消化?

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限定的原因。因为静态函数可以在对象的初始化阶段就开始工作,更详细的描述看这里。



---【补充笔记】---
楼下有好心朋友提醒:对象调用的虚函数是“本地版本”。
于是把书上的例子修改了一下:
class   base {
public :
   
base (){
      saysomething();
   }
       
   
virtual   void  saysomething(){ 
      printf(
" hello from base\n " ); 
      dosomething();
   }

   
virtual   void  dosomething()  =   0 ;
};
       
class  derived :  public   base {
public :
   derived(){
      saysomething();
   }

   
virtual   void  saysomething(){
      printf(
" hello from derived\n " );
      dosomething();
   }

   
virtual   void  dosomething(){
      printf(
" well, actually we never get here\n " );
   }
};
      
main(){
   derived D;
}
执行的结果是(Win XP, DEV C++ 4.9.9.2)
hello from  base ...
pure 
virtual  method called

This application has requested the Runtime to terminate it 
in  an unusual way.
Please contact the application
' s support team for more information.
可以看出的是,Jackie和1111说的是对的(至少在上面的环境下):“子类的虚表在子类的构造函数时生成,所以在父类的构造函数中调用虚函数使用的是父类的版本。”
子类和父类对象都会有自己的虚表,里面安置的是自己版本的虚函数实现。所以, 在父类构造函数中 调用的saysomething()就是父类的实现版本;而调用纯虚函数dosomething()的结果就是上面的错误,但是并不会调用子类部分的实现:因为此时对象还是个父类对象。

在记笔记的时候读的是第三版的英文原版,不知道是自己的理解出错还是由于历史原因使得书中的描述已经不存在了。不过还是非常感谢C++博客那么多热心的朋友不吝赐教。这也正是我在这里记录读书笔记的用意:即督促自己认真读书记录笔记,又让自己的理解接受更多的检验和指点。
还是那句老话:尽信书不如无书啊……


你可能感兴趣的:(读书笔记:Effective C++ 炒冷饭 - Item 9 开幕谢幕勿用虚函数)