梳理虚函数表、多态原理、动静态绑定的知识
目录
一、虚函数表
二、多态的原理
三、动态绑定和静态绑定
一、虚函数表
在学习多态原理之前,我们需要了解一下虚函数表的概念
我们先一起来看下下面这段代码
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
通过测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr指针放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
虚函数表指针和虚表的关系我们在后文中会详细讲解。
我们先来学习一下虚表的知识,这个派生类中的表(虚表)究竟是什么呢?它里面又放了些什么呢?我们接着往下分析
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
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:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
通过观察和测试,我们发现了以下几点问题:
1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,其中包括虚表指针和父类的成员变量,另一部分是自己的成员。
2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中;b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数;c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
6. 这里还有几个很容易混淆的问题:虚函数存在哪的?虚表存在哪的?虚函数是不是存在虚表中?虚表是不是存在对象里? 答:注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数是一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现,虚表在vs下是存在于代码段的。
【总结】对象->虚表指针->虚表(虚函数指针的指针数组)->虚函数指针->虚函数。
二、多态的原理
通过上面的学习,我们对虚表有了大概的了解。那么多态的原理到底是什么?和虚表又有什么样的关系呢?接下来让我们一起来看下面的图例,这里Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket
代码如下
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;
}
运行上面的代码并打开监视窗口,我们可以得到
1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是要对象(基类)的指针或引用调用虚函数。有兴趣的小伙伴可以思考一下为什么?答案就是虚函数覆盖才能输出不同的结果,通过基类的指针或引用调用虚函数,才能准确完成下面的流程:对象->虚表指针->虚表->虚函数指针->虚函数。
5. 再通过下面的汇编代码分析,我们可以得出,满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
//多态调用
void Func(Person* p)
{
p->BuyTicket();
}
int main()
{
Person mike;
Func(&mike);
mike.BuyTicket();
return 0;
}
下面展示的是多态调用的反汇编
// 以下汇编代码中与问题不相关的都被去掉了
void Func(Person* p)
{
...
p->BuyTicket();
// p中存的是mike对象的指针,将p移动到eax中
001940DE mov eax, dword ptr[p]
// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
001940E1 mov edx, dword ptr[eax]
// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
00B823EE mov eax, dword ptr[edx]
001940EA call eax
// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来
//以后到对象的中取找的。
001940EC cmp esi, esp
}
下面展示的是普通函数调用的反汇编
//普通函数调用
int main()
{
...
// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数
// 普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
mike.BuyTicket();
00195182 lea ecx, [mike]
00195185 call Person::BuyTicket(01914F6h)
...
}
三、动态绑定和静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。【我们接触到的多态多为动态多态】。
3. 第二节中展示的反汇编代码汇编代码很好的解释了什么是静态(编译器)绑定和动态(运行时)绑定。
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的,因此它为动态绑定。而一般的函数调用,在程序编译期间确定了程序的行为,是静态绑定。
以上就是本篇文章的所有内容,如果对您有帮助,不妨点赞、收藏、关注,感谢您的阅读。