不备案 国内网站,网页制作全过程视频,网络广告投放,室内设计专业就业前景文章目录 前言一、多继承的虚函数表二、菱形继承与菱形虚拟继承的虚函数表1.菱形继承2.菱形虚拟继承的虚函数表 三、抽象类1.抽象类的概念2.接口继承与实现继承 总结 前言
其实关于单继承的虚函数表我们在上一篇文章中已经说过了#xff0c;就是派生类中的虚表相当于拷贝了一… 文章目录 前言一、多继承的虚函数表二、菱形继承与菱形虚拟继承的虚函数表1.菱形继承2.菱形虚拟继承的虚函数表 三、抽象类1.抽象类的概念2.接口继承与实现继承 总结 前言
其实关于单继承的虚函数表我们在上一篇文章中已经说过了就是派生类中的虚表相当于拷贝了一份父类的虚表然后派生类中将重写的虚函数进行覆盖。如果派生类中也有自己的虚函数但是并没有与父类构成重写那么这个虚函数也是在虚表中的不过不同的是vs2022的监视窗口是不会显示自己的虚函数的。但是我们可以在内存中观测到这个虚函数并且我们进行验证使用了一些非正常手段去调用这个函数从而证明这个地址确实是这个虚函数的地址。还有一点需要注意的是虚表是存储在代码段的即常量区。需要注意我们验证的方法就是打印出每个区域的地址进行对比从而推测出虚表所在的地方。 一、多继承的虚函数表
我们先看下面这段代码猜一猜运行结果为多少
class Base1 {
public:virtual void func1() { cout Base1::func1 endl; }virtual void func2() { cout Base1::func2 endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout Base2::func1 endl; }virtual void func2() { cout Base2::func2 endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};
int main()
{cout sizeof(Derive) endl;return 0;
}运行结果如下所示结果是20 那么为什么是20呢我们画出如下的对象模型
即Derive这个对象由于继承了Base1和Base2那么首先他的Base1由于先继承所以在前面而Base1中有一个虚表指针还有一个int类型的变量所以占四个字节然后内部对齐后在与Base2和int进行对其于是最终结果就是20了 这样的样子与我们之前的单继承是十分相似的派生类是不会单独产生虚表的派生类都是继承了父类直接使用派生类中的父类的虚表即可。所以这里有两个虚表
所以我们就更能深刻里面多态的一个条件是父类的指针或者引用了。
如下面所示是监视窗口中的场景。 可以看到两个基类的func1都被重写了。如下所示 不过这里的问题主要还是在于监视窗口并不可信因为它这里并没有func3这个虚函数的地址。
根据单继承的思路我们这里的func3这个虚函数是会存储在某一个虚表中的那么是存储在Base1还是Base2还是两个都存储呢
我们可以使用之前的方案强行找出虚表的地址进行调用函数来研究
typedef void(*FUNC_PTR)();
void Print_VFT(FUNC_PTR* table)
{for (int i 0; table[i] ! nullptr; i){printf([%d]:%p-, i, table[i]);table[i]();}cout endl;
}
class Base1 {
public:virtual void func1() { cout Base1::func1 endl; }virtual void func2() { cout Base1::func2 endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout Base2::func1 endl; }virtual void func2() { cout Base2::func2 endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }
private:int d1;
};
int main()
{Derive d;int vft1 *(int*)d;//int vft2 *(int*)((char*)d sizeof(Base1));Base2* ptr d;int vft2 *(int*)ptr;Print_VFT((FUNC_PTR*)vft1);Print_VFT((FUNC_PTR*)vft2);return 0;
}关于上面的代码我们需要注意的是vft2的值的取法即Base2中的虚表的地址如何取出来。我们可以自己去计算偏移量从而得出结果当然我们也可以直接运用切片的特性直接拿到了地址然后在类型转换即可
运行结果如下 可以看到func3的地址其实是存储在Base1的虚表中的。
所以**多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中 ** 如上是Derive的虚表模型了但是我们会发现一个很奇怪的问题。那就是明明两个基类的func1都被重写了但是他们的地址为什么不一样呢 所以这里出现了一个很奇怪的问题虽然他们两个函数的地址不一样但是最终却调用到同一个函数去了。
按照单继承的理论这个func1只有一个地址才是比较合理的毕竟这个func1只生成一份才是最好的。
为什么这里重写了func1但是Base1和Base2的地址不一样呢
面对这个问题网上绝大多数是没有答案的。于是我们最好的办法就是看汇编
上面的问题换言之就是下面这段代码两个调用结果一样但是过程出现了不一样的情况 我们直接进入汇编模式 如上是需要注意的事件call之前都是在想办法取出虚表中的地址我们可以直接观测eax的值可以发现确实是Base1虚表中这个fun1函数的地址。
然后我们继续往深走即进入call的内部我们就会进入到一个jmp函数 也就是说我们call的地址只是一个jmp的地址一般jmp后面的才是真正的地址
如下所示果然如此jmp后面才会真正的调用func1函数 即如下所示的调用关系 我们再来观测后半部分代码注意这里我的代码前半部分的代码地址改变了这里其实只是我的一个小失误操作不过不影响后序现象 这里与前面的现象是一样的我们继续深入调查 在这里我们发现了一些需要注意的问题这里的jmp的地址首先和前面jmp指令的地址是不一样的其次jmp后面所跳转的地址也是不一样的。
现在我们只能继续深入调查了。 在这里我们发现它没有直接像前面的一样直接跳转到函数而是先对ecx减去8了。
要知道ecx一般存储的是this指针的值。所以这里是对this指针减去8
减去以后ecx的值发生了变化 然后我们继续进入jmp内部 这个时候我们发现jmp的地址着不就是Base1虚表中的func1的地址吗
所以现在就来了一个大折返直接与前面的一样了 也就是说这里只是多走了几步
总之无非就是后半部分多走了很多的代码。但是最终的结果是一样的。 那么编译器究竟为什么要这么做呢我们知道中间多出来几步中有一步是让this指针减去8。那么为什么要减去8呢
我们也许会注意到我们的d的对象模型ptr2和ptr1正好相差8 我们先想清楚func1是谁的成员函数其实是Derive的成员函数。而我们想要调用Derive的成员函数那么这个this指针应该指向的是d对象ptr1和ptr2就是充当着this指针而我们现在呢ptr1指向的恰好就是起始地址刚好巧了所以它就不需要动。ptr2指向的是Base2的那一部分。它的功能就是找到对应的虚表找到对应的函数地址以后我们也不能直接对其进行call调用因为实际调用成员函数的时候必须要传ecx的那么我们就需要将他调整到d的起始位置。
而且我们也注意到了这里的黄色部分刚好就是把ptr2的值交给了ecx这里就说明了ecx存储的是this指针 相应的ptr1的调用和ptr2是类似的先要去把this指针给处理好 ptr1的好处在于它的this指针本身就是d的起始位置本身就是正确的所以这个this指针不需要进行调整所以直接call函数即可
而ptr2可惜它指向的并非起始位置所以得先绕个弯子先把ecx给修正为正确的this指针才能去调用函数。
这样的话我们就已经彻底的辨析了下面的代码了三个的本质调用的是一个函数不过前两个是多态调用后面是普通调用。ptr2的调用由于this指针的问题所以需要进行修正。
int main()
{Derive d;Base1* ptr1 d;ptr1-func1();Base2* ptr2 d;ptr2-func1();Derive* ptr3 d;ptr3-func1();return 0;
}也就是说他们的调用可以分为两部分一部分是传this指针第二部分就是call地址。
当然我们这里的都是vs2022的行为不同的编译器可能有不同的效果
那么比如说有没有可能我们可以让多继承中的两个地址是一样的呢其实是可以的我们只要调用之前去修正this指针就可以了。因为vs采用的是调用的过程中修正所以他们的地址只能不一样了。
二、菱形继承与菱形虚拟继承的虚函数表
1.菱形继承
我们先简单的写一个菱形继承如下所示下面也刚好包括了虚函数的样例
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a;
};
class B : public A
{
public:int _b;
};
class C : public A
{
public:int _c;
};
class D : public B, public C
{
public :int _d;
};
int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
}其实但对于菱形继承还是比较简单的。因为菱形继承我们大可以抽象为两个类分别虚继承然后一个类多继承即可
我们根据菱形继承的知识不难画出如下对象模型 调试窗口的运行结果如下 所以菱形继承其实和多继承是一样的
即便是每个类里面都有一个虚函数也是一样的因为继承以后B使用A里面的虚表C使用A里面的虚表D使用B和C的虚表。对象模型里面存储的是虚表指针所以对象模型并未发生改变改变的只是虚表指针指向的内容就是虚表。根据单继承和多继承的规则B里面的虚函数并未构成重写则直接衔接在A的虚函数表后面。C同理D中的虚函数则放在B的虚表中即B中的A虚表 我们可以验证一下
typedef void(*FUNC_PTR)();
void Print_VFT(FUNC_PTR* table)
{for (int i 0; table[i] ! nullptr; i){printf([%d]:%p-, i, table[i]);table[i]();}cout endl;
}
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a;
};
class B : public A
{
public:virtual void func2(){cout B::func2() endl;}int _b;
};
class C : public A
{
public:virtual void func3(){cout C::func3() endl;}int _c;
};
class D : public B, public C
{
public:virtual void func4(){cout D::func4() endl;}int _d;
};
int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;int vft1 *(int*)d;C* ptr d;int vft2 *(int*)ptr;Print_VFT((FUNC_PTR*)vft1);Print_VFT((FUNC_PTR*)vft2);return 0;
}可见确实如此。
但是以上是没有发生重写/覆盖的情况下的。我们可以试一下发生了重写覆盖的情况
如下图所示是B重写/覆盖了A的情况下也是很好理解的其实就是相当于B的虚函数地址覆盖了A的虚函数地址。D并不会在意B里面A的细节它只关心虚表中的函数是否产生了重写/覆盖。如果是的话则覆盖即可。没有就往后续 我们继续观察当A与D产生了重写但A没有与B产生重写的条件下可见与我们前面所说的是一致的 当A、B、D都产生了重写的情况如下 当A、B、D和A、C产生了重写如下所示 2.菱形虚拟继承的虚函数表
我们使用与前文类似的代码不过我们这次使用菱形虚拟继承
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a;
};
class B : virtual public A
{
public:int _b;
};
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
}我们根据菱形虚拟继承的对象模型不难得出以下的内存图 我们可以使用内存图来进一步观察 这里就是因为菱形虚拟继承会将B和C中的A都放到了公共部分
此时的按照菱形虚拟继承的内存分配来看是没有什么大问题的但是当我们B和C同时对A的虚函数进行了重写的时候由于是菱形虚拟继承。所以都会让A给放到公共部分实际上是B和C共享的A。两个都一起重写导致编译器不知道什么该听哪一个的所以就报错了 主要还是因为B和C都想要去重写这个A才导致的问题。而如果只是一个菱形继承的话就不会出现这个问题因为各自重写各自的即可。
对于上面的情况我们有两种方案去处理第一种是只保留一种重写即可
如下所示就是我们只保留了C的重写不会发生冲突所以就不会报错了 第二种方案就是让D在来一个重写这样的话 反正无论B和C是否重写都要听D的重写函数了。 上面的这些原因其实都是因为只有A有一张虚表才导致的。最终上图中的虚表里面最后就只有D的虚函数了。
当然我们或许会以为B和C的重写就没有意义了。其实不是的当我们想要一个B或者C类对象的时候他们的重写就有意义了。
上面其实还是菱形虚拟继承中比较简单的情况事实上菱形虚拟继承是更加复杂的当我们在B和C里面又添加了一些虚函数这些虚函数指针又该放哪里呢都放A里面的虚表吗
其实不是的这里B和C又会各自生成一张虚表它们自己的虚函数存储在他们自己的虚表里面。因为按照一开始的A的虚表是B和C共享的如果两个都往A的虚表里面塞其实不太合适的。
class A
{
public:virtual void func1(){cout A::func1() endl;}int _a;
};
class B : virtual public A
{
public:virtual void func1(){cout B::func1() endl;}virtual void func2(){cout B::func2() endl;}int _b;
};
class C : virtual public A
{
public:virtual void func1(){cout C::func1() endl;}virtual void func2(){cout C::func2() endl;}int _c;
};
class D : public B, public C
{
public:virtual void func1(){cout D::func1() endl;}int _d;
};
int main()
{D d;d.B::_a 1;d.C::_a 2;d._b 3;d._c 4;d._d 5;return 0;
}如上图所示是内存窗口的模样那么我们可以看到B和C里面有两个指针一个是虚基表指针一个是虚表指针。那么究竟哪个是虚表指针哪个是虚基表指针呢我们可以测试一下 其实单纯看里面的数据我们大概可以猜测到第一个是虚表指针第二个是虚基表指针
对于虚基表里面的内容 它里面存储的是偏移量第一个是-4第二个是18可见第一个指针是为了找到该部分的起始位置第二个指针是为了找到A的部分
那么如果我们给D有自己单独的虚函数呢D会额外创建虚表吗其实不会的因为D完全可以已经存在的虚表就够了。我们可能会以为放入共享的A的虚表不过如果按照多继承的角度去理解也有可能会放入B的虚表。 三、抽象类
1.抽象类的概念 在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承 。 至于为什么起这个抽象的名字我们可以理解为这个类在现实世界中没有实体。所以不能实例化出对象。而且由于派生类继承了抽象类它里面也包含了纯虚函数那么它自然也不能实例化出对象了。派生类如果真的想要实例化出对象我们可以使用重写的方式这样的话它里面的这个纯虚函数就被覆盖就没有纯虚函数了自然就可以实例化出对象了
class Car
{
public:virtual void Drive() 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout Benz-舒适 endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout BMW-操控 endl;}
};
void Test()
{Car* pBenz new Benz;pBenz-Drive();Car* pBMW new BMW;pBMW-Drive();
}
int main()
{Test();return 0;
}如下是由于包含纯虚函数导致不能实例化出对象的情形 对于抽象类的多态我们可能更多是应用于如下场景 那么现在有一个问题Car类有虚表吗其实Car类甚至都没有实例化出对象是根本不可能有虚表的。只有Benz和BMW类才有虚表。
其实纯虚函数的作用就是强制了派生类的重写因为如果不重写的话要虚函数其实也没有什么其他用处了。
它与override的区别就是override则是检查派生类中的虚函数是否完成了重写
两者还是有一些差距的一个是在基类的一个是在派生类的。
2.接口继承与实现继承
普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数 。 总结
本篇文章着重讲解了多继承与菱形继承的虚表以及抽象类的使用等方法。也正如抽象类的名字一样本节内容确实比较抽象。愿可以为读者带来帮助