sherrylso

C++博客 首页 新随笔 联系 聚合 管理
  18 Posts :: 0 Stories :: 124 Comments :: 0 Trackbacks

四、c++中的多态规则。
一) c++中函数动态绑定规则。
看下面的例子:

class Window
{

public:
  virtual 
void  oops()
  
{
    cout
<<"Window oops"<<endl;
  }

public:
  
int height;
  
int width;
}
;
class TextWindow : public Window
{

public:
  virtual 
void  oops()
  
{
    cout
<<"TextWindow oops"<<cursorLocation<<endl;
  }

public:
  
int cursorLocation;
}
;

main()
{
  Window win;
  Window
* tWin;
  TextWindow 
* tWinPtr;

  tWinPtr 
= new TextWindow;
  tWin 
= tWinPtr;
  
  win.oops();
  tWin
->oops();
}


类TextWindow继承与类Window。我想程序运行的结果,大多数熟悉C++的人都会知道,
win.oops()最终调用的是父类oops函数,而tWin->oops()调用的是子类TextWindow的函数。
通过这个例子,我们先总结一下c++中的多态调用规则
第一、对于指针和引用类型,当消息调用的成员函数有可能被重写时,最终被选择调用的成员函数由消息接收者的动态类型确定(注意:在OO概念中,对某个对象成员函数进行调用,常常称为给该对象发送消息,该对象就是消息的接收者)。
      如上例:tWin->oops(),由于tWin的动态类型是子类TextWindow,而不是Windows,所以tWin->oops()调用的是子类TextWindow的函数。
二、对于其它的变量,对虚拟函数调用绑定完全由该变量的静态类型确定(即该变量的声明),而不是该变量的真实类型确定。
     如上例:win.oops(),由于win的声明类型为Window类,所以其结果调用的是父类oops函数。
二) 探讨。
     接下来,我们要看的问题是,在c++中,为什么对于多态规则(或者说是动态函数绑定规则),做出了两中不同的划分,即:只有指针与引用类型,才进行函数的后期动态绑定,也就是多态。这或许也是许多c++初学者非常迷惑的地方。这种规则的不一致性,的确给c++的语法造成一定的复杂性。而这在Java,或者C#中是没有的,后面我们会涉及到。
      我们先来看例子。 

void f()
{
  Window  win;
  Window
* tWinPtr;
  
  tWinPtr 
= new TextWindow;
  win     
= *tWinPtr;//what's problem happen
   
  win.oops(); 
//what's problem happen
  tWinPtr->oops();
}


      在这里,如果我们假设,c++的函数动态绑定规则是一致的,看看会发生什么问题???
      现在win被声明为Window类型,然而其真实的类型为TextWindow(因为win=*tWinPtr),由于我们的假设,win现在是允许进行动态函数绑定的,所以当执行win.oops()时,实际上是调用子类TextWindow的成员函数。
     现在,我们有必要来审视一下win变量的内存布局。由于win变量是在栈上声明的变量,其内存也是从栈进行分配(这是c++从c语言那里继承过来的优良特质,从栈上分配内存空间比动态分配内存有更好的执行速度),c++标准规定:给win变量分配内存空间的大小,由其静态的类型确定,即应该是Window类所使用的内存空间大小。在这种情况下,当执行win=*tWinPtr时,什么会发生?如下图:

在默认的拷贝构造函数情况下,信息会出现丢失,这就是著名的slicing off现象。结果,变量cursorLocation在win的内存空间里丢失了。然而,问题是:在我们假设下,我们要求win.oops()导致TextWindow的成员函数调用,而在这个函数中,访问到的cursorLocation变量是不存在!win.oops()调用将导致内存违例!
      到这里,我们可以总结一下:c++标准基于的其特定的内存分配规则,给出了以上,我们在前一节总结出的函数动态绑定规则。
三) 深入。
      当然,我们也可以说,c++也可以通过改变其内存分配规则,来给出一个一致性的函数动态绑定规则。比如:可以考虑在给win变量分配内存空间时,考虑其所有子类需求,然后分配最大数量的内存空间给win变量。这种做法可行性很差,对于编译器而言,需要扫描整个程序(确定该类的所有子类),才能确定最大的内存空间是多少。在使用类库或者框架的情况下,会引起整个类库,框架的重新编译,这是得不偿失的!而这种做法,在oo的语言中,基本上是没有的。这也是c++不得不基于其现有的内存管理机制,而对多态规则作出的不一致的解释。
    对于这个c++现有的内存管理机制,我们如果从另外角度去理解的话,是很合理的。当win=*tWinPtr发生
