目录
上一篇主要讲了多态的基本内容和使用,本篇文章将带领铁子们深入了解多态的底层原理,本文实验比较多,建议铁子们看完可以自己再实验实验,一定会收货颇丰。
class Person
{
public:
virtual void Buyticket()
{
cout << "全价票" << endl;
}
int _a;
};
class Student :public Person
{
public:
virtual void Buyticket()
{
cout << "半价票" << endl;
}
int _b;
};
int main()
{
cout << sizeof(Person) << endl;
return 0;
}
上面代码老铁们可以算一下Person的空间大小是多少?

答案出来了,是8,我们可以看一下正常没有虚函数的大小

上图我们发现正常的大小可能和大多数老铁算的一样是4,而有虚函数的是8,那么这多出来的4个字节用到哪里了呢?我们可以调试看一眼

经过调试观察,我们发现在对象的前面多了一个指针_vfptr,那么这个_vfptr叫做什么呢?这个指针我们叫做虚函数表指针(Virtual Function Pointer),指向虚函数表. 一个含有虚函数的类中至少有一个虚函数表指针指向虚函数表,虚函数的地址会存储在虚函数表中,那么子类的表中有什么呢,我们往下继续看。

观察上面,我们可以发现p和s所指向的虚函数表是不一样的,两者是独立的,子类的虚表中存储着自己重写后的虚函数的地址,这也是多态能够实现的重要原理,因此多态实现的重要条件之二必须通过父类的指针或者引用调用虚函数,大家想必也能够理解,以子类为例,父类的指针或者引用是对子类进行切片,因此指针和引用指向的仍是原来的空间,也因此_vfptr中存储的是子类重写的虚函数,借此可以明确区分子类和父类的虚函数调用。
反过来看,为什么不能传值调用呢?如果传值调用,就需要用到赋值重载,正常是不会拷贝虚表的,这样永远都是父类的虚表,多态就无法实现,但是如果我们要拷贝虚表呢,这样多态可能会实现,但是会造成混乱,如下面这种情况,p作为一个父类对象它的虚表是父类还是子类的呢?他会变成子类的,这正常吗,一个父类对象里用着子类虚表,很明显这不正常。
int main()
{
Person p;
Student s;
p = s;
return 0;
}
接下来我们来看一下为什么虚函数的重写也可以叫做覆盖
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _a=1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _b=2;
};
int main()
{
Person p;
Student s;
return 0;
}
我们以上面的代码为例,在上面的例子中我们可以看到,子类Studnet只重写了父类Person的Func1虚函数,那么我们进入调试模式看一看,Student和Person的虚表有什么不同。

通过观察上图,我们发现在子类中Func1由于我们重写后,所以父类虚表中Func1和子类虚表中Func1的虚函数地址是不一样的,而Func2子类则没有重写,父类虚表和子类虚表中的Func2的虚函数地址是一样的。
这是因为在子类継承父类后,子类对Func1进行了重写,所以函数虚表中父类原来的Func1就被子类重写的Func1的虚函数地址所覆盖。
因此虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖,重写是语法的叫法,覆盖是底层原理的叫法。
讲完重写的原理,接下来我们来探讨一下子类新建虚函数存放在什么位置?是父类的虚表中还是又创建了一个虚表存放?
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _a=1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _b=2;
};
int main()
{
Person p;
Student s;
return 0;
}
我们以上面代码为例来讲解这个问题,在上面代码中子类对象Studen对父类对象Person的Func1进行了重写,此外呢,我们在子类中又写了一个新的虚函数Func2,借此来观察Func2的存放位置。
我们可以先进行调试来观察虚表中有没有存储Func2,那么Func2难道没有被存储吗?这是不可能的,因为Student也有可能成为别的父类,因此其新建虚函数不可能不存储。

通过观察监视窗口,我们发现虚表中只存了Func1,真的是这样吗,要知道监视窗口是处理过的,我们可以再观察一下内存窗口

观察内存窗口,我们发现问题没有这么简单,第一行是Func1的地址,第二行的地址和Func1离得很近,那么这个有没有可能也是一个虚函数的地址?是Func2的地址呢?
接下来我们写一个打印虚表的小程序来证明一下

打印结果如下

观察打印结果我们发现,第二行的地址确实是Student类的新写的虚函数Func2的地址。
因此我们得出结论子类对象新写的虚函数尽管监视窗口看不到,但确实存到了虚表里面。
那么我们又有一个问题,虚表存放在哪里呢?
内存空间大概分为一下几个区,老铁们可以猜测一下虚表存放在那个区

