多态(Polymorphism)的贯彻机制(上)--C++篇

  
多态(Polymorphism)是面向对象的中坚概念,本文为C++为条例,讨论多态的切实可行实现。C++中多态可以分成基于继承与虚函数的动态多态以及基
于模板的静态多态,如果没有特别指明,本文中出现的多态都是依靠前者,也尽管是冲继承与虚函数的动态多态。至于什么是多态,在面向对象中怎样使用多态,使用
多态的利益等等问题,如果大家感谢兴趣的话,可以寻找本面向对象的写来探视。
    为了方便说明,下面举一个简约的用多态的事例(From [1] ):

class Shape
{
protected:
  int m_x;    // X coordinate
  int m_y;  // Y coordinate
public:
  // Pure virtual function for drawing
  virtual void Draw() = 0;  

  // A regular virtual function
  virtual void MoveTo(int newX, int newY);

 // Regular method, not overridable.
  void Erase();

  // Constructor for Shape
  Shape(int x, int y); 

 // Virtual destructor for Shape
  virtual ~Shape();
};

// Circle class declaration
class Circle : public Shape
{
private:
   int m_radius;    // Radius of the circle
public:
   // Override to draw a circle
   virtual void Draw();    

   // Constructor for Circle
   Circle(int x, int y, int radius);

  // Destructor for Circle
   virtual ~Circle();
};

// Shape constructor implementation
Shape::Shape(int x, int y)
{
   m_x = x;
   m_y = y;
}
// Shape destructor implementation
Shape::~Shape()
{
//…
}

 // Circle constructor implementation
Circle::Circle(int x, int y, int radius) : Shape (x, y)
{
   m_radius = radius;
}

// Circle destructor implementation
Circle::~Circle()
{
//…
}

// Circle override of the pure virtual Draw method.
void Circle::Draw()
{
   glib_draw_circle(m_x, m_y, m_radius);
}

main()
{
  // Define a circle with a center at (50,100) and a radius of 25
  Shape *pShape = new Circle(50, 100, 25);

  // Define a circle with a center at (5,5) and a radius of 2
  Circle aCircle(5,5, 2);

  // Various operations on a Circle via a Shape pointer
  //Polymorphism
  pShape->Draw();
**  pShape->MoveTo(100, 100);

**  pShape->Erase();
  delete pShape;

 // Invoking the Draw method directly
  aCircle.Draw();
}   

    
例子中使用及多态的代码以黑体标出了,它们一个生明白的特征就是经过一个基类的指针(或者引用)来调用不同子类的办法。
    
那么,现在的题材是,这个职能是怎贯彻的为?我们好事先来大概猜一下:对于一般的艺术调用,到了汇编代码这同层次之时节,一般还是行使
Call funcaddr
这样的命令展开调用,其中funcaddr是要调用函数的地方。按理来说,当自身下指针pShape来调用Draw的早晚,编译器应该拿
Shape::Draw的地点给给funcaddr,然后Call
指令就好一直调用Shape::Draw了,这便跟用pShape来调用Shape::Erase一样。但是,运行结果却告诉我们,编译器赋给
funcaddr的值也是Circle::Drawde的值。这就说明,编译器在自查自纠Draw方法和Erase方法时采取了双重标准。那么到底是何人发这般
大的佛法,使编译器这个铁面无私的判官都如任何眼相扣呢?virtual!!
    

Clever!!正是virtual这个第一字一手导演了立同起“乾坤大挪移”的好戏。说道这里,我们先行要旗帜鲜明两单概念:静态绑定和动态绑定。
    1、静态绑定(static
bingding),也为早期绑定,简单的话就是编译器在编译期间即显然掌握所而调用的法子,并将欠措施的地址与给了Call指令的funcaddr。因此,运行中一直下Call指令就只是调用到相应的艺术。
    2、动态绑定(dynamic
binding),也叫晚期绑定,与静态绑定不同,在编译期间,编译器并无能够肯定了解究竟要调用的凡呀一个道,而当时,要知运行期间采取的有血有肉是谁目标才能够操纵。
   
好了,有了立即片只概念之后,我们虽可说,virtual的用意就是是报告编译器:我如果进行动态绑定!编译器当然会侧重您的见解,而且以做到你这个要求,
编译器还要举行过多底事务:编译器自动在声明了virtual方法的类似吃插一个指南针vptr和一个数据结构VTable(vptr用以指向
VTable;VTable是一个指南针数组,里面存着函数的地点),并保管二者遵守下面的平整:
   
