`
Dustin
  • 浏览: 310105 次
  • 性别: Icon_minigender_1
  • 来自: 广州/成都
社区版块
存档分类
最新评论

多态(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);
}

int 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::Draw的值。这就说明,编译器在对待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 详细描述了多重菱形多重继承下的对象内存布局以及类型转换

分享到:
评论

相关推荐

    虚函数是C++中用于实现多态(polymorphism)的机制

    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。

    类的多态练习_C++_类的多态练习_

    C++类的多态练习(Class polymorphism exercise)

    程序员考试刷题-karthik-veeramani-understanding-polymorphism__2-4811:karthik-ve

    程序员考试刷题 理解多态 描述 本教程旨在涵盖多态的基本知识。 更多信息 提交日期 ...回到学校,以前我们做C++的时候,每次考试都会出现这个问题——“多态有哪些不同类型?举个例子”。 我读过的一些书描

    C++标准的STL介绍

    与OOPobject-oriented programming中的多态polymorphism一样,泛型也是一种软件的复用技术; 从实现层次看,整个STL是以一种类型参数化的方式实现的,这种方式基于一个在早先C++标准中没有出现的语言特性--模板...

    C++基础教程完整版

    同样对于C和C++在实现上有明显不同的概念,将备用如下标志标出: C 与 C++不同的地方 编译器 本教程中所有例题程序均为console程序(控制台程序)。此类程序以文本形式与用户交换信息,显示结果。 所有C++编译器均...

    C++ 虚函数与多态 教学PPT

    多态性(Polymorphism)是面向对象程序设计的一个非常重要的特性,是指不同的对象对于同样的消息会产生不同的行为,而消息在C++语言中指的就是函数的调用,不同的函数可以具有多种不同的功能,而多态就是允许用一个...

    polymorphism in c++

    原书的6,7章,关注怎样利用多态构建应用程序的框架,对于想进阶的朋友比较大的帮助 

    C++中的虚函数(virtual function)

    虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。假设我们有下面的类层次: class A { public: virtual void foo() { cout () is called" ;} }; class B: public A { ...

    c++第八章 继承与多态课件ppt

    该机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构。体现了由简单到复杂的认识...

    C++中的多态、重载、覆盖与隐藏

     虚函数是C++中用于实现多态(polymorphism)的机制。核心理念是通过基类访问派生类定义的函数。假设我们有下面的类层次: class A { public: virtual void foo() { cout &lt;&lt; "A::foo() is called" &lt;...

    深度探索C++对象模型

    深度探索C++对象模型 第0章 导读(译者的话) 第1章 关于对象(Object Lessons) ...加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作

    《深度探索C++对象模型》(Stanley B·Lippman[美] 著,侯捷 译)

    对于C++底层机制感兴趣的读者,这本书会给你“漫卷诗书喜欲狂”的感觉。 了解C++ Object Model,是学习Component ObjectModel的最短路线。 如果你是一位C++程序员,渴望对于底层知识获得一个完整的了解,那么Inside ...

    深度探索模C++对象模型PDF

    加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”...

    C++超详细的基础教程

    4.虚拟成员,抽象,多态 Virtual Members. Abstraction. Polymorphism 6.C++高级 Advanced concepts 1.模板 Templates 2.名空间 Namespaces 3.出错处理 Exception handling 4.类型转换高级 Advacned Class ...

    深度探索C++对象模型 超清版

    加上多态之后(Adding Polymorphism) 第2章 构造函数语意学(The Semantics of constructors) 2.1 Default Constructor的建构操作 “带有Default Constructor”的Member Class Object “带有Default Constructor”...

    Effective C++(第三版)

    understand implicit interfaces and compile-time polymorphism. 条款42:了解typename的双重意义 understand the two meanings of typename. 条款43:学习处理模板化基类内的名称 know how to access names in ...

    C++中虚函数与纯虚函数的用法

    本文较为深入的分析了C++...虚函数的作用是实现多态性(Polymorphism),多态性是将接口与实现进行分离,采用共同的方法,但因个体差异而采用不同的策略。纯虚函数则是一种特殊的虚函数。虚函数联系到多态,多态联系到

    Think in ActionScript 3.0Ⅰ Ⅱ Ⅲ汇总

    5. 多态(Polymorphism) 5.1 多态的概念 5.2 多态存在的三个必要条件 5.3 TestPolymoph.as —— 多态的应用,体会多态带来的好处 5.4 TestPolymoph 内存分析 5.5 多态的好处 5.6 抽象类的概念 5.7 对象转型...

    深入浅出MFC-简体版(2)PDF

    虚拟函数与多态(Polymorphism) 类与对象大解剖 Object slicing与虚拟函数 静态成员(变量与函数) C++程序的生与死:兼谈构造函数与解构函数 四种不同的对象生存方式(in stack、in heap、global、local static) ...

    详解C++编程的多态性概念

    C++支持多态性,在C++程序设计中能够实现多态性。利用多态性可以设计和实现一个易于扩展的系统。 顾名思义,多态的意思是一个事物有多种形态。多态性的英文单词polymorphism来源于希腊词根poly(意为“很多”)和morph...

Global site tag (gtag.js) - Google Analytics