初识C++之多态

多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
1、什么是多态
  多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。它是面向对象程序设计(OOP)的一个重要特征。如果一个语言只支持类而不支持多态,只能说明它是基于对象的,而不是面向对象的。C++中的多态性具体体现在运行和编译两个方面。它可以简单地概括为“一个接口,多种方法”。
  那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

#define _CRT_SECURE_NO_WARNINGS 1 
#include <iostream>
using namespace std;
class Animal
{
public:
  virtual void Sleep() //这里的关键字virtual后面会讲到
  {
    cout << "Sleep()" << endl;
  }
public:
  char sex[5]; int age;
};

class Person: public Animal
{ 
public:
  virtual void Sleep()
  {
    cout << "Person lying down to sleep!" << endl;
  }
public:
  char notionality[20]; //国籍
  char name[10];
};

class Horse: public Animal
{
public:
  virtual void Sleep()
  {
    cout << "Horse sleep standing up!" << endl;
  }
public:
  char rase[20]; //种族
};

int main()
{
  Person p;
  Horse h;
  Animal *pa1 = &p;
  Animal *pa2 = &h;
  pa1->Sleep();
  pa2->Sleep();
  return 0;
} 

输出:
初识C++之多态_第1张图片
可以看到,虽然pa1、pa1都是基类的指针,但是调用时却调用的是Person、Horse两个派生类的方法。这是因为pa1和pa2分别指向了Person、Horse两个派生类的对象。原来下来慢慢剖析。

2、多态的分类
①编译时多态:也叫静态多态,在编译时就可以确定对象使用的形式。 实现机制使通过函数重载(运算符重载实质也属于函数重载)。
②运行时多态:也叫动态多态,其具体引用的对象在运行时才能确定。 实现机制是通过虚函数。
  静态多态性与动态多态性的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  

3、动态多态性存在的三个必要条件
①要有类的继承关系;
②要有虚函数(只有重写了虚函数的才能算作是体现了C++多态性);
注意函数重写、重载、重定义的区别,可以参考:
http://blog.csdn.net/ljx_5489464/article/details/51127081
③要有父类指针/引用指向子类对象(用于访问派生类中同名覆盖成员函数)。

4、多态的实现
  C++多态性是通过虚函数来实现的,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写(重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)。
  虚函数:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数,用法格式为:
  virtual 函数返回类型 函数名(参数表)
  {
   函数体;
  }
关于虚函数这里不做过剖析,可以参考:
http://blog.csdn.net/ljx_5489464/article/details/51138393
  前面说了,多态的动态性实现是靠虚函数,它的实质是在基类定义一个虚函数,在派生类中对其进行重写,然后定义基类的指针指向派生类对象,然后用该指针调用虚函数,此时调用的就是指针指向的对象对应的类里面的同名函数。那么这种机制在底层是怎样实现的呢?  
  想要弄清楚这个就必须要清楚一个东西–>虚表,全称为虚拟函数表。什么是虚表,虚表就是一张表,它里面存放虚函数的入口地址,在C++语言中,每个有虚函数的类或者基类有虚函数的派生类,编译器都会为它生成一个虚拟函数表(注意:虚表是从属于类的)。此外,编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针(常被称为vfptr,注意:虚表指针是从属于对象的)。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
  虚表的内容是依据类中的虚函数声明次序–填入函数指针。派生类会继承基础类别的虚表(以及所有其他可以继承的成员),再在此虚表上添加上自己的虚函数指针,当我们在派生类中改写虚函数时,虚表就受了影响,改写后的的虚函数的地址会替换原虚函数的地址存放于虚表中。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class no_Virtual
{
public:
    void Fun()
    {
        cout << "no_Virtual::Fun()" << endl;
    }
};

class Virtual
{
public:
    virtual void Fun()     //注意这儿加了关键字virtual
    {
        cout << "Virtual::Fun()" << endl;
    }
};

int main()
{
    cout << sizeof(no_Virtual) << endl;
    cout << sizeof(Virtual) << endl;
    return 0;
}

