cpp中的多态及其实现原理自述
实验平台:vs2013
操作系统:windos 10(指针大小为4字节)
首先咱们来说说这个多态是咋那么回事?
多态按字面的意思就是多种形态。通俗的讲就是不同的对象做同一件事,得到的结果不相同
- 如图
在打印同一份报告的时候,使用彩色打印机和黑白打印机打出来的效果是不同的,虽然两者都是打印机但是其内部结构有很大分别
- 那么问题来了,这个多态是用来干嘛的呢?
简单来说,当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
那么什么样的前提/环境才适合用多态呢?
- 使用多态的前提
- 1.调用函数的对象必须是指针或者引用。
- 2.被调用的函数必须是虚函数,且完成了虚函数的重写。
好啦,说了这么多我们来实际使用代码来看看多态的表现吧
#include<iostream>
using namespace std;
class The_average_person {
public:
virtual void BuyTicket(){
cout << "普通人全价票:100" << endl;
}
};
class Student :public The_average_person{
public:
virtual void BuyTicket(){
cout << "学生半价票:50" << endl;
}
};
void Buy(The_average_person& ticket){
ticket.BuyTicket();
}
int main(){
The_average_person person;
Student s;
Buy(person);
Buy(s);
return 0;
}
- 执行结果
- 不同的对象因为调用同一个函数而出现同样的结果,这就是多态的体现
在上面的多态构成的前提中我么看到一条被调用的函数必须是虚函数
那么是什么虚函数呢?
如上述代码中的添加了关键字virtual的函数就是虚函数,看起来也没有什么特别的地方,但是类在构成多态的时候,它起到的作用是不可替代的
virtual void BuyTicket(){
cout << "普通人全价票:100" << endl;
}
提到虚函数,那么我们就需要提一下纯虚函数了,是不是感觉他们听起来很相似啊?
其实他们差别还是蛮大的,让我们接着往下看吧
- 纯虚函数的格式
virtual void Print()=0;
其中包含纯虚函数的类叫做抽象类
但是抽象类不能用来实例化对象
其次如果子类继承抽象类,不重写纯虚函数.子类仍然是一个抽象类
//抽象类
class Date{
public:
virtual void Print()=0;
}
- 虚函数和纯虚函数的异同点
- 相同点:
1.都使用了关键字virtual
2.作用域都在类内部 - 不同点
1.声明的格式有很大差别
2.包含虚函数的类就是普通的类,而包含纯虚函数的类叫做抽象类
3.包含虚函数的类可以实例化对象,但是包含纯虚函数的类不能实例化对象
4.定义一个虚函数是为了使用基类指针来访问派生类中的这个函数.而定义一个纯虚函数代表这个函数没有实现
看完上述多态的讲解,有没有觉得多态很神奇啊?
那么它到底是怎么实现的呢?
你的小脑袋是不是也充满了疑惑呢?
好了就让我们一起来通过实验来验证一下吧
多态的实现原理
- 首先说明一下多态是通过虚基表和虚表指针实现的
- 虚表指针:存在于对象中,位于对象的前4/8个字节(与平台有关),指向虚表的首地址
- 虚表:存放函数指针,本质为指针数组,虚表本身存在(vs)代码段
- 子类继承父类的虚表,对于子类重写的函数,覆盖掉父类对应的虚函数
- 虚表中存放虚函数的地址,不存放普通函数的地址
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
你的答案是多少?若你的答案是4那就错了。正确的答案是8;
原因是该类在定义的过程中内部有虚函数的存在,那么它就产生了一张虚基表,而类大小中大小为8的另外4的字节就和虚基表有关,另外4个字节存放的是一个指向这个虚基表的指针.(32位平台下,指针大小为4个字节).这下你知道另外4个字节的来源了吧?
而这个虚基表本质上就是一个指针数组(存放指针的数组),用来存放类中虚函数的函数地址
光说不练假把式,还是来代码咱们看监视结果
#include<iostream>
using namespace std;
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
void Func1(){
cout << "Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
- 监视结果
-
从监视结果我们可以得到以下几点
1.在vs2013中的虚表指针是存在于对象地址的前面部分的
2.虚基表确实是一个指针数组
3.虚基表中存放的是虚函数的地址
4.派生类中没有重写的虚函数在派生类的虚基表中显示的地址和基类中的地址相同
5.派生中重写过的虚函数在派生类的虚基表中显示的地址和基类中的地址不同(已经发生了覆盖) -
根据以上几点,我们画出基类的模型图
那么派生类是如何继承基类的虚基表的呢?
- a.先将基类中的虚表内容拷贝一份到派生类虚表中
- b.如果派生类重写了基类中某个虚函数,再用派生类自己的虚函数覆盖虚表中基类的虚函数
- c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的后面。
上述过程清楚的指出了继承和重写过程发生了什么,那么我们现在就来说说多态是怎么实现的
- 还是来段代码
#include<iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
- 当p指向mike对象时,p->BuyTicket在mick的虚表中找到虚函数是Person::BuyTicket
- 当p指向johnson对象时,p->BuyTicket在johnson的虚表中找到虚函数是Student::BuyTicket
- 这样的话就可以实现不同的对象去完成同一行为时,展现出不同的形态了
上一篇: 动手实现编译器(二)——语法分析
下一篇: base64原理及其实现