餐饮网站建设案例,网页制作代码简单,2021外贸网站有哪些,网站建设培训多少钱#x1f496;作者#xff1a;小树苗渴望变成参天大树#x1f388; #x1f389;作者宣言#xff1a;认真写好每一篇博客#x1f4a4; #x1f38a;作者gitee:gitee✨ #x1f49e;作者专栏#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法#x1f384; 如 果 你 … 作者小树苗渴望变成参天大树 作者宣言认真写好每一篇博客 作者gitee:gitee✨ 作者专栏C语言,数据结构初阶,Linux,C 动态规划算法 如 果 你 喜 欢 作 者 的 文 章 就 给 作 者 点 点 关 注 吧 文章目录 前言一、虚函数表二、多态的原理三、解决疑惑四、多继承中的虚函数表五、总结 前言
今天我们开始讲解多态的底层原理相信这篇博客会让你对多态的理解会更加的透彻话不多说我们开始进入讲解 一、虚函数表
我们在多态语法的时候一直强调要构成虚函数我们来看看虚函数在内存是怎么存储的
// 这里常考一道笔试题sizeof(Base)是多少
class Base
{
public:virtual void Func1(){cout Func1() endl;}
private:int _b 1;
};我们发现我们对象里面存放两个内容成员变量和一个指针地址刚好大小就是8。 通过观察测试我们发现b对象是8bytes除了_b成员还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数的地址要被放到虚函数表中虚函数表也简称虚表。那么派生类中这个表放了些什么呢我们接着往下分析 我们往这这个Base里面增加先的内容
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;
}派生类对象d中也有一个虚表指针d对象由两部分构成一部分是父类继承下来的成员,成员包括自身的变量和虚表里面的虚函数。还有一部分是自己的成员。基类b对象和派生类d对象虚表是不一样的这里我们发现Func1完成了重写所以d的虚表中存的是重写的Derive::Func1所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法覆盖是原理层的叫法。另外Func2继承下来后是虚函数所以放进了虚表Func3也继承下来了但不是虚函数所以不会放进虚表。虚函数表本质是一个存虚函数指针的指针数组一般情况这个数组最后面放了一个nullptr。 但是最好每一次都重新生成一下解决方案不然你在是之前的基础上修改在调试的可能就看不到效果 有虚函数的类创建不同对象共有的是同一张虚表 总结一下派生类的虚表生成a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。 我们猜想这可能是vs监视窗口的一个bug,一会验证内存当中多出来的地址是不是我们特有的虚函数。 二、多态的原理
为什么需要的是基类的指针和引用调用虚函数 1为什么是基类的 我们在继承的第二节讲到继承的赋值子类对象可以赋值给父类的对象指针和引用父类可以接收自身的也可以接收子类的而子类只能接收自己的如果是强转可能会造成一系列问题所以多态规定只能是基类的 2为什么是指针和引用去调用 是将地址赋值给父类指针变量引用底层也就是指针道理是一样的指向什么对象就去调用什么哪个对象的函数了这样就实现同一行为展现出不同的形态 3为什么虚函数要进行重写 如果不重写就达不到覆盖的效果那么子类的虚表还是存的是父类里面的虚函数虽然你指向子类的虚表但是虚表里面指向的函数地址还是父类的虚函数重写了就完成了覆盖重写就相当于对父类虚函数的重新定义放在了子类的虚表里面了。 那么对象为什么不行 原因是对象赋值时要调用拷贝构造或者赋值运算符的子类会进行切片将父类的那一部分拷贝给父类对象此时子类的虚表指针如果也拷贝过去了会影响父类对象里面的虚表指针那样就乱套了指针和引用并不会改变父类对象里面的变量和虚表指针的。所以就不允许这样的赋值就算拷贝过来也是属于父类里面本身的属性拷贝过来就把d1里面的_b给拷贝过去了虚表还是父类本身的虚表那么调用的时候就还是父类的函数就调不到子类的。 三、解决疑惑
1. 虚函数存在哪的虚表存在哪的
答虚函数存在虚表虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意虚表存的是虚函数指针不是虚函数虚函数和普通函数一样的都是存在代码段的只是他的指针又存到了虚表中。另外对象中存的不是虚表存的是虚表指针。
证明一下 我们猜想有四个位置栈堆数据段(静态区)代码段(常量区) 我们通过代码来演示 通过测试我们发现虚表的地址离常量区最近也就是代码段有的书上说是在静态区但是自己测试之后才知道应该离常量区最近。
2. 为什么监视窗口没有特有的虚函数内存当中多出来的地址是不是我们猜想的结果
在上面第二节的第六小点我们发现子类特有的属性居然不在虚表里面二内存中却多出来了一个地址我们猜测是那个特有的虚函数但是也不能确定所以我们只能想办法验证 我们需要写一个函数将函数数组指针里面的地址取出来然后再调用 我们确实把地址取出来接下来直接通过地址来调用 确实和我们猜想的是一样的 测试代码
class A
{
public:virtual void fun1() { cout A::fun1 endl; }
};
class B :public A
{
public:virtual void fun1() { cout B::fun1 endl; }virtual void fun2(){ cout B::fun2 endl; }
};typedef void(*Fun_c)();void Adderss(Fun_c arr[])
{for (int i 0; arr[i] ! nullptr; i){printf([第%d个地址]%p\n, i 1, arr[i]);arr[i]();}
}
int main()
{B b;
// 思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr
// 1.先取b的地址强转成一个int*的指针
// 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针
// 3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
// 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最
//后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案再编译就好了。Adderss((Fun_c*)(*(int*)b));return 0;
}四、多继承中的虚函数表
刚才我们说的都是单继承中的把单继承的原理搞懂多继承的处理方法其实思路是一样的有虚表但是因为是多继承继承下来的不是一个类的成员所以再处理方面还是有所不同的接下来我来给大家介绍我们的多继承的虚表是什么样的 我们来看测试代码
class A
{
public:virtual void funa1() { cout A::funa1 endl; }virtual void funa2() { cout A::funa1 endl; }//基类特有的虚函数
};
class B
{
public:virtual void funb1(){ cout B::funb1 endl; }virtual void funb2() { cout B::funb1 endl; }//基类特有的虚函数
};
class C :public A,public B
{
public:virtual void funa1() { cout C::funa1 endl; }//重写A类的虚函数virtual void funb1(){ cout C::funb1 endl; }//重写B类的虚函数virtual void func1() { cout C::func1 endl; }//派生类特有的虚函数void func2() { cout C::func2 endl; }//不是虚函数
};
int main()
{C c;return 0;
}通过上面图的结果来看我们多继承的派生类中有两张虚表而且猜想派生类特有的虚函数是放在第一张表中此时我们还是按照上面方法去验证 果然和我们猜想是一样的 我们再来看看下面的案例
class A
{
public:virtual void fun1() { cout A::fun1 endl; }virtual void fun2() { cout A::fun2 endl; }
};
class B
{
public:virtual void fun1(){ cout B::fun1 endl; }virtual void fun2() { cout B::fun2 endl; }};
class C :public A,public B
{
public:virtual void fun1() { cout C::fun1 endl; }virtual void fun3(){ cout C::fun3 endl; }//特有的虚函数
};int main()
{C c;//因为C类的一个fun1是重写了A和B类的虚函数所以指向谁就调用谁A* a c;a-fun1();B* b c;b-fun1();return 0;
}通过这个案例我们又可以猜想是不是地址实际就一个其中以恶搞是直接找到的另一个做了一下修改最后也能找到因为实际想想同一分代码用两个地址存显然有点浪费空间了所以编译器也不允许这样的事情发生带着这个疑问我们通过汇编来看看是什么样的 因为fun1的真正地址只有一份有三种调用fun1的方式其中两种就是上面画图演示的多态调用还有一种是c对象自己去调用而多态调用是去虚表中找自己就直接调用最终都需要指向c对象才能去调用a和c对象的指向刚好重叠了所以也类似于直接调用而b对象需要修正才能去调用。 上面我们说的都不是菱形继承的多继承前面也说过尽量不要设计出菱形继承所以我们去研究也没有什么意义所以菱形继承、菱形虚拟继承我们的虚表我们就不看了一般我们也不需要研究清楚因为实际中很少用。
五、总结
今天讲解的知识还是以往大家下来自己去测试一样尽量在测试前清理一下解决方案不然会有影响。相信大家知道了底层原理之后对于多态的时候应该不在陌生了就是由于这一系列的底层要求多态的形成条件才有那么多也明白了为什么要哪些条件了这篇博主花了很长时间帮助自己梳理了一遍知识也把知识分享给大家啊希望大家多多支持