初识C++之多态_第2张图片
①no_Virtual类没有任何数据成员,也没有虚函数,按说它的大小应该是0,但这儿它的大小是1。我理解的原因有以下两点:
  这个是因为no_Virtual既然是一个类型,那么就能用来定义变量,而定义变量肯定要分配空间,分配多大合适呢?0个?显然不可能,2个、4个?太浪费了,它什么都不用存储,分配那么大干什么,所以编译器就为我们选了个折中的办法,分配一个,既不会浪费空间,也能用来定义变量。
  既然no_Virtual是类型,用来定义变量的,那么要是它一个空间都不占的话,用它定义出来的变量就都从同一位置开始存储了,那我们访问那块空间时,怎么知道到底是访问哪一个变量呢?
②Virtual类没有任何数据成员,但是有虚函数,按我们上面的说法,它的大小是4,结果很给面子的正确了。
定义一个Virtual类的变量,看看是不是像上面说的那样,有一个从属于它的虚表指针,从监视窗口看,确实是的。
初识C++之多态_第3张图片

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void Fun1()
    {
        cout << "Base::Fun1()" << endl;
    }

    virtual void Fun2()     //注意这儿加了关键字virtual
    {
        cout << "Base::Fun2()" << endl;
    }
};

class Derived: public Base
{
public:
    virtual void Fun3()     //注意这儿加了关键字virtual
    {
        cout << "Derived::Fun3()" << endl;
    }

    virtual void Fun4()     //注意这儿加了关键字virtual
    {
        cout << "Derived::Fun4()" << endl;
    }
};

void PrintVirTable()
{
    Base b;
    Derived d;
    cout << "Base:" << endl;
    int *vfpTable = (int *)(*(int*)&b);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }

    cout << "Derived:" << endl;
    vfpTable = (int *)(*(int*)&d);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }
}

int main()
{
    PrintVirTable();
    return 0;
}

输出结果:
初识C++之多态_第4张图片

因为VS的监视窗口好像有Bug:
初识C++之多态_第5张图片
像这儿,派生类的对象就只能看到继承与基类的虚表,而不能看到派生类自己的,所以上面我一个函数分别打印了基类与派生类的虚表,可以很直观的看到,首先,派生类的虚表是继承于基类的,然后在加上自己的虚函数的地址到虚表中去,就构成了自己的虚表。
现在来剖析一下打印虚表的函数是怎么实现的:
首先得清楚带有虚函数的类创建的对象,前四个字节里面存储的就是虚表指针的地址,如下图:

再回过头来看看虚表的打印函数:

void PrintVirTable()
{
    Base b;
    Derived d;
    cout << "Base:" << endl;
    int *vfpTable = (int *)(*(int*)&b);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }

    cout << "Derived:" << endl;
    vfpTable = (int *)(*(int*)&d);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }
}

它的原理其实是这样的:
初识C++之多态_第6张图片

再来看一个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;

class Base
{
public:
    virtual void Fun1()
    {
        cout << "Base::Fun1()" << endl;
    }

    virtual void Fun2()     //注意这儿加了关键字virtual
    {
        cout << "Base::Fun2()" << endl;
    }
};

class Derived: public Base
{
public:
    virtual void Fun3()     //注意这儿加了关键字virtual
    {
        cout << "Derived::Fun3()" << endl;
    }

    virtual void Fun4()     //注意这儿加了关键字virtual
    {
        cout << "Derived::Fun4()" << endl;
    }
public:
    int data;
};

void PrintVirTable()
{
    Derived d1;
    Derived d2;
    cout << "d1::virTable" << endl;
    int *vfpTable = (int *)(*(int*)&d1);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }

    cout << "d2::virTable" << endl;
    vfpTable = (int *)(*(int*)&d2);
    while (*vfpTable != NULL)
    {
        int *pFun = (int*)(*vfpTable);
        cout << pFun << endl;
        vfpTable++;
    }
}

int main()
{
    PrintVirTable();
    return 0;
}

输出结果:
初识C++之多态_第7张图片

这儿就证明了如果一个类有虚函数,那么该不同对象公用同一张虚表。因为函数的地址是在编译期间的确定的,一个函数的地址只会被存储一次,虽然累的对象不同,但它们调用该类的同名函数其实是调用的同一函数,因此它们的虚表会相同。

你可能感兴趣的:(C++,虚函数,多态,虚表,继承体系)