多态 : 顾名思义,多态就是多种形态,也就是对不同对象发送同一个消息,不同对象会做出不同的响应。
并且多态分为静态多态和动态多态。
静态多态就是在系统编译期间就可以确定程序执行到这里将要执行哪个函数,例如:函数的重载,对象名加点操作符执行成员函数等,都是静态多态,其中,重载是在形成符号表的时候,对函数名做了区分,从而确定了程序执行到这里将要执行哪个函数,对象名加点操作符执行成员函数是通过this指针来调用的。
函数的重载比较简单,不再赘述,这里我们通过一个简单的例子来看一下对象名加点操作符执行成员函数的静态多态:
class A
{
public:
void Set(int a)
{
_a = a;
}
public:
int _a;
};
int main()
{
A a1;
a1.Set(15);
return 0;
}
这里定义了一个A类,有一个成员函数和一个成员,我们将程序的部分汇编代码截取出来如下图:
我们可以看到这里直接是一个lea指令将a1对象的地址放入寄存器eax中,也就是对象的this指针,然后用call指令就可以跳转到Set函数,也就是说其汇编代码在此时就知道应该要去到哪个地方之行哪个函数,这就是静态多态,也叫编译时多态。
动态多态则是利用虚函数实现了运行时的多态,也就是说在系统编译的时候并不知道程序将要调用哪一个函数,只有在运行到这里的时候才能确定接下来会跳转到哪一个函数的栈帧。
在说动态多态之前我们先来看一下什么是虚函数,虚函数就是在基类中声明该函数是虚拟的(在函数之前加virtual关键字),然后在子类中正式的定义(子类中的该函数的函数名,返回值,函数参数个数,参数类型,全都与基类的所声明的虚函数相同,此时才能称为重写,才符合虚函数,否则就是函数的重载),再定义一个指向基类对象的指针,然后使该指针指向由该基类派生的子类对象,再然后用这个指针来调用改虚函数,就能实现动态多态。
下面我们通过一个例子来看一下利用虚函数实现的动态多态:
class A
{
public:
A(int a = 10)
:_a(a)
{}
virtual void Get()
{
cout << "A:: _a=" << _a << endl;
}
public:
int _a;
};
class B : public A
{
public:
B(int b = 20)
:_b(b)
{}
void Get()
{
cout << "B:: _b=" << _b << endl;
}
int _b;
};
int main()
{
A a1;
B b1;
A* ptr1 = &a1;
ptr1->Get();
ptr1 = &b1;
ptr1->Get();
return 0;
}
在这里我们看到,基类A的Get函数声明为虚函数,在B类中进行了重写,
然后在main函数中分别用基类的ptr1和指向子类的ptr2进行调用虚函数Get,我们得到了如下图的输出:
这说明确实是实现了不同调用,而且是在运行时,那么虚函数的底层到底是怎么实现的呢,我们来看一下汇编代码及其对象模型:
通过上图的汇编代码,我们看到这里做了一系列的指针解引用处理,最后确定了eax中应该存放的this指针的值,要搞清楚这个必须要搞清楚子类的对象模型。
用监视窗口查看b1可以看到如上图所示,这里的_vfptr是一个虚表指针,它指向一个存放该类对象的所有虚函数的地址的表,我们可以将该表理解为一个函数指针数组,在该数组的最后一个元素,编译系统会将其置为0,。
对象模型如下图示:
其中红色为A类的成员,黑色为B类对象b1的成员,紫色就是一个虚函数表,存放着存放该类对象的所有虚函数的地址,汇编代码做了一系列的指针解引用处理就是为了从虚函数表中找到相应的虚函数进行调用,从而实现了动态多态。