网站的购物车怎么做,做婚恋网站投入多少钱,wordpress 书站,电子商务网站开发这书不出版了吗目录 一、什么是多态
二、多态的定义及实现
1.多态构成条件
2.虚函数的重写和协变
虚函数重写的两个例外#xff1a;
2.1协变
2.2析构函数的重写 #xff08;析构函数名统一处理成destructor#xff09;
3.重载、覆盖(重写)、隐藏(重定义)的对比
4.final 和 overr…
目录 一、什么是多态
二、多态的定义及实现
1.多态构成条件
2.虚函数的重写和协变
虚函数重写的两个例外
2.1协变
2.2析构函数的重写 析构函数名统一处理成destructor
3.重载、覆盖(重写)、隐藏(重定义)的对比
4.final 和 override
三、抽象类
四.多态的原理
1.虚函数表
2.多态的原理
2.1虚表指针里的内容
2.2引用和指针如何实现多态
2.3普通类接收为什么实现不了多态 3.虚函数表存放位置
五.单继承和多继承关系的虚函数表
1.单继承中的虚函数表
2.多继承中的虚函数表
3.菱形继承和菱形虚拟继承
做一道题吧 一、什么是多态 多态的概念通俗来说就是多种形态具体点就是去完成某个行为当不同的对象去完成时会 产生出不同的状态。 举个栗子比如买票这个行为当普通人买票时是全价买票学生买票时是半价买票军人 买票时是优先买票。
二、多态的定义及实现
1.多态构成条件 在继承中要构成多态还有两个条件 1. 必须通过基类的指针或者引用调用虚函数 2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写 class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }
};
void Func(Person p)
{ p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
} 注意接受对象为父类的指针或者引用你传递的是父类就调用父类的函数传递的是子类就调用子类的函数 在重写基类虚函数时派生类的虚函数在不加virtual关键字时虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范不建议这样使用 2.虚函数的重写和协变 上面例子中我们实现了虚函数的重写(覆盖) 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同)称子类的虚函数重写了基类的虚函数。 虚函数重写的两个例外
2.1协变 派生类重写基类虚函数时与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用派生类虚函数返回派生类对象的指针或者引用时称为协变。 这里不仅仅可以返回当前基类和子类的类型还可以返回其他有继承关系的类和类型。 2.2析构函数的重写 析构函数名统一处理成destructor
首先我们来看看析构函数不处理成virtual的情况
我们本义是想让p1调用Person的析构p2先调用Person的析构在调用Student的析构但是这里并没有调用Student的析构只析构了父类就可能发生内存泄漏。 这是为什么呢 因为这里发生了隐藏~Person()变为 this-destructor() ~Student()为this-destructor() 编译器将他们两个的函数名都统一处理成了destructor因此调用的时候只看自身的类型是Person就调用Person的函数是Student就调用Student的函数根本不构成多态这并不是我们期望的哪样。 我们给析构函数添加上virtual
发现子类对象Student对象就能正常析构了
注意::析构函数加virtual是在new场景下才需要 其他环境下可以不用 3.重载、覆盖(重写)、隐藏(重定义)的对比 4.final 和 override
在添加父类虚函数后面添加final代表不能再被重写 final修饰类代表不能被继承 override代表必须要重写虚函数如果没有重写便会报错 三、抽象类 在虚函数的后面写上 0 则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类也叫接口类抽象类不能实例化出对象。派生类继承后也不能实例化出对象只有重写纯虚函数派生类才能实例化出对象。纯虚函数规范了派生类必须重写另外纯虚函数更体现出了接口继承。 注意这里的包含只要类里面有一个有纯虚函数就是抽象类就无法实例化对象间接强制派生类重写。 普通函数的继承是一种实现继承派生类继承了基类函数可以使用函数继承的是函数的实现。虚函数的继承是一种接口继承派生类继承的是基类虚函数的接口目的是为了重写达成多态继承的是接口。所以如果不实现多态不要把函数定义成虚函数。 四.多态的原理
1.虚函数表 以下代码环境在X86中涉及到的指针是4个字节 我们定义一个Base类里面有虚函数还有一个变量int按照我们之前学习到了这里Base类的大小应该是4个字节图中确是8个字节 为什么会发生这种现象呢
用监视窗口看一下 除了_b成员还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面这个跟平台有关)对象中的这个指针我们叫做虚函数表指针(v代表virtualf代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针因为虚函数 的地址要被放到虚函数表中虚函数表也简称虚表。 其实应该叫__vftptr多个t代表table 我们多添加几个虚函数看看这个表里面的内容是怎么样的
可以发现虚函数会放到虚函数表中普通函数不会并且表里面的内容是一个数组是函数指针数组 2.多态的原理 有了虚函数表的概念我们可以尝试通过虚函数表去找到多态的原理
下面是测试代码
class Person {
public:virtual void BuyTicket() { cout 买票-全价 endl; }virtual void fun(){}
private:int a;
};
class Student : public Person {
public:virtual void BuyTicket() { cout 买票-半价 endl; }
private:int b;
};
void Func(Person* p)
{p-BuyTicket();
}
int main()
{Person p;Student s;Func(p);Func(s);return 0;
}
2.1虚表指针里的内容 从图中我们可以看到在内存1里面输入p可以找到p的地址 因为p的第一个内容就是__vfptr因此p的地址也是__vfptr的地址那么我们通过__vfptr的地址就可以找到虚函数表里面的内容因此我们在内存2里面输入__vfptr的地址我们便找到了两个虚函数的地址。 去找s的虚表虚函数也同理 为什么我们要这么麻烦的去找呢监视窗口不是可以看到吗 这是因为VS2022的监视窗口可能会骗人不一定百分百准确使用内存是一定准确的。 通过上面的图片我们可以提炼出如下内容 注意这里Student类和Teacher的类表里的第二个虚函数地址是一样的因为B类没有重写第二个虚函数因此继承下来了。 为什么第一个虚函数不一样呢 因为子类重写后覆盖掉了这也是为什么重写被称作覆盖的由来 2.2引用和指针如何实现多态
可以分析为什么多态可以实现指向父类调用父类函数 指向子类调用子类函数
传递父类通过vftptr找到虚函数表的地址再去找到虚函数的地址有了虚函数的地址便可以去call这个虚函数 传递子类首先会进行切割 将子类的内容切割掉父类再去接受这个数据了一样会有vftptr是子类的vftptr再去找到虚函数的地址有了虚函数的地址便可以去call这个虚函数。
这样就完成了多态。 附加一句对于下面指出的代码他其实并不清楚自己所存放的虚函数表指针是父类的还是子类的他只是蠢蠢的去调用这个虚函数而已 2.3普通类接收为什么实现不了多态
依然是之前的代码参数部分不再是指针和引用而是用普通类我们发现这里没有实现多态。 我们将代码做一个小改动方便观看区别 给Person类添加上一个构造函数
class Person {
public:Person(int x 0):a(x){}virtual void BuyTicket() { cout 买票-全价 endl; }virtual void fun(){}
private:int a;
}; 我们给Person类构建出的p对象传10他会调用构造函数将10赋值给成员a。
当执行Func(p)函数时注意观察此时的a的值为10虚函数表地址为0x004f9bfc
当执行Func(s)函数时注意观察此时的a的值为0虚函数表地址也为0x004f9bfc。
从上面的分析可以看出Func(s)传递时切割出子类中父类的那一份成员会拷贝给父类但是没有拷贝虚函数表指针。
为什么只拷贝成员不拷贝虚函数表指针呢C祖师爷为何这么设计 我们可以用反证法 假设 拷贝构造 和 赋值重载 会拷贝虚函数表指针 那么我们写出如下代码运行后输出结果就应该为 两个 买票-半价 了因为不管指向的累人只管你所存储的数据 这样就不能保证多态调用时指向父类父类调用的是父类的虚函数。因为还有可能经过一些操作变成子类的虚函数 也许上面的问题并不那么致命你说你自己控制好一点不就行了。
那么析构呢要知道虚函数表中还可能有析构函数如果我写出如下代码阁下又该如何应对 Person* p new Person;Student s;*p s;delete p;
这个时候你会发现Person父类的对象delete会去调用子类Student类的析构函数这样会引发很多不可控制的事情。因此祖师爷帮我们处理了
这里会有点绕不理解也没关系 只要知道只有引用和指针才能触发多态就行 最后再补充两点 同类对象的虚表一样。 如果子类没有重写父类的虚函数那么他们的虚函数表指针不同但里面的内容相同 3.虚函数表存放位置 我们通过代码来打印各个区地地址可以判断虚函数表存放位置
class Base {
public:virtual void func1() { cout Base::func1 endl; }virtual void func2() { cout Base::func2 endl; }
private:int a;
};void func()
{}int main()
{Base b1;Base b2;static int a 0;int b 0;int* p1 new int;const char* p2 hello world;printf(静态区:%p\n, a);printf(栈:%p\n, b);printf(堆:%p\n, p1);printf(代码段:%p\n, p2);printf(虚表:%p\n, *((int*)b1));printf(虚函数地址:%p\n, Base::func1);printf(普通函数:%p\n, func);
} 注意打印虚表这里vs x86环境下的虚表的地址是存放在类对象的头4个字节上。因此我们可以通过强转来取得这头四个字节 b1是类对象取地址取出类对象的地址强转为(int*)代表我们只取4个字节再解引用就可以取到第一个元素的地址也就是虚函数表指针的地址 从图中可以发现代码段和虚表地址非常接近存在代码段的常量区。
虚函数和普通函数地址非常接近存在代码段。
五.单继承和多继承关系的虚函数表
1.单继承中的虚函数表
我们使用如下代码测试一下单继承的虚函数表。
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;
};
class X : Derive
{
public:virtual void fun3(){ cout X::func3 endl; }
};int main()
{Base b;Derive d;X x;return 0;
}
明明Base类和X类应该有4个虚函数监视窗口发现表里面竟然只有2个这真的很奇怪。
VS下的监视窗口不一定准确我们用之前的办法打开内存来看看
通过输入__vfptr的地址我们成功找到了里面虚函数的地址。并且我们还发现似乎下面那两个地址跟上面两个非常接近我们可以合理的设想下面两个地址也是虚函数指针。 在vs环境下虚函数表里面的虚函数以0结尾 也很符合之前我们观察到的。 我们可以通过这一点来打印虚表。 下面我们typedef了虚函数表指针 typedef void(*VFTPTR)(); 可以通过这个函数指针数组来打印里面的虚函数这个打印函数终止条件就是 !0 传递的参数内容跟前面我们分析的差不多只是躲了一个强转PrintVFPtr((VFTPTR*)*(int*)b) ; 因为后面的 *(int*)b 虽然内容是地址但是表现形式是一个整形需要强为 (VFTPTR*) 。 在*((int*)d) 就会取到vTableAddress指向的地址就得到虚函数的地址了。
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;
};
class X : Derive
{
public:virtual void func3(){ cout X::func3 endl; }
};typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i 0; a[i] ! 0; i){printf(a[%d]:%p\n, i, a[i]);}cout endl;
}int main()
{Base b;Derive d;X x;PrintVFPtr((VFTPTR*)*(int*)b);PrintVFPtr((VFTPTR*)*(int*)d);PrintVFPtr((VFTPTR*)*(int*)x);return 0;
}
我们运行一下如上代码便可以打印出虚函数表里面的内容 但是目前我们还是可以质疑这个地址到底是不是虚函数地址我们可以打印虚函数的内容看一下。下面给打印代码略作修改因为a[i]里面存放的就是函数指针因此我们可以选择直接调用。
void PrintVFPtr(VFTPTR a[])
{for (size_t i 0; a[i] ! 0; i){printf(a[%d]:%p-, i, a[i]);VFTPTR p a[i];p();}cout endl;
} 这下真的可以看到结果了可以确认虚函数都会放到虚表里面。并且监视窗口可能是个大骗子要小心他
2.多继承中的虚函数表
这里选择多继承如图 这里我们代码 Base1有虚函数func1和func2 Base2也有虚函数func1和func2。 derive继承了Base1和Base2并重写了虚函数func1还有虚函数func3 typedef void(*VFTPTR)();void PrintVFPtr(VFTPTR a[])
{for (size_t i 0; a[i] ! 0; i){printf(a[%d]:%p-, i, a[i]);VFTPTR p a[i];p();}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;PrintVFPtr((VFTPTR*)(*(int*)d));return 0;
}
使用监视窗口观察到Dervie类对象d有两个虚函数表那么问题就来了他自身的虚函数func3会放在哪一张表里面呢 我们还是选择打印来观看发现这里只能打印出第一张虚表的内容并且func3在第一张虚表里第二张虚表有没有func3呢好像也需要打印出来观看而好像我们对于第二张虚表不好打印我们有没有什么方法可以打印第二张虚表里面的内容呢 答案是有的有很多种方法这里我们介绍两种 第一种将d强转为Base1*这样1就会跳过整个Base1就刚好到达了Base2类的开始再进行之前的强转便可以打印了。 第二种方法直接将d赋值给Base2* ptr;这样Base2会进行切片操作于是ptr就直接指向了Base2的虚函数表依然就行之前的强转操作便可以打印了。 主函数代码如下
int main()
{Derive d;PrintVFPtr((VFTPTR*)(*(int*)d));//写法1//PrintVFPtr((VFTPTR*)(*(int*)((Base1*)d 1)));//写法2Base2* ptr d;PrintVFPtr((VFTPTR*)(*(int*)ptr));return 0;
} 我们打印出来看一看这两个表是什么情况 这里可以得出结论了Derive类对象的虚函数会放在多继承中继承的第一个类的虚函数表里即Base1类虚函数表
问题又来了为什么多继承要搞多个虚表呢 还是之前的继承关系请看如下图和代码如果不搞多个虚表那么p1去调用func1()p2也去调用func1()如若d没有重写func1()那么这个多态就会紊乱调用的都是那一个func1()了而不是p1调用Base1的func1()p2调用Base2的func1()了。因此我们多继承就搞多个虚表才不会出现紊乱的问题 int main()
{Derive d;Base1* p1 d;PrintVFPtr((VFTPTR*)*(int*)p1);p1-func1();Base2* p2 d;PrintVFPtr((VFTPTR*)*(int*)p1);p2-func1();return 0;
}
运行一下代码 调用的func1()函数确实没问题实现了多态但是我们发现两张虚表里func1()函数的地址竟然不同这是为什么我们重写的func1()两个都实现了啊调用的也肯定是同一个函数按道理来说应该地址是一样的为什么地址不一样呢我们尝试用反汇编来看一下 p1调用func1的反汇编 call了eax走到了eax里面的jmp指令再走一步就到了func1()函数 p2调用反汇编首先也是call了eax再jmp但是这个jmp竟然没有走到func1()函数而是先执行了 sub ecx,8 指令后面再jmp了两下才走到了func1()函数。why sub ecx,8 指令代表了什么为什么调用的同一个函数汇编代码却不相同 我们尝试破解一下这个指令到底是为什么首先Derive类重写了func1()函数既然是Derive类的func1()函数那么他所存放的*this指针类型肯定也是Derive。那么在这个函数里我是不是可以调用父类的非私有成员或者函数 那如果我执行Base2* p2 d; 这就会对d对象进行切片p2对象按道理来说只能看到Base2的虚函数表和自己存放的数据b2但是又有可能在func1()函数访问Base1或者Base2的非私有成员和函数这样好像就不太好调用了如果我将p2的地址放在Derive类对象地址的地方那是不是就会很方便访问了。 sub ecx,8 此时我们在俩看这句指令从下面图片可以看出来exc存放的是p2的地址sub 是减去的意思 这局指令是 exc - 8也就是p2的地址-8那么我现在的地址是什么没错啦现在的地址就回到的d的地址这样一来是不是就可以调用整个Derive类还有继承下来的数据啦 这局代码的本质就是修正this指针指向derive对象 那么为何 Base1* p1 d; p1却没有减去值回到d的地方了呢笨蛋因为p1的地址就是d的地址Derive首先继承的就是Base1而d的首元素就是Base1的虚函数表指针啊。因此我自己就在这里我还减去什么我传过去直接开用就完事了 我们成功的管中窥豹明白了多继承虚函数表的情况还有为什么重写多个父类的同名虚函数地址为何不一样了如果地址一样就不可能一起回到Derive类对象的地址因为类里的元素总会有先后顺序
3.菱形继承和菱形虚拟继承
我的建议是别碰饶了我吧投降了。 做一道题吧 这道题选B很难相信 首先B类型的对象p去调用test(); test()是B类继承下来的但是里面默认存放的this指针依然是A*将一个B类型的指针传给A类型的指针会发生多态B类里面的func()是重写了A类的func() (A类func()为虚函数B类重写了可以不写virtual)。 注意重写的关键点仅仅是重写了A类的实现而前面的那些声明依然是调用的A类的声明因此给到的val默认值是1调用了B类的函数实现 所以输出B-1 ,