`
Dustin
  • 浏览: 314671 次
  • 性别: 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++之多态实现机制剖析

    本文将深入剖析C++中的多态实现机制。 1. 虚函数(Virtual Functions) 虚函数是C++中实现动态多态的基础。通过在基类中声明虚函数,子类可以重写这些函数,使得在使用基类指针或引用调用该函数时,实际执行的是...

    C++多态的实现

    C++中的多态主要通过虚函数机制来实现。本文将详细探讨C++中如何实现多态,并通过具体示例说明如何在实际编程中运用这一特性。 #### 一、多态的概念 多态(Polymorphism)是指同一个操作作用于不同的对象,可以有...

    多态代码实例

    在编程领域,多态(Polymorphism)是一个关键概念,尤其在面向对象编程中发挥着重要作用。多态允许我们使用一个通用的接口来处理不同类型的对象,提供了代码的灵活性和可扩展性。虚函数(Virtual Function)是实现...

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

    ### C++中的虚函数及其实现多态的机制 #### 一、虚函数的概念与作用 在C++中,虚函数是一种特殊类型的成员函数,它的主要目的是实现多态性(polymorphism)。多态性允许子类重写父类的方法,并且能够在运行时根据...

    C++---易学C++

    《C++---易学C++》是一本专为初学者设计的C++编程入门书籍,其特点是用简单易懂的语言讲解了C++的基础知识和核心概念。这本书旨在帮助新手快速理解并掌握C++编程语言,使得即使没有编程背景的读者也能轻松上手。由于...

    C语言实现C++多态

    总结起来,虽然C语言不具备C++那样的内置多态机制,但通过函数指针和结构体,我们可以实现类似的功能。这种方法需要更高的编程技巧和更严谨的设计,但在某些场合下,如嵌入式系统或性能要求极高的场景,可能是一种...

    面向对象程序设计---C++语言描述 原书第2版

    在C++中,可以通过虚函数(Virtual Function)实现运行时多态。 4. **封装**:封装是将数据成员和成员函数组合在一个单独的单元——类中,并控制外部对它们的访问程度,以保护数据的完整性和安全性。 #### C++语言...

    C++ 多态 实例

    在C++编程语言中,多态(Polymorphism)是一种核心特性,它允许不同类型的对象对同一消息作出不同的响应。这个实例将帮助初学者深入理解多态的概念,并通过实际代码来展示其工作原理。 多态分为编译时多态(静态...

    多态PPT文件!!!!

    在IT行业中,多态(Polymorphism)是一个核心概念,主要在面向对象编程(Object-Oriented Programming, OOP)中出现。它允许我们使用一个接口来表示多种不同的类型,从而提高了代码的灵活性、可扩展性和可重用性。在...

    经典c/c++多态实例

    在C/C++中,多态性主要通过虚函数(Virtual Functions)和纯虚函数(Pure Virtual Functions)来实现,这两者都是在基类中定义的,允许子类进行重写,从而达到行为上的多样性。 1. 虚函数:在C++中,虚函数是通过在...

    61 多态案例二-PK小游戏.rar

    在C++编程语言中,多态(Polymorphism)是一种核心特性,它允许不同类型的对象对同一消息作出不同的响应。这个“61 多态案例二-PK小游戏.rar”压缩包文件提供了一个实际的编程示例,用于演示如何在实践中应用多态、...

    深入编程内幕--Visual C++

    4. 多态性(polymorphism)和虚函数(virtual functions),实现类的继承和接口的多态实现。 5. 模板元编程(template metaprogramming),利用模板在编译期进行计算和逻辑判断。 6. STL中的容器(如vector, list, ...

    C++多态PPT

    C++中的多态有两种形式:静态多态(Static Polymorphism)和动态多态(Dynamic Polymorphism)。静态多态主要通过函数模板和运算符重载在编译时实现,而动态多态则是通过虚函数在运行时实现。 **总结** C++的多态...

    C++多态day15

    在C++编程语言中,多态(Polymorphism)是面向对象编程的三大特性之一,另外两个是封装和继承。多态允许我们使用一个接口来表示多种类型,它提供了代码的灵活性和重用性,使得程序更加通用。今天我们将深入探讨C++中...

    c/c++面试题(一些常出的面试题,包括多态,继承,函数参数等常考的内容)

    静态多态主要通过函数重载和运算符重载来实现,而动态多态则依赖于虚函数和纯虚函数,以及继承机制。 其次,“继承”(Inheritance)是另一个关键概念。继承允许一个类(子类)从另一个类(父类)中继承属性和行为...

    C++多态Demo

    在C++编程语言中,多态(Polymorphism)是一种重要的特性,它允许我们使用一个接口来表示多种不同的类型。这个“C++多态Demo”示例中,我们看到长方形和三角形作为形状(Shape)类的派生类,体现了面向对象编程中的...

    基于C++的多态模式编写类的代码多态.docx

    在C++中,多态(Polymorphism)是面向对象编程的一个核心概念,它允许我们使用一个接口来表示多种不同的类型。多态性使得代码更加灵活,可扩展性更强,可以减少代码重复,并提高代码的复用性。在这个文档中,通过两...

    C++中的虚函数与多态

    在C++编程语言中,虚函数(Virtual Functions)和多态(Polymorphism)是面向对象编程的重要特性,它们使得程序具有高度的灵活性和可扩展性。本文将深入探讨这两个概念,结合示例代码进行详细解释。 首先,我们来...

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

    在C++编程语言中,"类的多态性"(Polymorphism)是面向对象编程的三大特性之一,另外两个是封装和继承。多态性允许我们使用一个接口来表示不同的类型,使得代码更加灵活,易于扩展。在这个“类的多态练习”中,我们...

    入门组C++(CSP2019-junior-C++-A).pdf

    在C++中,可以通过虚函数实现运行时的多态。 ```cpp class Shape { public: virtual void draw() { cout 绘制一个形状" ; } }; class Circle : public Shape { public: void draw() override { cout 绘制一个...

Global site tag (gtag.js) - Google Analytics