多态(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