当前位置: 首页 > ops >正文

C++语法之--多态

目录

一、多态的概念

1、多态的构成条件

2、多态的格式

二、虚函数的重写/覆盖

三、协变

四、析构函数的重写

五、override 和 final关键字

六、重载/重写/隐藏的对比

七、纯虚函数和抽象类

八、多态的原理

1、虚函数表指针

2、多态的实现

九、动态绑定与静态绑定

十、虚函数表


一、多态的概念

   首先,多态分为静态多态和动态多态之分。

   静态多态主要就是之前讲过的重载和函数模板,它们在编译的时候就完成参数的分别匹配。

   动态多态具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种形态。

1、多态的构成条件

    态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为。

   eg:

class Person
{
public:virtual void fun(){cout << "Person" << endl;}protected:string _name;
};class Student :public Person
{virtual void fun(){cout << "Student" << endl;}protected:int _id;
};class Staff :public Person
{virtual void fun(){cout << "Staff" << endl;}
protected:string _address;
};void F(Person& x)
{x.fun();
}int main()
{Student a;Staff b;Person c;F(a);F(b);F(c); return 0;
}

   在上面代码中,当我们分别在F函数中调用a,b,c对象时,会分别调用Person、Student、Staff类中的fun函数从而构成多态。

2、多态的格式

   1、必须是基类的指针或者引用调用虚函数

void F(Person& x)//Person*x
{x.fun();
}

   2、被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。

   说明:

   要实现多态效果,第⼀必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象。

   第二派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。

二、虚函数的重写/覆盖

   类成员函数前面加上virtual就构成虚函数。非类成员函数前面不能加virtual修饰。

class Person
{
public:virtual void fun(){cout << "Person" << endl;}protected:string _name;
};
class Student :public Person
{virtual void fun(){cout << "Student" << endl;}protected:int _id;
};

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

   上面Person和Student类中的fun函数就构成虚函数的重写/覆盖。

   另外注意的是:基类中的虚函数一定要加上virtual修饰,但在派生类中可以对虚函数不加上virtual。因为派生类中的虚函数可以从基类继承下来。但这的写法并不很规范,还是建议在派生类中的虚函数前面加上virtual修饰。

   下面再来看看这道题目:

   A类和B类中的func函数构成多态,在main函数中我们也是调用的B类型指针的test函数。test函数在A是类中的虚函数,但并没有与B类构成多态。故程序还是调用从A类继承下来的test函数(虚函数被特殊处理,放到了类外。)而test函数在B类中又会继续调用func函数。

   这么分析下来感觉答案是D。但正确答案是B。

   实际上,如果不传参,则会调用基类多态函数形参的缺省值(确保群编译期与运行期行为分离、效率优先、语言规则一致性以及实现兼容性)。这个点需要特别注意一下。

三、协变

   协变是指派生类重写基类虚函数时,返回值类型可以是基类虚函数返回值类型的派生类指针或引用,这种情况下仍能保持多态特性。

class animal
{
public:void fun1(){cout << "这是一个动物农厂" << endl;}
};class dog :public animal
{
public:void fun2(){cout << "这是一只狗" << endl;}
};class animalfarm
{
public:virtual animal* create(){cout << "一只狗出生了"<<endl;return new animal();}
};class dogfarm :public animalfarm
{
public:virtual dog* speak(){cout << "汪汪汪" << endl;return new dog();}
};int main()
{animalfarm* A = new dogfarm();animal* B = A->create();B->fun1();return 0;
}

四、析构函数的重写

   

   上面代码中,我们开辟了一个B类型的空间给p2,按照语法来说在析构p2的时候应该先调用B的析构再调用A的析构,然而实际上并没有调用B的析构导致了内存泄漏。

   我们应该让析构函数也实现多态好让其分别调用B和A中的析构,但是根据重写的条件来看,两者的函数名明显不同,那又怎么实现重写呢?

   只需要将析构函数设置为虚函数就可以了

   

   为了解决上面的问题,C++标准特意将析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派生类的析构函数就构成重写。

   所以派生类中的析构函数最好设置为虚函数。

