您现在的位置是:首页 > 伤感句子

【C++】深度解剖多态

作者:纳雷武时间:2024-03-26 16:45:53分类:伤感句子

简介  文章浏览阅读825次,点赞36次,收藏45次。前面我们学习了继承,在c++的面向对象中是重要的部分,而c++另一个重要的部分就是多态,这个板块很实用,在java中使用起来尤其频繁,Java这个语言也是借鉴了c++使用起来比较轻松,学习c++的多态也是必不可少的

点击全文阅读

> 作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等
> 座右铭:松树千年终是朽,槿花一日自为荣。

> 目标:了解什么是多态,熟练掌握多态的定义,熟读抽象类。

> 毒鸡汤:一半明媚,一半阴霾,这就是人生

> 望小伙伴们点赞👍收藏✨加关注哟💕💕 

🌟前言  

前面我们学习了继承,在c++的面向对象中是重要的部分,而c++另一个重要的部分就是多态,这个板块很实用,在java中使用起来尤其频繁,Java这个语言也是借鉴了c++使用起来比较轻松,学习c++的多态也是必不可少的。

⭐主体

学习多态咱们按照下面的图解:

🌙多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态 。

举个例子:比如买票这个行为 ,当普通人买票时,是全价买票; 学生买票时,是半价买票; 军人买票时是优先买票。再举个栗子: 最近为了争夺在线支付市场 ,支付宝年底经常会做诱人的 扫红包 - 支付 - 给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜 8 块、 10 块 ... ,而有人扫的红包都是 1 毛, 5毛 .... 。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99 ;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1 ;总结一下: 同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。 ps :支付宝红包问题纯属瞎编,大家仅供娱乐。总结:多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。 Person 对象买票全价, Student 对象买票半价。

🌙多态的定义及实现

💫多态构成条件

在继承中要构成多态还有两个条件:

必须通过基类的指针或者引用调用虚函数被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

举个栗子:

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关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。

💫虚函数的重写和协变

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

1.协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

2.析构函数的重写

举例说明:

class Person {public:~Person() { cout << "~Person()" << endl; }};class Student : public Person {public:~Student() { cout << "~Student()" << endl; }};int main(){Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;}

问题分析:

想让p1调用Person的析构,p2先调用Person的析构在调用Student的析构,但是这里并没有调用Student的析构,只析构了父类,就可能发生内存泄漏。

问题拓展:

因为这里发生了隐藏,~Person()变为 this->destructor()    ~Student()变为this->destructor() 编译器将他们两个的函数名都统一处理成了destructor(析构函数),因此调用的时候只看自身的类型,是Person就调用Person的函数,是Student就调用Student的函数,根本不构成多态,这并不是我们期望的哪样。

问题解决:

析构函数添加上virtual

class Person {public:virtual ~Person() { cout << "~Person()" << endl; }};class Student : public Person {public:virtual ~Student() { cout << "~Student()" << endl; }};int main(){Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;}

再次运行:

💫final 和 override

在添加父类虚函数后面添加final代表不能再被重写

override代表必须要重写虚函数,如果没有重写便会报错

💫重载、覆盖(重写)、隐藏(重定义)的对比

🌙抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数包含纯虚函数的类叫做抽象类(也叫接口

类),抽象类不能实例化出对象,但可以new别的对象来定义指针,例如Car* pBMW = new BMW;

子类继承后也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。父类的纯虚函数强制了派生类必须重写,才能实例化出对象(跟override异曲同工,override是放在子类虛函数,检查重写。功能有一些重叠和相似 )另外纯虚函数更体现出了接口继承。纯虚函数也可以写实现{ },但没有意义,因为是接口继承,{ }中的实现会被重写;父类没有对象,所以无法调用纯虚函数

问题分析:

class Car{public:// 纯虚函数virtual void Drive() = 0;};int main(){Car c;return 0;}

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

🌙多态的原理

💫虚函数表

问题探究:

下面代码运行结果:

我们定义一个Base类,里面有虚函数,还有一个变量int,按照我们之前学习到了,这里Base类的大小应该是4个字节,图中确是8个字节,为什么会发生这种现象呢?

问题分析:

除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。其实应该叫__vftptr(多个t代表table)

总结归纳:

我们多添加几个虚函数,看看这个表里面的内容是怎么样的,可以发现虚函数会放到虚函数表中,普通函数不会,并且表里面的内容是一个数组,是函数指针数组

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;}

