目录
1.多态的原理
(1)引入
(2)函数指针
(3)对象模型
(4)虚函数表
[1]虚函数表图例
[2]验证虚函数表
(5)多态原理
(6)动态绑定与静态绑定
2.单继承和多继承的虚函数表
(1)单继承中的虚函数表
[1]对象模型
[2]虚函数表
(2)多继承中的虚函数表
[1]对象模型
[2]虚函数表
本文主要是通过编译器的内存窗口和监视窗口来探究多态的原理,以及含有虚函数的类的对象模型,单继承的含有虚函数的类的对象模型,多继承的含有虚函数的类的对象模型。
在这一节需要用到函数指针,以及指向函数指针的指针,如果对这个不了解的话,建议先查一下资料或者看一下本文中的简单讲解。
(本文代码在win10系统的vs2019的x86模式验证)
首先看一下这个含有虚函数的类的大小。按照常理,函数不在对象中存储,那么这个对象的大小应该是4,但实际结果是8,比预想的多了四个字节。这个场景似曾相识,在虚拟继承中见过,会不会也是类似的原理?后面通过它的对象模型来理解。
代码一:
//代码一
#include "iostream"
using namespace std;
class Base {
public:
int a;
public:
virtual void f1() { cout << "f1()" << endl; }
virtual void f2() { cout << "f2()" << endl; }
virtual void f3() { cout << "f3()" << endl; }
virtual void f4() { cout << "f4()" << endl; }
};
int main() {
Base bas;
bas.a = 1;
cout << sizeof(bas);//8
}
函数名就是函数的地址。
要在本文理解多态的原理,必须要使用函数指针,因为监视窗口中显示的内容不全,我们需要在内存窗口中验证,需要使用函数指针。
函数指针:函数类型的指针。void (* fr)(),这就是指向返回值为空,参数列表为空的函数的指针,fr是指针变量。
使用函数指针调用函数:(*fr)()
函数指针类型:void (*)(),这是上面那个函数指针的类型,这是因为语法的原因才是这个样子。
为函数指针取别名:typedef void (*Pr)() 这是为返回值是空,参数列表是空的函数指针类型取了别名Pr。因为加了关键字typedef,所以它已经不是一个变量名了,而是一个类型名。
指向函数指针的指针:先来看一个指向int类型指针的指针。如果一个变量中存储的是int类型的指针,那么我们用指针访问这个变量就需要使用二级指针。(就像:我们用一级指针访问整型变量)
代码二:
//代码二
int a = 10;
int* pa = &a;
int** ppa = &pa;
那么指向函数指针的指针也跟他类似, 如果一个数组中存储函数指针,我们就需要二级函数指针指向数组。需要先理解这个,不然等会有些地方会迷糊。
代码一的对象模型如下图,前四个字节就是多出来的四个字节,里面存储了一个地址,后四个字节是a变量。
在编译过程中,如果没有定义构造函数,编译器会自动生成并调用,为前四个字节赋值。若显式定义,编译器会对其进行修改,目的就是对多出来的四个字节赋值。
可以在内存一中看到,bas对象空间中前四个字节已经被赋值。
多出来的四个字节称为虚函数表指针(虚表指针),位置在对象的前四个字节,虚表指针指向的空间称为虚函数表(虚表),里面存储的是每个虚函数的入口地址,函数地址按照声明顺序存储。但是监视窗口显示的内容不全,实际在所有虚函数地址后,还有四个字节存储的是0。这个零就是这个虚函数表结束的标志,可以在内存窗口中查看到。
这是代码一的虚函数表示意图,内存1窗口是对象空间的内容,内存二是虚表空间的内容,可以看到需表空间最后是0。
要在程序中验证虚函数表就需要用到函数指针,因为虚表中存储的是函数的名字(函数名就是函数地址),我们可以通过(*函数名)()的方式调用函数。
代码三:
//代码三
void Prin() {
cout << "hello" << endl;
}
int main() {
(*Prin)();
}
问题来了,怎么获取虚表中的函数地址呢?
首先需要获得虚表指针,内存窗口中能看见,可是程序中怎么得到?
先来看:int类型的指针可以访问的空间是sizeof(int)大小的,double类型也同理。那么我们现在只有类类型的指针怎么办?我们可以把类类型指针强转为int类型,这样它就只能指向前四个字节的空间了。然后用函数二级指针类型的指针接收。为什么要用函数二级指针类型的指针?因为这个地址指向的空间中保存的是函数的地址。指向一级指针的指针当然要用二级指针。
我们这里给的都是返回值为void 参数为空的函数类型,其函数指针类型是void (*){}。声明一个该类型的变量:void (* pt){}。
这个类型使用的时候太长,不方便,可以用typedef给它起别名 typedef void(*P){},这样就可以用P来表示原本的类型。
代码四:
//代码四
#include "iostream"
using namespace std;
class Base {
public:
int a;
public:
virtual void f1() { cout << "f1()" << endl; }
virtual void f2() { cout << "f2()" << endl; }
virtual void f3() { cout << "f3()" << endl; }
virtual void f4() { cout << "f4()" << endl; }
};
typedef void (*P)();
int main() {
Base bas;
bas.a = 1;
int num = *(int*)&bas;
P* p = (P*)num;
while (*p) {
(*p)();
p++;
}
}
首先,取得对象的地址&bas,然后把该地址强转为(int*)类型,然后对int*解引用就得到了前四个字节的内容num,num需要用P*类型接收,为什么?因为num中的地址指向的空间中保存的是函数指针,要用二级指针指向一级指针。
为什么可以这么写循环条件,因为这就是0的作用。当p访问到最后一个空间,解引用结果是0,循环不成立,则退出。
下面是打印结果。
这里来看一下多态的实现原理,这里需要用到汇编。这种函数的调用方式有没有觉得奇怪,对象中不是没有函数吗,为什么可以用指针指向函数。这是因为成员函数有this指针,这里相当于是把ba指针赋值给了this指针。
代码五:
//代码五
#include "iostream"
using namespace std;
class Base {
public:
int a;
public:
void f1() { cout << "Base:f1()" << endl; }
virtual void f2() { cout << "Base:f2()" << endl; }
virtual void f3() { cout << "Base:f3()" << endl; }
};
void Prin(Base* ba) {
ba->f1();
ba->f2();
ba->f3();
}
int main() {
Base bas;
Prin(&bas);
}
可以看到普通函数f1编译时就被确定调用哪个类的函数,而虚函数是在程序运行时通过各种操作得到对应的地址,然后使用call指令调用。
静态绑定:也叫静态多态,早绑定。利用普通函数实现的都是静态多态,如:函数重载,模板。他们都会在编译时进行推演,找到合适的函数在编译时就确定调用哪个函数。
动态:就像上图中一样,需要在程序运行时通过各种操作在虚表中查找虚函数地址。
VS2019中,编译时就会把虚函数地址放在虚表中 ,把虚表存放在代码段区域。
探索多态中的对象模型需要在内存窗口查看,因为监视窗口中显示不完全,这里需要使用到函数指针。子类与基类不共享同一张虚表。
代码六:此代码是单继承,子类与基类中都有各自的虚函数。
//代码六
#include "iostream"
using namespace std;
class Base {
public:
int a;
public:
virtual void f1() { cout << "Base:f1()" << endl; }
virtual void f2() { cout << "Base:f2()" << endl; }
virtual void f3() { cout << "Base:f3()" << endl; }
};
class Son : public Base {
public:
int b;
public:
virtual void f3() { cout << "Son:f3()" << endl; }
virtual void f4() { cout << "Son:f4()" << endl; }
};
typedef void(*P)();
int main() {
Son so;
so.a = 1;
so.b = 2;
Base ba;
ba.a = 3;
int mum = *(int*)&ba;
P* pb = (P*)mum;
cout << "父类函数调用" << endl;
while (*pb) {
(*pb)();
pb++;
}
int num = *(int*)&so;
P* pa = (P*)num;
cout << "子类函数调用" << endl;
while (*pa) {
(*pa)();
pa++;
}
}
打印结果:通过打印结果可以看出来,当子类重写基类中的虚函数后,就不会调用基类中对应的虚函数了。
继承后父类模型前四个字节是虚表指针,后四个字节是元素a。
继承后子类的对象模型,前四个字节是虚表指针,然后是从父类中继承的普通成员,子类新增在下。
子类与父类不是同一张虚表。
子类和继承自父类的虚函数地址存在需表中,按照声明顺序排列。当子类重写父类虚函数后,子类的虚表中会用子类重写后的虚函数地址替换掉父类的虚函数地址。(仔细看内存2和内存4的截图,是不是发现内存4中的前两个函数指针和内存2中前两个一样,第三个被改变了)
代码七:
//代码七
#include "iostream"
using namespace std;
class Base1 {
public:
int a;
public:
virtual void B1() { cout << "Base1:B1()" << endl; }
};
class Base2 {
public:
int b;
public:
virtual void B2() { cout << "Base2:B2()" << endl; }
};
class Son : public Base1 ,public Base2{
public:
int c;
public:
virtual void S3() { cout << "Son:S3()" << endl; }
};
typedef void(*P)();
void Prin_Base1(Base1& b1) {
P* p = (P*)*(int *)&b1;
while (*p) {
(*p)();
p++;
}
}
void Prin_Base2(Base2& b2) {
P* p = (P*)*(int*)&b2;
while (*p) {
(*p)();
p++;
}
}
int main() {
Son so;
so.a = 1;
so.b = 2;
so.c = 3;
cout << "so对象中B1部分的虚表" << endl;
Prin_Base1(so);
cout << "----------" << endl;
cout << "so对象中B2部分的虚表" << endl;
Prin_Base2(so);
}
打印结果:
可以看到,每继承一个类,都会多增加类中变量和一个虚表指针。
子类自己的虚函数地址被放在了第一个虚函数表中,可以看到内存2窗口中有两个函数指针,但是第二个虚函数表中没有增加。(这是为了节省资源)