C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
温馨提示:这篇文章已超过399天没有更新,请注意相关的内容是否还可用!
C++:多态究竟是什么?为何能成为面向对象的重要手段之一?
- 前言
- 一、多态的概念
- 二、多态的定义及实现
- 2.1 多态的构成条件
- 2. 2 虚函数
- 2.3 虚函数的重写
- 2.3.1 虚函数重写的例外1:协变(基类与派生类虚函数返回值类型不同)
- 2.3.2 虚函数重写的例外2:析构函数的重写(基类与派生类析构函数的名字不同)
- 2.4 C++11 override 和 final
- 2.4.1 final
- 2.4.2 override
- 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
- 三、抽象类
- 3.1 概念
- 3.2 接口继承和实现继承
- 四、虚函数及虚函数表
- 4.1 虚函数表
- 4.2 虚函数重写覆盖虚表
- 4.3 虚函数存在哪的?虚表存在哪的?
- 五、多态原理
- 5.1 多态调用过程及原理
- 5.2 多态运行时确定?
- 5.3 动态绑定与静态绑定
- 六、单继承和多继承关系的虚函数表
- 6.1 单继承中的虚函数表
- 6.2 多继承中,子类新增虚函数存入那个虚表?
- 6.3 总结
- 七、菱形继承、菱形虚拟继承
前言
本篇博客基于VS2019X86环境下,后续关于多态原理相关验证都是基于vsX86环境,而虚表本质上是一个虚函数指针数组,在X86环境下VS编译器会在数组最后放一个unllptr!!
一、多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
二、多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
但在继承中要构成多态还有两个条件:(后续会具体介绍其细节)
- 必须通过基类的指针或者引用调用虚函数。
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
比如Student继承了Person。Person对象买票全价,Student对象买票半价:
class Person { public: virtual void BuyTicket()//被virtual修饰的类成员函数称为虚函数 { cout public: virtual void BuyTicket()//对基类虚函数重写 { cout people.BuyTicket(); } int main() { Person p; Func(p);//涉及赋值兼容规则,后续会介绍。 Student s; Func(s); return 0; } public: //BuyTicket()为虚函数 virtual void BuyTicket() { cout public: virtual void BuyTicket() { cout public: virtual void BuyTicket() { cout p.BuyTicket(); } int main() { Person p; Student s; Func(p); Func(s); return 0; } }; class B : public A {}; class Person { public: virtual A* f() { return new A; } }; class Student : public Person { public: virtual B* f() { return new B; } }; public: virtual ~Person() { cout public: virtual ~Student() { cout Person* p1 = new Person; Person* p2 = new Student; delete p1; // delete p1编译器处理为: p1-destrutor() + operator delete(p1) delete p2; // delete p2编译器处理为: p2-destrutor() + operator delete(p2) return 0; } public: virtual void Drive() final {} }; class Benz :public Car { public: virtual void Drive() { cout public: virtual void Drive() {} }; class Benz :public Car { public: virtual void Drive() override { cout public: virtual void Drive() = 0;//纯虚函数 }; class Benz :public Car { public: virtual void Drive() { cout public: virtual void Drive() { cout Car* pBenz = new Benz; pBenz-Drive(); Car* pBMW = new BMW; pBMW-Drive(); } public: virtual void Func1() { cout Base b1; cout public: virtual void Func1() { cout cout cout public: virtual void Func1() { cout Base b; Derive d; return 0; } public: virtual void func1() { cout cout cout Base b1; int a; int* a2 = new int; //const int a3 = 1; const char* s = "hello world"; static int d4 = 0; printf("栈:%p\n", &a); printf("堆:%p\n", a2); printf("静态区:%p\n", &d4); //printf("代码段:%p\n", &a3); printf("代码段:%p\n", s); printf("虚表:%p\n", *(int*)(&b1)); //在类域中的函数地址比较特殊,要加& printf("虚函数地址:%p\n", &Base::func1); printf("普通函数地址:%p\n", func); return 0; } public: virtual void BuyTicket() { cout public: virtual void BuyTicket() { cout p.BuyTicket(); } int main() { Person Mike; Func(Mike); Student Johnson; Func(Johnson); return 0; } p-BuyTicket(); } int main() { Person mike; Func(&mike); mike.BuyTicket(); return 0; } // 以下汇编代码中跟你这个问题不相关的都被去掉了 void Func(Person* p) { ... p-BuyTicket(); // p中存的是mike对象的指针,将p移动到eax中 001940DE mov eax,dword ptr [p] // [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx 001940E1 mov edx,dword ptr [eax] // [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax 00B823EE mov eax,dword ptr [edx] // call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来 以后到对象的中取找的。 001940EA call eax 00头1940EC cmp esi,esp } int main() { ... // 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调 用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址 mike.BuyTicket(); 00195182 lea ecx,[mike] 00195185 call Person::BuyTicket (01914F6h) ... } public: virtual void func1() { cout cout public: virtual void func1() { cout cout cout cout public: virtual void func3() { cout Base b; Derive d; X x; return 0; } //由于VSX86环境下,编译器会在虚表结尾放一个nullptr,所以此处结束条件为a[i]!=0 for (int i = 0; a[i] != 0; i++) { printf("[%d]-%p ", i, a[i]); //调用a[i]中存储的函数 VFUNC f = a[i]; f(); //(*f)() cout Base b; // 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数 // 指针的指针数组,这个数组最后面放了一个nullptr // 1.先取b的地址,强转成一个int*的指针 // 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针 // 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。 // 4.虚表指针传递给PrintVTable进行打印虚表 // 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最 //后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。 printVFT((VFUNC*)(*((int*)(&b)))); printf("----------------------------------\n"); Derive d; printVFT((VFUNC*)(*(int*)(&d))); printf("----------------------------------\n"); X x; printVFT((VFUNC*)(*(int*)(&x))); return 0; } public: virtual void func1() { cout cout public: virtual void func1() { cout cout public: virtual void func1() { cout cout for (int i = 0; a[i] != 0; i++) { printf("[%d]-%p ", i, a[i]); VFUNC f = a[i]; f(); } cout Derive d; printVFT((VFUNC*)(*(int*)&d)); printf("-----------------------------\n"); //由于d对象中存有两张虚表,要打印第2张虚表,需要将&d偏移指向Base2开头,有如下几种方式 //printvft((vfunc*)(*(int*)((char*)&d+sizeof(base1)))); Base2* ptr = &d; printVFT((VFUNC*)(*(int*)ptr)); return 0; }
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!