时,我们可以类似地认为:好比一个float类型的数赋给了一个interger类型的变量,其结果当然是float的值被截断了。
    我们再来看其它语言,Java(或者C#)是怎么解决的。
    最重要的一点是,在Java(C#)中只有引用的概念,所以在栈上声明的类的变量,只需要分配一个指针大小的内存空间就行了,而不需要给该变量分配空间来保存变量内容本身,这其实就是我们现在看到的c++中指针和引用的情况。
    

 

posted on 2008-01-05 23:12 爱上龙卷风 阅读(3126) 评论(8)  编辑 收藏 引用

Feedback

# re: c语法背后的故事(二) [未登录] 2008-01-06 12:21 ZZZ
是有点晦涩  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-07 22:53 abettor.org
看来这个容易理解一些,呵呵。  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) [未登录] 2008-01-10 11:10 Composition
sherrylso兄,
void f()
{
Window win;
Window* tWinPtr;

tWinPtr = new TextWindow;
win = *tWinPtr;//what's problem happen

win.oops(); //what's problem happen
tWinPtr->oops();
}
中win.oops(); 仍然会调用父类的方法,因为编译器生成的拷贝构造函数不会拷贝vptr。希望你check一下。
这里仅仅切割了子对象,但是切割后的对象是合法的父对象,除非你的设计不合理,违反了LSP。  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-11 22:04 爱上龙卷风
@Composition
我的论述的前提是:“假设:c++的函数动态绑定规则是一致的“
搞不清楚你的观点是什么?
  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-14 13:35 Composition
我的意思是不管子对象怎么切割,win.oops(); 永远会调用父类的函数,绝不会因为切割调用子类的方法。既然不调用子类的方法,自然不会出错。你可以在编译器实验一下,你这个例子会输出Window oops xxx,没有错误。
不管怎么假设,通过对象调用成员函数都是编译时绑定,永远不可能调用到子类的成员函数。
win = *tWinPtr;这句会调用Window的拷贝构造(编译器生成),但是拷贝构造不会拷贝对象的vptr。而且就算拷贝了,通过对象调用成员函数也不通过vptr找vtable,而是直接调用对象类型对应到成员函数(编译后成为全局函数)。你这里假设“c++的函数动态绑定规则是一致的”,但别忘了,vptr不拷贝,这是C++实现机制,怎么调用都是父类的成员函数,不会有多态。
如果你做出拷贝构造会拷贝vptr+对象调用会查找vtable的假设,那么你的论述应该合理。但是这么假设个人认为意义不大。  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-14 21:39 爱上龙卷风
@Composition
1) 首先,我要论述的是多态规则,不是只有C++有。很多面向对象的语言都有的特性。Java, C#, Smalltalk。你不必要局限于C++本身。
2) 其次,你说的,我都赞同,这就是c++的内存管理机制。
3) 基于c++的内存管理机制,推出了现有的C++多态规则。
4)我是在讨论,C++语法为什么复杂的背后原因。也就是怎么去更好地理解现行的语法。
  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-14 21:41 爱上龙卷风
@Composition
再者,如果C++改变了现有的内存管理机制,即我提到的:可以考虑在给win变量分配内存空间时,考虑其所有子类需求,然后分配最大数量的内存空间给win变量。
是可以做到多态规则的一致性  回复  更多评论
  

# re: c++晦涩语法背后的故事(二) 2008-01-15 12:14 Composition
我明白你的意思了,不过我也没有完全拘泥于C++,但是C++的多态实现确实与其他语言很不同。如Python,Ruby之类语言通过给类建立哈希表来实现多态。Java,C#之类应该是通过元数据。而且只有C++才有切割问题,其他语言在堆上分配就不存在切割了。
至于你的例子f()我觉得可以改成如下的例子更能说明问题,而且也不必做过多假设:
void f()
{
Window win;
TextWindow textWin;
Window* tWinPtr;

tWinPtr = new TextWindow;
memcpy(reinterpret_cast<char*>(tWinPtr) + 4,
reinterpret_cast<char*>(&win) + 4,
sizeof(win) - 4); //don't change vptr

tWinPtr->oops(); //what's problem happen
}
这个例子会发生错误,即使运行不出错,逻辑也是错的。
至于你对“可以考虑在给win变量分配内存空间时,考虑其所有子类需求,然后分配最大数量的内存空间给win变量。”的论述很透彻,我同意。  回复  更多评论
  


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