携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第12天,点击查看活动详情
💦 多态原理
✔ 测试用例一:
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票-全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "买票-半价" << endl;
}
//int _a = 0;
//string _b = "dancebit";
};
void Func(Person& p)
{
p.BuyTicket();
}
void f()
{
cout << "f()" << endl;
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
//Person p = Johnson;
f();//普通函数
return 0;
}
-
为什么多态的条件之一是重写。因为只有完成了重写,子类的虚表里面才会被覆盖成子类的虚函数。

-
为什么多态的条件之二是父类的指针或引用调用虚函数。首先这里的虚表一定是用来被调用的,如果父类的指针指向父类对象,就去父类的虚表中找虚函数;如果父类的指针指向子类对象,看到的就是子类中父类的那一部分,虽然对指针而言看到的与父类对象一样,也是在类似的位置找虚函数调用,但是这时调用的是子类的虚函数,因为父子类无论是否完成虚函数重写,都有各自独立的虚表,且这里重写后子类会把自己的虚函数覆盖拷贝下来的虚函数。所以这里达到的目的就是指向谁,调用谁,这里 p.BuyTicket(),它并不知道也不识别指向的是父类还是子类 (就像虚继承中也不识别,而是统一取偏移量,然后找基类),你传不同的对象去调用 Func,执行的是同样的指令,都是去找头上的 4 个字节,也就是虚表指针,然后找虚函数调用。

为什么条件之二是对象就不行了 ❓
因为如果是指针或引用调用虚函数,这里的切片行为:指向父类就是父类;指向子类,看到的是子类中继承下来的父类部分,虽然看到的是父类部分,但是虚表是子类的虚表,虚函数也是子类的虚函数。
如果是对象,虽然能编译通过,但是没有构成多态。原因是如果是对象调用虚函数,那么对于传父子类对象都是拷贝构造,此时,父类对象会拷贝构造成员,但不会处理虚表,也不需要处理,因为它们指向的是同一张虚表;子类对象会先切片出父类部分,然后再拷贝构造成员,子类的虚表不会处理,因为要是把子类的虚表也拷贝了,如果给你一个父类对象,你都不知道父类对象中的虚表内容是什么,因为父类可能指向父类虚表,也可能父类被子类切片过,然后指向子类虚表,一个父类对象指向子类虚表当然不合理,这时就会导致一个父类指针指向一个父类对象,调用的是子类的虚函数。

普通函数的调用和多态的调用 ❓
普通函数的调用是编译或链接时确定地址,有两种情况,当看到 f 的调用时,编译期间,往上找到函数的定义,这里就直接成 call + 地址;编译期间,往上找到函数的声明,这里就先 call + ???,链接时再其它文件中查找。
多态的调用是运行时确定地址,编译器会先检查是否多态,如果是就按多态的规则执行,它会去指向对象中的前 4 个字节指向的虚函数表中找到虚函数的地址。如果不是多态,就在编译时确定地址。

//简单瞅下汇编: //[p]就是取p指向的内容,这里把p移动到eax中 00BD25D8 mov eax, dword ptr [p] //[eax]就是取eax指向的内容,这里就是把指向对象的头4个字节(虚表指针)移动到edx中 00BD25D8 mov edx, dword ptr [eax] //[edx]就是取edx指向的内容,这里把虚表中所存储的虚函数的地址移动到eax中 00BD25E2 mov eax, dword ptr [edx] //call eax中虚函数的指针,这里就可以看出多态的调用不是在编译时确定的 00BD25E4 call eax