1、VTable中不得不存放声明也virtual的措施,其它方法不克存于中。在方的例子中,Shape的VTable中即惟有
Draw,MoveTo和~Shape。方法Erase的地点并无可知存于VTable中。此外,如果艺术是纯虚函数,如
Draw,那么同样如果于VTable中保存相应的职,但是出于纯虚函数没有函数体,因此该岗位被并无存Draw的地址,而是可以择存放一个串处理
的函数的地点,当该职务给意外调用时,可以为此出错函数进行对应的拍卖。
   
2、派生类的VTalbe中著录的从基类中连续下去的虚函数地址之索引号必须与该虚函数在基类VTable中的索引号保持一致。如在上例中,如果在
Shape的VTalbe中,Draw为 1 号, MoveTo 2 号,~Shape也 3
号,那么,不管这些办法在Circle中凡遵照什么顺序定义之,Circle的VTable中都不能不保证Draw为
1 号,MoveTo为 2如泣如诉。至于
3哀号,这里是~Circle。为什么未是~Shape啊?嘿嘿,忘啦,析构函数不会见连续的。
   
3、vptr是由于编译器自动插入生成的,因此编译器必须负责呢其进展初始化。初始化的工夫选择在靶创建时,而地方就是当构造函数中。因此,编译器必须确保每个接近至少发生一个构造函数,若没有,自动为夫好成一个默认构造函数。
     4、vptr通常位于对象的苗子处,也尽管是Addr(obj) == Addr(obj.vptr)。
   
你看,天下果然没有免费之午餐,为了实现动态绑定,编译器要为我们私下干了如此多之脏话累活。如果您想体验一下编译器的惨淡,那么可以品味用C语言模拟一
下者的作为,【1】中便闹如此一个例证。好了,现在万事具备,只欠东风了。编译,连接,载入,GO!当程序执行到
pShape->Draw()的时光,上面的配备也开从作用了。。
   
前面都提到,晚期绑定时用不可知确定调用哪个函数,是盖具体的对象非确定。好了,当运行至pShape->Draw()常常,
对象下了,它由pShape指针标出。我们找到这目标后,就好找到其其中的vptr(在对象的起始处),有矣vptr后,我们虽找到了
VTable,调用的函数就当前边了。。等等,VTable中法那么基本上,我究竟以谁吧?不用急,编译器早已为咱搞好了记录:编译器在开立
VTable时,已经也每个virtual函数安排好了座次,并且把此索引号记录了下来。因此,当编译器解析到pShape->Draw()的时刻,它已暗中的以函数的讳用索引号来顶替了。这时候,我们经过者索引号就可以以VTable中获得一个函数地址,Call
it!
   
在此间,我们不怕体会至为何会产生第二条规定了,通常,我们还是为此基类的指针来引用派生类的靶子,但是无论是具体目标是哪位派生类的,我们还得以用同样的索引号来取相应的函数实现。
    
现实中发出一个例子其实与这蛮像的:报警电话起110,119,120(VTable中不同的道)。不同地方的人口转打不同之数码所来的结果尚且是匪均等
的。譬如,在三环绕外的一个总人口(具体对象)跟同样围外的一个口(另外一个有血有肉对象)打119,最后调用的消防队肯定是不同等的,这便是多态了。这是怎么落实的
呢,每个人且清楚一个报警中心(VTable,里面有三个道
110,119,120)。如果三绕外之一个人数用火警抢险(一个切实目标)时,它就拨打119,但是他自然不明了最后是啦一个消防队会面世的。这得生回报
警中心来决定,报警中心经过是实际对象(例子中就是具体位置了)以及他说拨打的电话号码(可以知道成索引号),报警中心可以确定相应调度哪一个消防队进
行抢险(不同之动作)。
    
这样,通过vptr和VTable的扶植,我们便贯彻了C++的动态绑定。当然,这仅仅是单继承时的情事,多重复继承的处理要相对复杂一点,下面简要说一下
最简便的文山会海继承的场面,至于虚继承的动静,有趣味之爱侣可看
Lippman的《Inside the C++ Object
Model》,这里少虽未开展了。(主要是协调还尚未干懂,况且现在基本上更继承都有些用了,虚继承应用的会就又不见了)
    
首先,我要优先说一下大抵重继承下对象的内存布局,也就是说该对象是怎么存放本身的数目的。

class Cute
{
public:
 int i;
 virtual void cute(){ cout<<“Cute cute”<<endl; }
};