五、override 和 final关键字

   C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。

   

六、重载/重写/隐藏的对比

七、纯虚函数和抽象类

   在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

   抽象类Person不能实例化,但是继承抽象类的Student却可以完成实例化。因为纯虚函数在Student中完成了重写。

八、多态的原理

1、虚函数表指针

   一个包含虚函数的类中都至少包含一个虚函数表指针用来指向虚函数的位置。

   一般情况下虚函数表指针存放在类的头部位置,我们通过特殊的手段可以拿到虚函数表指针。

class Person
{
public:virtual void fun1(){cout << "Person" << endl;}virtual void fun2(){cout << "Person" << endl;}virtual void fun3(){cout << "Person" << endl;}virtual void fun4(){cout << "Person" << endl;}};int main()
{Person b;cout << (int*)&b << endl;return 0;
}

   从监视窗口上来看,b对象中包含了一个特殊的指针_vfptr。这个指针就是虚函数表指针。

   但是vs的监视窗口有时候并不准确,我们这次主要看内存监视窗口。通过内存窗口我们可以很清楚的看到有4个连续的地址存储在_vfptr指针中,这4个地址分别对应着fun1、fun2、fun3、fun4这4个虚函数的地址。

   虚函数表指针不仅会存储继承下来的虚函数地址,还会存储增自己本身的虚函数地址。

2、多态的实现

   满足多态条件后,底层不再是编译时通过调用对象确定函数的地址,而是运行时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类对应的虚函数。

九、动态绑定与静态绑定

   1、对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。

   2、满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。

十、虚函数表

   1、基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同⼀张虚表,不同类型的对象各自有独立的虚表,所以基类和派生类有各自独立的虚表。

   2、派生类由两部分构成,继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个

   3、派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。 

   4、派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数完成覆盖后的地址(3)派生类自己的虚函数地址三个部分。

   5、虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)

   6、虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址最后又存到了虚表中

   7、虚函数表的存储位置C++并未严格规定,需要看具体的编译器。

http://www.xdnf.cn/news/20161.html

相关文章:

  • 了解Python
  • Ubuntu:Git SSH密钥配置的完整流程
  • 捷多邦揭秘超厚铜板:从制造工艺到设计关键环节​
  • 让字符串变成回文串的最少插入次数-二维dp
  • 单元测试详解
  • 基于树莓派与Jetson Nano集群的实验边缘设备上视觉语言模型(VLMs)的性能评估与实践探索
  • 【c++进阶系列】:万字详解AVL树(附源码实现)
  • ubuntu 系統使用過程中黑屏問題分析
  • 前端上传切片优化以及实现
  • 基于LLM开发Agent应用开发问题总结
  • equals 定义不一致导致list contains错误
  • SQL面试题及详细答案150道(81-100) --- 子查询篇
  • webrtc弱网-LossBasedBandwidthEstimation类源码分析与算法原理
  • 【Proteus仿真】定时器控制系列仿真——秒表计数/数码管显示时间
  • 【ComfyUI】混合 ControlNet 多模型组合控制生成
  • ANSYS HFSS边界条件的认识
  • 【LeetCode热题100道笔记】二叉树中的最大路径和
  • 9.FusionAccess桌面云
  • Spring的事件监听机制(一)
  • 03.缓存池
  • 【数学建模】质量消光系数在烟幕遮蔽效能建模中的核心作用
  • 故障诊断 | MATLAB基于CNN - LSSVM组合模型在故障诊断中的应用研究
  • 在Ubuntu上配置Nginx实现开机自启功能
  • 54.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--新增功能--实现手机邮箱注册
  • js面试题 什么是作用域?
  • 【Proteus仿真】定时器控制系列仿真——LED小灯闪烁/流水灯/LED灯带控制/LED小灯实现二进制
  • EG2104 SOP-8 带SD功能 内置600V功率MOS管 栅极驱动芯片
  • 智能客户服务支持智能体
  • 基于GOA与BP神经网络分类模型的特征选择方法研究(Python实现)
  • 登录优化(双JWT+Redis)