💫多态的原理

1.虚表指针里的内容

举例说明:

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;}

问题分析:

从图中我们可以看到,在内存1里面输入&p可以找到p的地址, 因为p的第一个内容就是__vfptr,因此p的地址也是__vfptr的地址,那么我们通过__vfptr的地址就可以找到虚函数表里面的内容,因此我们在内存2里面输入__vfptr的地址,我们便找到了两个虚函数的地址。 

问题拓展:

注意这里Student类和Teacher的类表里的第二个虚函数地址是一样的,因为B类没有重写第二个虚函数,因此继承下来了。为什么第一个虚函数不一样呢?因为子类重写后覆盖掉了(这也是为什么重写被称作覆盖的由来)

2.引用和指针如何实现多态 

可以分析,为什么多态可以实现指向父类调用父类函数 ,指向子类调用子类函数?传递父类,通过vftptr找到虚函数表的地址,再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数。父类再去接受这个数据了,一样会有vftptr(是子类的vftptr),再去找到虚函数的地址,有了虚函数的地址,便可以去call这个虚函数这样就完成了多态。

标题
传递子类,首先会进行切割

 💫虚函数表存放位置

举例说明:

我们通过代码来打印各个区地地址,可以判断虚函数表存放位置

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个字节,再解引用,就可以取到第一个元素的地址,也就是虚函数表指针的地址

从图中可以发现代码段和虚表地址非常接近,存在代码段的常量区。虚函数和普通函数地址非常接近,存在代码段。

🌙单继承和多继承关系的虚函数表

💫单继承中的虚函数表

举例说明:

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;}

代码分析:

通过输入__vfptr的地址,我们成功找到了里面虚函数的地址。并且我们还发现似乎下面那两个地址跟上面两个非常接近,我们可以合理的设想,下面两个地址也是虚函数指针。

问题分析:

下面我们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->", i, a[i]);VFTPTR p = a[i];p();}cout << endl;}int main(){Base b;Derive d;X x;PrintVFPtr((VFTPTR*)*(int*)&b);PrintVFPtr((VFTPTR*)*(int*)&d);PrintVFPtr((VFTPTR*)*(int*)&x);return 0;}

运行:

分析:可以确认虚函数都会放到虚表里面

💫多继承中的虚函数表

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;}; typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]){// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;}int main(){printf("%p\n", &Derive::func1); Derive d;//PrintVTable((VFPTR*)(*(int*)&d));PrintVTable((VFPTR*)(*(int*)&d));    PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1))));}


PrintVTable((VFPTR*)(*(int*)&d)); 

因为对象中存虚表指针,虚表指针中存的是虚表(一个指针数组),则需要先解引用访问到这个对象的前四个字节内容(存的就是虚表指针),此时的虚表指针 *((int*)&d)是一个int类型,再把虚表指针类型强转成指针数组类型才能传参

PrintVTable((VFPTR*)(*(int*)((char*)&d+sizeof(Base1)))); 是找到Base2的虚表地址后再解引用找到虚表(直接加2个int字节也能找到base2,考虑Base1可能不单单是2个int大小,这里建议用sizeof(Base1) )


结论: Derive对象Base2虚表中func1时,是Base2指针ptr2去调用。但是这时ptr2发生切片指针偏移,需要修正。中途就需要修正存储this指针ecx的值

💫菱形继承和菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的 模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看 了,一般我们也不需要研究清楚,因为实际中很少用。

🌙继承和多态常见的面试问题

1.什么是多态?本篇已讲解了

2. 什么是重载、重写(覆盖)、重定义(隐藏)?这里在本篇已讲解了

3. 多态的实现原理?这里在本篇已讲解了

4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。

5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表 阶段才初始化的。

7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析 构函数定义成虚函数。

 8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。

10. C++菱形继承的问题?虚继承的原理?这里在本篇已讲解了

11. 什么是抽象类?抽象类的作用?抽象类强制重写了虚函数,另外抽 象类体现出了接口继承关系。

🌟结束语

       今天内容就到这里啦,时间过得很快,大家沉下心来好好学习,会有一定的收获的,大家多多坚持,嘻嘻,成功路上注定孤独,因为坚持的人不多。那请大家举起自己的小手给博主一键三连,有你们的支持是我最大的动力💞💞💞,回见。

点击全文阅读

郑重声明:

本站所有活动均为互联网所得,如有侵权请联系本站删除处理

我来说两句