这里我们用来试验的方法是,写创建四个变量,分别存于栈,堆,静态区,常量区,然后取其地址,将他们的地址和虚表的地址作对比,借此来推断虚表存储的位置。
class Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
int _a = 1;
};
class Student :public Person
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
virtual void Func2()
{
cout << "Func2" << endl;
}
int _b = 2;
};
int main()
{
int i;//栈
int* ptr = new int;//堆
static int a = 0;//静态区
const char* b = "cccccccccc";//常量区
Person p;
Student s;
printf("栈对象:%p\n", &i);
printf("堆对象:%p\n", ptr);
printf("静态区对象:%p\n", &a);
printf("常量区对象:%p\n", b);
printf("p虚函数表:%p\n", *((int*)&p));
printf("s虚函数表:%p\n", *((int*)&s));
return 0;
}
试验结果如下

通过实验,我们发现虚函数表的存储位置和常量区十分接近
由此可得,虚函数表存储的位置和虚函数存储的位置一样在代码段也就是常量区。
我们上面讲了这么多。那么多态的原理到底是什么呢?
实际上,多态实现的根本原理就是通过virtual声明存在多态,生成虚表指针,管理虚函数,如果访问的为虚函数,则通过指针/引用找到实际指向的实体,获取实体中的虚表指针,通过虚表指针访问虚表,在虚表中找到需要执行的虚函数指针,通过虚函数指针执行具体函数行为。
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
上面讲的都是以单继承为例,接下来我们来讲一下多继承的虚函数表
在说多继承的虚函数表之前,铁子们可以思考一个问题,多继承的子类中有几张虚表?
class Base1
{
public:
virtual void Func1()
{
cout << "Base1:Func1" << endl;
}
virtual void Func2()
{
cout << "Base1:Func2" << endl;
}
int _a=1;
};
class Base2
{
public:
virtual void Func1()
{
cout << "Base2:Func1" << endl;
}
virtual void Func2()
{
cout << "Base2:Func2" << endl;
}
int _b=2;
};
class Son :public Base1, public Base2
{
public:
virtual void Func1()
{
cout << "Son:Func1" << endl;
}
int _c = 3;
};
int main()
{
Son s;
return 0;
}
我们以上面代码为例子,来看一下多继承的内存模型

通过观察我们发现多继承内虚函数表有两张,内存模型如下

上面单继承时,子类新建虚函数存放在父类的虚表中,但是这个多继承有两个父类,那么他新建的虚函数地址存在哪里呢。
class Base1
{
public:
virtual void Func1()
{
cout << "Base1:Func1" << endl;
}
virtual void Func2()
{
cout << "Base1:Func2" << endl;
}
int _a=1;
};
class Base2
{
public:
virtual void Func1()
{
cout << "Base2:Func1" << endl;
}
virtual void Func2()
{
cout << "Base2:Func2" << endl;
}
int _b=2;
};
class Son :public Base1, public Base2
{
public:
virtual void Func1()
{
cout << "Son:Func1" << endl;
}
virtual void Func3()
{
cout << "Son:Func3" << endl;
}
int _c = 3;
};
typedef void(*VF_PTR)();
//typedef void(*)() VF_PTR;上面定义的效果就和这个类似,
//但是由于是函数指针,所以只能用上面的定义方式
void print(VF_PTR* table)
{
for (int i = 0; table[i] != nullptr; i++)
{
printf("[%d]:%p->", i, table[i]);
VF_PTR f = table[i];//保存函数地址
f();//对函数地址调用
}
cout << endl;
}
int main()
{
Son s;
return 0;
}
这里我们可以用之前打印虚表的方式来验证。但是这里有一个问题,那就是Base2的指针要如何找到呢?我们这里采取的方法是,切片,对Base2切片,指针会自动偏移到Base2的位置。

由上述结果我们可以发现在多继承中子类新建虚函数的地址存储在第一个父类的虚表中。
上述实验结果不知到老铁们有没有发现一个问题,那就是我们在子类中重写了Func1,但是Base1和Base2两张虚表中Func1的地址却不一样,这是为什么?
我们可以将Base1 Base2分别切片出来调用Func1来观察各自汇编来看看各自的走向。

汇编实验结果如下

通过观察结果我们发现,ptr1调用Func1是直接调用,而ptr2调用Func1的过程确实如此艰难尤其是中间还有个sub 8,这是为什么呢?
其实是调用Func1最终要靠*this指针来调用,注意*this指针指向的是Son类,ptr1之所以可以直接调用,是因为他切片出来的ptr1指向的地方和*this是相同的,所以可以直接调用Func1,而ptr2则不行,ptr2指向的是Base2的位置,与*this所指向的位置没有重合,会对其进行封装,将ptr2所指向的位置调整到*this指向的位置,再调用Func1,也就是中间有一个-8的指令起的就是这个作用。

