C++——多态性实现机制

C++的多态性实现机制剖析

1. 多态性和虚函数

#include <iostream.h>
class animal
{
public:
    void sleep()
    {
        cout<<"animal sleep"<<endl;
    }
    void breathe()
    {
        cout<<"animal breathe"<<endl;
    }
};
class fish:public animal
{
public:
    void breathe()
    {
        cout<<"fish bubble"<<endl;
    }
};
void main()
{
    fish fh;
    animal *pAn=&fh;
    pAn->breathe();
}

注意。程序中未定义虚函数。

程序执行的结果是什么?答案是输出:animal breathe

我们在main()函数中首先定义了一个fish类的对象fh。接着定义了一个指向animal类的指针变量pAn,将fh的地址赋给了指针变量pAn。然后利用该变量调用pAn->breathe()。很多学员往往将这种情况和C++的多态性搞混淆,觉得fh实际上是fish类的对象。应该是调用fish类的breathe(),输出“fish bubble”,然后结果却不是这样。以下我们从两个方面来讲述原因。

1、 编译的角度。C++编译器在编译的时候,要确定每一个对象调用的函数的地址。这称为早期绑定(early binding),当我们将fish类的对象fh的地址赋给pAn时。C++编译器进行了类型转换。此时C++编译器觉得变量pAn保存的就是animal对象的地址。

当在main()函数中执行pAn->breathe()时,调用的当然就是animal对象的breathe函数。

2、 内存模型的角度。我们给出了fish对象内存模型,例如以下图所看到的

我们构造fish类的对象时。首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完毕自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类的对象转换为animal类型时,该对象就被觉得是原对象整个内存模型的上半部分。也就是图1-1中的“animal的对象所占内存”。

那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此。输出animal breathe,也就顺理成章了。

前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址。要解决问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时。就会在执行时再去确定对象的类型以及正确的调用函数。而要让编译器採用迟绑定。就要在基类中声明函数时使用virtual关键字,这种函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在全部的派生类中该函数都是virtual,而不须要再显式地声明为virtual。

#include <iostream.h>
class animal
{
public:
    void sleep()        {   cout<<"animal sleep"<<endl;}
    virtual void breathe()  {   cout<<"animal breathe"<<endl;   }
};
class fish:public animal
{
public:
    void breathe()  {   cout<<"fish bubble"<<endl;}
};
void main()
{
    fish fh;
    animal *pAn=&fh;
    pAn->breathe();
}

大家可以再次执行这个程序,你会发现结果是“fish bubble”,也就是依据对象的类型调用了正确的函数。

那么当我们将breathe()声明为virtual时。在背后发生了什么呢?

编译器在编译的时候。发现animal类中有虚函数。此时编译器会为每一个包括虚函数的类创建一个虚表(即vtable),该表是一个一维数组,在这个数组中存放每一个虚函数的地址。对于例1-2的程序,animal和fish类都包括了一个虚函数breathe(),因此编译器会为这两个类都建立一个虚表,例如以下图所看到的:

那么怎样定位虚表呢?编译器另外还为每一个类的对象提供了一个虚表指针(即vptr),这个指针指向了对象所属类的虚表。在程序执行时。依据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就行找到正确的函数。对于例1-2的程序,因为pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable。当调用pAn->breathe()时,依据虚表中的函数地址找到的就是fish类的breathe()函数。

正是因为每一个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是很重要的。换句话说,在虚表指针没有正确初始化之前。我们不可以去调用虚函数。那么虚表指针在什么时候。或者说在什么地方初始化呢?

答案是在构造函数中进行虚表的创建和虚表指针的初始化

还记得构造函数的调用顺序吗。在构造子类对象时,要先调用父类的构造函数,此时编译器仅仅“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针。该虚表指针指向父类的虚表。

当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表

对于例2-2的程序来说,当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn->breathe(),因为pAn实际指向的是fish类的对象。该对象内部的虚表指针指向的是fish类的虚表,因此终于调用的是fish类的breathe()函数。

要注意:对于虚函数调用来说,每一个对象内部都有一个虚表指针。该虚表指针被初始化为本类的虚表。所以在程序中,无论你的对象类型怎样转换,但该对象内部的虚表指针是固定的。所以呢,才干实现动态的对象函数调用,这就是C++多态性实现的原理。

总结(基类有虚函数):

1、 每一个类a都有虚表。

2、 虚表可以继承,假设子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,仅仅只是这个地址指向的是基类的虚函数实现。假设基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表。至少有三项,假设重写了对应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。假设派生类有自己的虚函数。那么虚表中就会加入该项。

3、 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序同样。

时间: 2024-10-08 02:13:33

C++——多态性实现机制的相关文章

Java虚拟机 - 多态性实现机制

