西安站,wordpress使用iis重定向到目录,建材网站模板,建设网站商品怎么弄#x1f3ac;慕斯主页#xff1a;修仙—别有洞天 ♈️今日夜电波#xff1a;世界上的另一个我 1:02━━━━━━️#x1f49f;──────── 3:58 #x1f504; ◀️ ⏸ ▶️ ☰ 慕斯主页修仙—别有洞天 ♈️今日夜电波世界上的另一个我 1:02━━━━━━️──────── 3:58 ◀️ ⏸ ▶️ ☰ 关注点赞收藏您的每一次鼓励都是对我莫大的支持 目录 多态的原理 首先理解虚函数表
正式理解多态的原理
一些拓展
多态对于引用、指针和对象
虚表的拓展 多继承中的虚函数表
先了解如何打印虚函数表
然后理解多继承中的虚函数表
方法一加上base1的大小
方法二切片
结论 多态的原理
首先理解虚函数表 class Base
{
public:
virtual void Func1()
{
cout Func1() endl;
}
private:
int _b 1;
}; 请问sizeof(Base)的大小为多少 答案为x64下16字节x86下8字节 解析如下 Base类中包涵着int类型的成员变量占4字节而由于有虚函数因此会有一个虚函表的指针vfptr因此根据内存对齐得到上述答案。 这时就会有疑惑了虚函数表指针和虚函数表是什么呢 如下通过监视窗口可以看到vfptr指向了一个数组也就是虚函数表而数组中存储着虚函数指针 继续分析我们在上述代码的基础上增加代码 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下是存在代码段的Linux g下大家自己去验证 正式理解多态的原理 见以下代码 class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }virtual void vv() { cout 打折 endl; }int a 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }int b 1;
};
void Func(Person p)
{p.BuyTicket();
}
int main()
{Person m;Func(m);Student s;Func(s);return 0;
} 从监视中很明显的看到子类继承了父类的虚函数表但是很明显的看到虚函数表中我们的BuyTicket()的虚函数指针地址改变了而vv()确没有改变这就很明显了因为BuyTicket()被重写了而vv()没有而重写也有另外一个名字覆盖当我们重写了虚函数那么就会覆盖对应虚函数在虚函数表中的指针 内存方面观察 可以看到其中vfptr中存储的地址是发生了改变的也就是说我们可以根据这个地址找到新的一张虚函数表在前面我们学习过“切片”的概念我们知道当以父类的类型去访问子类的类型会发生“切片”使得只访问父类的类型的空间也就是说我们只访问上图中蓝色框内的内容再结合上上张图监视中如果子类重写了虚函数则虚函数表中虚函数指针改变。当我们调用对应的虚函数时就会调用子函数的虚函数而不是父类的虚函数这就是多态实现的原理因此多态中指向父类调用父类指向子类调用子类 一些拓展
多态对于引用、指针和对象 为什么多态只允许引用和指针呢我们都知道引用的底层实现实际上还是指针多态的实现就是指向子类对象中切割出来的那一部分而对象只会拷贝子类对象中父类的那一部分但是不会拷贝虚函数表指针。为什么呢因为如果允许虚函数表指针的拷贝会造成二义性如下 int main()
{Person m;Student s;ms;Func(m);Func(s);return 0;
} 如果对象可以像引用和指针一样那么当拷贝了虚函数表指针后你会发现我们实现不了多态中指向父类调用父类指向子类调用子类的场景。也会造成析构函数调用调错等等的错误。 虚表的拓展 如果子类与父类中不重写虚函数子类与父类的续虚函数表一样吗不一样他们存在不同的位置虽然他们的内容是一样的同类对象的虚函数表一样吗一样 总结不同的类不会共用虚函数表只有相同的类才会共用虚函数表 多继承中的虚函数表
先了解如何打印虚函数表 我们都知道虚函数表是一个函数指针数组并且数组最后一位是以nullptr结尾的。因此我们可以根据该特性打印虚函数表 typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (size_t i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
} 例子打印单继承的虚函数表 class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout Derive::func1 endl; }virtual void func3() { cout Derive::func3 endl; }virtual void func4() { cout Derive::func4 endl; }
private:int b;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (size_t i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}cout endl;
}int main()
{Base b;Derive d;VFPTR * vTableb (VFPTR*)(*((int*)b));PrintVTable(vTableb);VFPTR* vTabled (VFPTR*)(*((int*)d));PrintVTable(vTabled);return 0;
}思路取出b、d对象的头4bytes就是虚表的指针前面我们说了虚函数表本质是一个存虚函数指针的指针数组这个数组最后面放了一个nullptr。需要注意的是这是在x86的运行环境下的如果是x64则需强转为long long 1.先取b的地址强转成一个int*的指针 2.再解引用取值就取到了b对象头4bytes的值这个值就是指向虚表的指针 3.再强转成VFPTR*因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。 4.虚表指针传递给PrintVTable进行打印虚表 5.需要说明的是这个打印虚表的代码经常会崩溃因为编译器有时对虚表的处理不干净虚表最后面没有放nullptr导致越界这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案再编译就好了。 然后理解多继承中的虚函数表 概念 C中的多继承是指一个派生类可以同时从多个基类派生从而继承它们的属性和行为。 多继承是面向对象编程中一个重要的概念它允许一个类继承多个其他类的成员。这样做有几个目的 代码重用多继承可以提高代码的重用性因为派生类可以访 问所有基类的公有成员和保护成员。功能组合通过继承多个类派生类可以将不同基类的功能组合在一起形成更复杂的功能。 然而多继承也可能带来一些问题如菱形继承问题这可能导致二义性。为了解决这个问题C引入了虚基类的概念。 如下为一段多继承的代码可以看到drive继承了base1和base2 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;
};那么他的虚函数表又是什么样的呢如下
可以看到正如我们猜测的那样它包含着两张虚函数表然而我们在derive中重写了func1()函数以及额外添加了一个func3()函数但是并没有在监视中显示这是因为编译器并没有让你实际的看到也就是说编译器在骗人实际上就是在其中的一张表当中可以理解为监视的一个bug。我们通过上述打印虚函数表可以看到具体的效果注意此为x64环境下 typedef void(*VFPTR) ();
void PrintVTable(VFPTR* vTable)
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout 虚表地址 vTable endl;for (size_t i 0; vTable[i] ! nullptr; i){printf( 第%d个虚函数地址 :0X%x,-, i, vTable[i]);VFPTR f vTable[i];f();}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()
{cout base1: endl;base1 b;PrintVTable((VFPTR*)(*(long long*)b));cout base2: endl;base2 c;PrintVTable((VFPTR*)(*(long long*)c));cout derive 表1: endl;derive d;PrintVTable((VFPTR*)(*(long long*)d));//printvft((vfunc*)(*(int*)((char*)dsizeof(base1))));cout derive 表2: endl;base2* ptr d;PrintVTable((VFPTR*)(*(long long*)ptr));
} 得到第一个虚基表的方法很简单因为第一个虚基表的指针正好处在前8个字节处只需要向上面一样进行强转即可如果要找到第二个虚基表则有如下两种方法 方法一加上base1的大小 printvft((vfunc*)(*(int*)((char*)dsizeof(base1)))); 也就是加上sizeof(base1)即可但是需要注意的是d的类型是Derive在d后变为Derive* 的一个指针1 跳转的是Derive类型的字节大小而我们想要的是每次1跳转1个字节所以需要强制转换char* 方法二切片 base2* ptr d;PrintVTable((VFPTR*)(*(long long*)ptr)); 把d利用切片的原理给到ptr然后再按照上面强转的原理找到虚基表即可 结论 如下为上面代码的运行结果 可以看到上面的图示我们可以得出相应的结论多继承中重写的虚函数以及新增的虚函数都是在第一个虚基表当中进行修改以及增加的如果重写的虚函数在其他基类中也有对应的虚函数那么继承下来的虚基表也需要重写。 更加详细的图解如下 这里又引申出来一个问题为什么其derive继承的两个虚基表中func1()的地址不同呢这里就需要从汇编的角度进行理解了: 在以上的代码的基础上调试下面这段代码(x86环境下)通过反汇编可得结果如下 derive d;base1* p1 d;p1-func1();base2* p2 d;p2-func1(); 从上图的图示可以看到p1只经过了一次jmp就找到了derive中的func1()的地址而p2则是经过了多次的jmp才找到func1()地址。这是因为p1的调用的地址恰好与derive* 类型的this指针的地址是重叠的因此不需要去找这个地址而p2要经过蓝框中的“8字节的偏移”才能找到this指针可以看到有ecx标识ecx是存储this指针的才能指向derive对象的开始才可以调用derive的func1()毕竟fun1也可能调用成员函数、成员变量等等。 总结这里是为了修正this指针指向derive对象这里调用的是derive重写的func1()。 感谢你耐心的看到这里ღ( ´ᴗ )比心如有哪里有错误请踢一脚作者o(╥﹏╥)o 给个三连再走嘛~