【摘要】多态性是面向对象程序设计语言中数据抽象和继承之外的第三个基本特性。
要想认识多态,我们需要从最基础的知识开始着手,这篇博客是我整理了很久才发出来的,里面对于多态的底层分析很详尽,希望可以对你们有所帮助
多态的概念
多态,顾名思义就是一种事物具有多种形态,用比较正式的话来说,大概就是下面这段话啦。
向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法)。也就是说,每个对象可以用自己的方式去相应共同的消息。而所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。
粗略点来说,
函数的重载和运算符重载也都算作多态,因为这类函数的一大特点就是在程序运行之前就已经确定了该调用哪一个函数,这类多态称为静态多态,静态多态性的函数调用具有调用速度快,效率高的优点,但是缺点就是缺乏灵活性。还有一类多态是程序在运行时才知道该调用那个函数,这类多态称为运行时多态,而今天我主要分析的就是
运行时多态。
多态性提供了接口和具体实现之间的另一层隔离。多态性改善了代码的组织性和可读性,同时也使得创建的程序具有可扩展性。
在真正认识多态之前,我希望你们可以看一下下面这段代码。
class A
{
public:
void f1()
{
cout<<"A::f1"<<endl;
}
private:
int _a;
};
class B:public A
{
public:
void f1()
{
cout<<"B::f1"<<endl;
}
private:
int _b;
};
int main()
{
A a; //定义一个A类对象
B b; //定义一个B类对象
A *p = &a; //定义一个A类指针指向对象a
p->f1(); //用指针调用f1()
p = &b; //让指针重新指向子类对象b
p->f1(); //用指针调用f1()
这个结果肯定不是我们想要的,但是为什么会出现这种情况呢?我用图片的形式给你们解释一下
如果出现下面这种情况,就会发生隐藏的情况,先看代码吧。
class A
{
public:
void f1()
{
cout<<"A::f1"<<endl;
}
private:
int _a;
};
class B:public A
{
public:
void f1()
{
cout<<"B::f1"<<endl;
}
private:
int _b;
};
int main()
{
A a;
B b;
b.f1();
我只是在main函数里面改了一下,程序就会出现下面的结果。
按理说,应该调用A的f1()吧,为什么会调用B的呢?因为 f1() 函数已经构成了隐藏,而构成隐藏的情况下,派生类会隐藏基类的函数,那就会造成基类函数无法调用的情况。
为了解决此类情况,让我们只需要定义一个对象,让它可以随意调用基类或者派生类的函数呢?这就需要用到虚函数的知识了。
所谓虚函数,就是在普通函数前面加上关键字 virtual ,听上去好像没什么难的,但是它的底层实现还是很复杂的,现在我们就来一探究竟吧。
老办法,还是刚才的那段代码,我只是修改了基类函数 f1() 成为虚函数 ,得到的结果就会完全不一样了,不行你可以试一试。
这里我们来看一看对象 b 里面都有什么吧。
对了,在这里我得先说一下,只有定义成为虚函数才会有虚函数表。每个类只有一个虚函数表,不管这个类中定义了多少个虚函数,他们都会存储在这张表里面。也正是由于这张表,才使得虚函数的多态性体现出来。
接下来我们验证一下,我上面说的对不对。验证方法很简单,只需要在同一个类中定义多个函数然后用sizeof比较一下类的大小就清楚了。
最后得到的结果都是8.从上面我们已经知道,在类A中存了一个指针指向一个虚基表,因此占用4个字节大小,还有本身的整型 a ,占用4个字节,因此总共是八个字节。并且,我们也知道了,定义虚函数就会产生虚基表,那么这样说的话,应该有两个指针才对,实际上,我们从见识窗口就可以直接看到,只有一个指针,这就间接说明了两个虚函数都放在同一个虚基表里面。
现在我们已经知道了多态的实现机制,根本上就是虚函数表的应用。有了这张虚基表,我们就可以通过不同的对象调用不同的函数而不用担心出现隐藏或者调用函数模糊的情况啦。因为这篇博客篇幅太长了,我决定再写一篇博客详细讲解虚函数表的底层。