上面就是多态原理部分的所有内容,上面的实验均为博主完成,希望铁子们能够有所收获
在VMware16.2.4安装Ubuntu一、安装VMware1.打开VMwareWorkstationPro官网,点击即可进入。2.进入后向下滑动找到Workstation16ProforWindows,点击立即下载。3.下载完成,文件大小615MB,如下图:4.鼠标右击,以管理员身份运行。5.点击下一步6.勾选条款,点击下一步7.先勾选,再点击下一步8.去掉勾选,点击下一步9.点击下一步10.点击安装11.点击许可证12.在百度上搜索VM16许可证,复制填入,然后点击输入即可,亲测有效。13.点击完成14.重启系统,点击是15.双击VMwareWorkstationPro图标,进入虚拟机主
我使用的是遗留数据库,所以我无法控制数据模型。他们使用了很多多态链接/连接表,就像这样createtableperson(per_ident,name,...)createtableperson_links(per_ident,obj_name,obj_r_ident)createtablereport(rep_ident,name,...)其中obj_name是表名,obj_r_ident是标识符。因此链接的报告将按如下方式插入:insertintoperson(1,...)insertintoreport(1,...)insertintoreport(2,...)insertint
1.问题描述使用Python的turtle(海龟绘图)模块提供的函数绘制直线。2.问题分析一幅复杂的图形通常都可以由点、直线、三角形、矩形、平行四边形、圆、椭圆和圆弧等基本图形组成。其中的三角形、矩形、平行四边形又可以由直线组成,而直线又是由两个点确定的。我们使用Python的turtle模块所提供的函数来绘制直线。在使用之前我们先介绍一下turtle模块的相关知识点。turtle模块提供面向对象和面向过程两种形式的海龟绘图基本组件。面向对象的接口类如下:1)TurtleScreen类:定义图形窗口作为绘图海龟的运动场。它的构造器需要一个tkinter.Canvas或ScrolledCanva
目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式
有这个:classEventtrueenduser=User.create!我可以:Event.create!(:historizable=>user)但我不能:Event.where(:historizable=>user)#Mysql2::Error:Unknowncolumn'events.historizable'in'whereclause'我必须改为这样做:Event.where(:historizable_id=>user.id,:historizable_type=>user.class.name)更新重现问题的代码:https://gist.github.com/fg
我有一个UserType和一个可以是Writer或Account的userable。对于GraphQL,我想也许我可以像这样使用UserableUnion:UserableUnion=GraphQL::UnionType.definedoname"Userable"description"AccountorWriterobject"possible_types[WriterType,AccountType]end然后像这样定义我的用户类型:UserType=GraphQL::ObjectType.definedoname"User"description"Auserobject"fie
目录一、安装包链接二、安装详细步骤1.安装Wireshark和WinPcap2.安装OracleVMVirtualBox3.安装ensp三、安装后注册四、启动路由器出现40错误怎么解决一、安装包链接二、安装详细步骤链接:https://pan.baidu.com/s/1QbUUYMOMIV2oeIKHWP1SpA?pwd=xftx提取码:xftx1.安装Wireshark和WinPcap找到Wireshark安装包所在文件夹,双击它,按照以下步骤安装。2.安装OracleVMVirtualBox找到OracleVMVirtualBox安装包所在文件夹,双击它,按照以下步骤安装。注:可自定义安装
文章目录认识unity打包目录结构游戏逆向流程Unity游戏攻击面可被攻击原因mono的打包建议方案锁血飞天无限金币攻击力翻倍以上统称内存挂透视自瞄压枪瞬移内购破解Unity游戏防御开发时注意数据安全接入第三方反作弊系统外挂检测思路狠人自爆实战查看目录结构用il2cppdumper例子2-森林whoishe后记认识unity打包目录结构dll一般很大,因为里面是所有的游戏功能编译成的二进制码游戏逆向流程开发人员代码被编译打包到GameAssembly.dll中使用il2ppDumper工具,并借助游戏名_Data\il2cpp_data\Metadata\global-metadata.dat
Nginx安装1.官网下载Nginx2.使用XShell和Xftp将压缩包上传到Linux虚拟机中3.解压文件nginx-1.20.2.tar.gz4.配置nginx5.启动nginx6.拓展(修改端口和常用命令)(一)修改nginx端口(二)常用命令1.官网下载Nginxhttp://nginx.org/en/download.html这里我下载的是1.20.2版本,大家按需下载对应稳定版即可2.使用XShell和Xftp将压缩包上传到Linux虚拟机中没有XShell可以参考《Linux操作系统CentOS7连接XShell》3.解压文件nginx-1.20.2.tar.gz1)检查是否存
三大公有云厂商,香港地区主机测评一、ping时延比对(厦门电信本地测试):Ping时延测试腾讯云阿里云华为云延迟率最低时延44ms,最高72ms,平均46ms47.242段:最低时延59ms,最高204ms,平均107ms最低时延45ms,最高93ms,平均47ms丢包率丢包率小有的ip段丢包率较大每个段都会有概率丢包阿里云:47.242段:最低时延59ms,最高204ms,平均107ms,有的ip段丢包率较大8.210段:最低时延64ms,最高232ms,平均119ms,丢包率较好腾讯云:最低时延44ms,最高72ms,平均46ms,丢包率小华为云:最低时延45ms,最高93ms,平均47m