class Pet
{
public:
   int j;
   virtual void say(){ cout<<“Pet say”<<endl;  }
};

class Dog : public Cute,public Pet
{
public:
 int z;
 void cute(){ cout<<“Dog cute”<<endl; }
 void say(){ cout<<“Dog say”<<endl;  }
};

   
在上面这例子中,一个Dog对象在内存中的布局如下所示:                    

Dog

Vptr1

Cute::i

Vptr2

Pet::j

Dog::z

    
也就是说,在Dog对象被,会在个别只vptr,每一个跟所继承的父类相呼应。如果我们若惦记实现多态,就亟须于目标被规范地找到呼应的vptr,以调用不
同的法。但是,如果因单继承时底逻辑,也就是是vptr放在指针指为位置的发端处,那么,要当多重新继承情况下实现,我们要确保在拿一个派出生类的指针隐
式或者显式地变换成为一个父类的指针时,得到的结果对相应派生类数据以Dog对象中之起始位置。幸好,这工作编译器已经帮助咱成功了。上面的事例中,如果
Dog向上转换成为Pet的口舌,编译器会自行计算Pet数据以Dog对象被之偏移量,该偏移量加上Dog对象的起首位置,就是Pet数据的骨子里地址了。

int main()
{
 Dog* d = new Dog();
 cout<<“Dog object addr : “<<d<<endl;
 Cute* c = d;
 cout<<“Cute type addr : “<<c<<endl;
 Pet* p = d;
 cout<<“Pet type addr : “<<p<<endl;
 delete d;
}

output:
Dog object addr : 0x3d24b0
Cute type addr : 0x3d24b0
Pet type addr : 0x3d24b8   //
正好指向Dog对象的vptr2处,也就算是Pet的数目

     
好了,既然编译器帮咱机关完成了不同父类的地方转换,我们调用虚函数的进程也不怕跟单继承统一起来了:通过具体目标,找到vptr(通常指针的开场位置,
因此Cute找到的是vptr1,而Pet找到的凡vptr2),通过vptr,我们找到VTable,然后因编译时获得的VTable索引号,我们取
得相应的函数地址,接着就是得即时调用了。

     
在这边,顺便为领到一下片只特别的办法以多态中的特别之处吧:第一独凡是构造函数,在构造函数中调用虚函数是勿会见生多态行为的,例子如下:

class Pet
{
public:
   Pet(){ sayHello(); }
   void say(){ sayHello(); }

   virtual void sayHello()
   {
     cout<<“Pet sayHello”<<endl;
   }
  
};

class Dog : public Pet
{
public:
   Dog(){};
   void sayHello()
   {
     cout<<“Dog sayHello”<<endl;
   }
};

int main()
{
 Pet* p = new Dog();
 p->sayHello();
 delete p;
}

output:
Pet sayHello //直接调用的凡Pet的sayHello()
Dog sayHello //多态

    
第二只就是是析构函数,使用多态的下,我们经常采取基类的指针来引用派生类的目标,如果是动态创建的,对象下完后,我们用delete来刑满释放对象。但是,如果我们不留意的言辞,会产生意想不到的图景有。

class Pet
{
public:
   ~Pet(){ cout<<“Pet destructor”<<endl;  }
  //virtual ~Pet(){ cout<<“Pet virtual destructor”<<endl; 
}
};

class Dog : public Pet
{
public:
   ~Dog(){ cout<<“Dog destructor”<<endl;};
   //virtual ~Dog(){ cout<<“Dog virtual destructor”<<endl; 
}
};

int main()
{
 Pet* p = new Dog();
 delete p;
}

output:
Pet destructor  //糟了,Dog的析构函数没有调用,memory leak!

只要我们以析构函数改化virtual以后,结果如下
Dog virtual destructor
Pet virtual destructor   // That’s OK!

   
所以,如果一个接近设计用来吃连续的话,那么她的析构函数应该受声称也virtual的。

Reference:
[1] Comparing C++ and C (Inheritance and Virtual
Functions)  
[2]
C++对象布局以及多态实现的探究 
[3] Multiple inheritance and the this
pointer 讲述多再度继承下之类型转换问题

[4] Memory Layout for Multiple and Virtual
Inheritance 详细描述了差不多再菱形多又继承下之目标内存布局以及类型转换

 转
http://hi.baidu.com/daping\_zhang/blog/item/e87163d06c42818fa0ec9cfc.html