[深入Java虚拟机]之五:多态性实现机制--静态分派与动态分派 方法解析 Class文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在Class文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给Java带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析.这在前面的"Java内存区域与内存溢出"一文中有提到. 静态解

JVM基础(3)-多态性实现机制

一.方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址. 因此,想要使用这些符号引用必须经过转换,转换为直接引用,即内存中一个地址,可以直接指向方法本身.但是,转换的时机随着方法不同而不同. 有些方法可以在编译时就确定,比如static的方法(工具方法),比如private的方法,比如final的方法,这些方法有一个特点就是可以在编译期就确定,这种方式被称为静态解析. 但是有些方法

多态性实现机制——静态分派与动态分派

方法解析 Class 文件的编译过程中不包含传统编译中的连接步骤,一切方法调用在 Class 文件里面存储的都只是符号引用,而不是方法在实际运行时内存布局中的入口地址.这个特性给 Java 带来了更强大的动态扩展能力,使得可以在类运行期间才能确定某些目标方法的直接引用,称为动态连接,也有一部分方法的符号引用在类加载阶段或第一次使用时转化为直接引用,这种转化称为静态解析.这在前面的“Java 内存区域与内存溢出”一文中有提到. 静态解析成立的前提是:方法在程序真正执行前就有一个可确定的调用版本,并

java父类引用指向子类对象

父类引用指向子类对象指的是: 例如父类Animal,子类Cat,Dog.其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类. Animal animal = new Cat(); 即声明的是父类,实际指向的是子类的一个对象. 那这么使用的优点是什么,为什么要这么用?可以用这几个关键词来概括:多态.动态链接,向上转型 也有人说这是面向接口编程,可以降低程序的耦合性,即调用者不必关心调用的是哪个对象,只需要针对接口编程就可以了,被调用者对于调用者是完全透明的.让你更关

【转】父类引用指向子类对象

父类引用指向子类对象指的是: 例如父类Animal,子类Cat,Dog.其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类. Animal animal = new Cat(); 即声明的是父类,实际指向的是子类的一个对象. 那这么使用的优点是什么,为什么要这么用?可以用这几个关键词来概括:多态.动态链接,向上转型 也有人说这是面向接口编程,可以降低程序的耦合性,即调用者不必关心调用的是哪个对象,只需要针对接口编程就可以了,被调用者对于调用者是完全透明的.让你更关

多态机制详细分析以及c++内存对象布局

什么是多态? 多态性可以简单的概括为"1个接口,多种方法",在程序运行的过程中才决定调用的机制 程序实现上是这样,通过父类指针调用子类的函数,可以让父类指针有多种形态. 2 实现机制 举一个例子: #include <iostream.h> class animal { public: void sleep() { cout<<"animal sleep"<<endl; } void breathe() { cout<<

JAVA- 面向对象的三大特征(封装性、继承性、多态性)

程序的发展经历了两个主要阶段:面向过程.面向对象.面向对象是当前软件开发的主流. 面向过程就是分析出解决所需要的步骤,然后用函数将这些步骤一步一步实现,使用的时候一个一个一次调用. 面向对象是把构成问题的事务分解成各个对象.建立对象的目的不是为了完成一个步骤而是为了描述某个事物在整个解决问题步骤中的行为. 面向对象是为了专注在程序中采用封装.继承.多态等设计方法.面向对象的设计是一种提供符号设计系统的面向对象的实现过程,它用非常接近实际领域术语的方法吧系统构造成"现实世界"的对象. 面

第二十二篇:C++中的多态机制

前言 封装性,继承性,多态性是面向对象语言的三大特性.其中封装,继承好理解,而多态的概念让许多初学者感到困惑.本文将讲述C++中多态的概念以及多态的实现机制. 什么是多态? 多态就是多种形态,就是许多情况下可以互换地使用基类型和派生类型的多种形态. 多态的实现 依赖于动态绑定机制. 动态绑定机制相关 动态绑定是函数实际参数和形式参数绑定的一种方式,它是指我们能够在函数接口中使用继承层次中任意类型的对象,无需关心对象的具体类型. 动态执行接口函数的对象参数的哪个函数得在程序实际执行的时候才能确定

java的多态性(二)

2013-10-16 19:44 9364人阅读 评论(25) 收藏 举报  分类: [JAVA开发]-----Java提高篇(36)  版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[+] 面向对象编程有三大特性:封装.继承.多态. 封装隐藏了类的内部实现机制,可以在不影响使用的情况下改变类的内部结构,同时也保护了数据.对外界而已它的内部细节是隐藏的,暴露给外界的只是它的访问方法. 继承是为了重用父类代码.两个类若存在IS-A的关系就可以使用继承.,同时继承也为实现多态做了铺