C++多态-虚函数

news2024/11/15 10:35:35
        多态分为编译时多态和运行时多态。编译时多态就是在编译阶段就能绑定要执行的那个函 数。运行时多态要等到运行到调用的那条语句时,根据指针/引用所绑定的对象,来决定执行哪
个函数,我们要讲的虚函数就是运行时多态,是 C++中非常重要的一个东西。
        讲虚函数之前,我们先来脑洞讨论一下,我们都知道继承了,那么我们来设计一个王者荣耀的英雄,,首先每个英雄都是一个人物,它们都可以行走,都可以释放技能,那么就可以设计一个人物基类,基类的成员我们定义英雄公有的一些属性,例如血量值,移动速度,攻击力等等。那么这个时候我们就想一个问题,每个英雄类都去继承这个人物基类,那么相对于每个英雄的一些行为,按理来说形式都是一样的,不过方式不一样,例如,每个游戏都可以释放1,2,3,技能,这个是每个英雄都会的一个行为形式,不过不同英雄的技能方式会不一样,那我们可不可以将这个这个公有行为放在人物基类中呢?然后当游戏类去继承我的人物基类时,只需要重新去实现我这个行为就好了。虚函数就可以完成这样的一个工作。
虚函数定义语法:
class 类名
{
    virtual 函数返回值 函数名(函数参数列表);
}
例:
class A
{
    public:
        int a;
        int b;
    public:
        A(int x,int y):a(x),b(y){}
        virtual void fun() //虚函数
        {
            cout << "a:" << a << "b:" << b << endl;
        }
};
        设置为虚函数后,那么在类的头部就会生成一个虚表指针,它指向一张虚表,虚表中保存了各虚函数的地址,也就是说,上面的类 A 在内存中的情况如下

纯虚函数

        纯虚函数就是在定义虚函数的时候加上 = 0;那么该函数就是一个纯虚函数,语法如下:
class 类名
{
    virtual 函数返回值 函数名(函数参数列表) = 0;
}
示例:
class A
{
    public:
        int a;
        int b;
    public:
        A(int x,int y):a(x),b(y){}
        virtual void fun() = 0; //纯虚函数
};
定义纯虚函数需要注意的地方:
        1.有纯虚函数就是抽象类,这样的类只能做基类,因为它不能实例化对象
        2.纯虚函数不需要在本类中实现,子类继承了该抽象类一定要去实现这些纯虚函数,不然继承下来的子类也将变成抽象类。

虚函数与继承

        当基类中有虚函数时,子类继承它他时,同样也会继承它的虚表指针,对于虚函数,子类可以重写他(纯虚函数必须重写),重写了虚函数后,那么子类继承下来的虚表中将会替换被重写的虚函数地址。例如:
class A
{
    public:
        int a;
        int b;
    public:
        A(int x,int y):a(x),b(y){}
        virtual void fun() //虚函数
        {
            cout << "A" << endl;
        }
};
class B:public A
{
    private:
        int c;
        int d;
    public:
        B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
        virtual void fun() //重写虚函数
        {
            cout << "B" << endl;
        }
};
        上面类 B 继承了类 A,如果类 B 不去对虚函数 fun 进行重写的话,那么继承下来的虚表地址中保存虚函数地址应该是 A 中实现的 fun,但是如果类 B 对 fun 进行了重写,那么虚表地址中保存的虚函数地址就是 B 中实现的这个 fun.
        当然,重写基类虚函数时,子类中加不加 virtual 关键字都是一样的,基类加就好了。
补充:
        需要注意的是,当基类定义虚函数为私有成员的时候,子类继承它也会进行对他进行重写,但是就无法通过基类指针去动态访问这个虚函数了,但是如果基类的虚函数不是私有的,而派生类重写的虚函数是私有的,这个时候是可以通过基类指针去动态访问这个虚函数的,这个也很好理解,虚函数是存在在虚表中的,所以不管这个虚函数是什么类型的成员都会被子类重写,毕竟子类都会继承虚表,但是对于访问而言,通过函数名字去访问虚函数,就需要访问权限,打个比喻,基类虚函数为私有成员,派生类重写的私有成员为公有成员,那么不能通过基类指针去访问这个虚函数,因为这个虚函数在基类中是私有的,但是可以通过派生类指针去访问这个虚函数,因为这个虚函数在派生类中是公有的。同理,如果基类的虚函数是公有的,派生类继承过去的虚函数是私有的,那么可以通过基类指针去访问这个虚函数,而且基类指针指向派生类,那么访问的虚函数内容就是派生类中的内容,因为不是直接通过函数名去访问派生类中这个虚函数的,而是因为虚表替换了虚函数内容,通过虚表访问的,所以尽管派生类中这个虚函数是私有的,通过基类指针也是可以访问的。
例:
class A
{
    public:
        int a;
        int b;
    public:
        A(int x,int y):a(x),b(y){}
        virtual void fun() //公有虚函数
        {
            std::cout << "A" << std::endl;
        }
};
class B:public A
{
    private:
        int c;
        int d;
    public:
        B(int x,int y,int z,int k):A(x,y),c(z),d(k){}
    private:
        virtual void fun() //私有重写虚函数
        {
            std::cout << "B" << std::endl;
        }
};
int main(int argc, char const *argv[])
{
    B b(1,2,3,4);
    A *p = &b;
    p->fun();
    return 0;
}

虚函数与析构函数

        构造函数和析构函数是不能被继承的,因为构造函数和析构函数负责的是他们对应的那一个类成员的初始化和销毁。对于构造函数而言,构造函数是用来初始化对象的,不应该将构造
函数指定为虚函数,原因如下:
1 )当子类创建对象时,会首先调用子类的构造函数,然后再调用父类的构造函数(即子类不继承父类的构造函数),因此,构造函数为虚函数没有意义。
2 )虚函数的调用,要用到虚函数表指针,指针属于类的对象上,实例化对象需要调用构造函数,如果构造函数为虚函数,前后相矛盾。
基类的析构函数 应当被定义为虚函数 , 原因是:
1 )可以保证当我们 new 一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。
例如下面例子,A 是基类,B 是继承 A 类。
A *p = new B(1,2,3,4);
delete p;
        当我们用基类指针去 new 子类时,如果基类析构函数不是虚函数的话,那么释放对象的时 候,就自会调用基类的析构函数,不会调用子类的析构函数,当我们将基类的析构函数定义为虚函数之后,这个时候我们释放对象的时候就会先调用子类的析构函数再调用基类的析构函数
不能被定义为虚函数的元素:
1.构造函数
2.静态成员函数
3.友元函数

虚函数的使用

        其实,如果你没有真正接触 C++程序的话,你并不会明白 C++中的虚函数到底用处在哪里,但是不得不说的是虚函数在 C++中真的是非常的重要,一个 C++程序的设计好坏就主要看虚函数的设计。
        c++中的虚函数之所以能实现运行时多态是因为当我们子类继承了基类后,我们不直接使用子类去做一些操作,因为使用子类去做操作的话,那么你在编译阶段就已经绑定了这个子类的操作函数,就不会是运行时多态了。真正的用法是用基类指针去操作,那么我这个基类指针指向哪个子类,就会运行哪个子类中的操作函数,例如:
class Person //人物类
{
    private:
        int m_blood; //血量
        int m_person; //蓝量
    public:
        Person(int x,int y):m_blood(x),m_person(y){}
        virtual ~Person(){};
        virtual void skill1() = 0;//1 技能
        virtual void skill2() = 0;//2 技能
        virtual void skill3() = 0;//3 技能
};
//英雄类
class JiaLuo:public Person //伽罗
{
    public:
        JiaLuo(int x,int y):Person(x,y){}
        ~JiaLuo(){}
        virtual void skill1()//1 技能
        {
            cout << "伽罗释放渡灵之箭" << endl;
        }
        virtual void skill2()//2 技能
        {
            cout << "伽罗释放静默之箭" << endl;
        }
        virtual void skill3()//3 技能
        {
            cout << "伽罗释放纯净之域" << endl;
        }
};
class SunCe:public Person //孙策
{
    public:
        SunCe(int x,int y):Person(x,y){}
        ~SunCe(){}
        virtual void skill1()//1 技能
        {
            cout << "船夫释放劈风斩浪" << endl;
        }
        virtual void skill2()//2 技能
        {
            cout << "船夫释放惊涛骇浪" << endl;
        }
        virtual void skill3()//3 技能
        {
            cout << "船夫释放长帆破浪" << endl;
        }
};
void fun(Person *P) //释放技能,具体释放谁的技能要看参数传什么对象,这样就做
到了运行时多态
{
    P->skill1();
    P->skill2();
    P->skill3();
}
int main(int argc, char const *argv[])
{
    Person *p = new JiaLuo(1290,1270); //创建一个伽罗类
    Person *q = new SunCe(3450,2370); //创建一个孙策类
    fun(p); //伽罗放技能
    fun(q); //船夫放技能
    return 0;
}

通过虚表访问虚函数

        我们知道,当类中含有虚函数时,就会在类的头部生成一个虚表指针,虚表指针指向的虚表的地址,虚表中保存着虚函数的地址,那么知道这些我们就可以通过虚表指针来访问虚函数,例如:
class A
{
    public:
        virtual void fun1()
        {
            cout << "fun1" << endl;
        }
        virtual void fun2()
        {
            cout << "fun2" << endl;
        }
        virtual void fun3()
        {
             cout << "fun3" << endl;
        }
};
int main(int argc, char const *argv[])
{
    A a;
    typedef void (*p)();
    p f1,f2,f3;
    //&a ---虚表指针地址
    //*(unsigned long *)(&a) ---虚表地址
    //*(unsigned long *)(*(unsigned long *)(&a)) ---第一个虚函数地址
    //(p)((unsigned long *)(*(*(unsigned long *)(&a)) + n) ---第 n-1 个虚函数地址
    //通过虚表指针访问 fun1
    f1 = (p)(*(unsigned long *)(*(unsigned long *)(&a)));
    f1();
    //通过虚表指针访问 fun2
    f2 = (p)(*((unsigned long *)(*(unsigned long *)(&a))+1));
    f2();
    f3 = (p)(*((unsigned long *)(*(unsigned long *)(&a))+2));
    f3();
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1251163.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式的学习需要合理规划时间

低级的欲望放纵即可获得&#xff0c;高级的欲望只有克制才能达成。——卡耐基1、粉丝的误会 很多粉丝&#xff0c;问我&#xff0c; "胡老师我想报您的培训班。" ... 得知我知识业余时间写文章&#xff0c;紧接着又会问&#xff0c; "jg单位这么清闲啊&#…

带你理解H桥电路

H桥电路 文章目录 H桥电路前言一、H桥基本结构二、H桥原理二、控制模式三、MOS管H桥 前言 在做单片机控制小车的时候一定会用一个电机驱动板&#xff0c;这个电机驱动板是怎么做的呢&#xff0c;答案就是H桥&#xff0c;没学过电路的同学可能会问什么是H桥&#xff0c;这篇文章…

【c++哈夫曼树代码实现】

哈夫曼树是不定长编码方式&#xff0c;由于是将权值大的元素放在离根结点近的地方 &#xff0c;权值小的放在离根远的地方&#xff0c;哈夫曼树效率很高&#xff0c;并且一个编码不会以另一个编码作为前缀&#xff0c;避免了编码的歧义性&#xff0c;本文将带大家探索如何创建和…

SAP smartform 实现打印条形码

先在SE73里定义一个新的BARCODE&#xff0c;注意一定要用新的才可以&#xff0c;旧的是打印不出来的。 然后定义一个SMARTFORM的样式&#xff0c;把你定义的BARCODE放到字符样式里面去。 再做SMARTFORM就可以了&#xff0c;将需要作为条码的变量的格式选为该BARCODE格式&…

ASP.NET Core 使用 SignalR 实现实时通讯

&#x1f433;简介 SignalR是一个用于ASP.NET的库&#xff0c;它允许服务器代码向连接的客户端实时发送推送通知。它使用WebSockets作为底层传输机制&#xff0c;但如果浏览器不支持WebSockets&#xff0c;它会自动回退到其他兼容的技术&#xff0c;如服务器发送事件&#xff…

GEE:众数滤波

作者:CSDN @ _养乐多_ 在本文中,我们将介绍如何使用Google Earth Engine(GEE)平台对遥感影像进行众数滤波处理。并以众数滤波平滑NDVI图像为示例,演示众数滤波整个过程。 结果如下图所示, 文章目录 一、众数滤波二、完整代码三、代码链接一、众数滤波 众数滤波是一种图…

保护您的IP地址:预防IP地址盗用的关键措施

随着互联网的发展&#xff0c;IP地址作为标识互联网设备的重要元素&#xff0c;成为网络通信的基石。然而&#xff0c;IP地址盗用威胁正不断增加&#xff0c;可能导致敏感信息泄露、未经授权的访问和网络攻击。本文将介绍一些有效的方法&#xff0c;以帮助组织和个人预防IP地址…

【外贸干货】领英客户开发与营销的六个策略方向

领英(LinkedIn)已经成为外贸营销人员&#xff0c;尤其是B2B外贸营销人员&#xff0c;一个重要且有效的社交媒体平台。 相比于其他社交媒体平台&#xff0c;领英(LinkedIn)在增加流量、产生高质量的潜在客户和建立思想领导力方面有着独有的优势。 因为领英(LinkedIn)不仅仅是获…

【力扣:1707 1803】0-1字典树

思路&#xff1a;树上每个节点存储拥有该节点的数组元素的最小值&#xff0c;left节点表示0&#xff0c;right节点表示1&#xff0c;构建完成后遍历树当子节点没有比mi小的元素时直接输出-1&#xff0c;否则向下构造。 struct tree{int m;tree*leftnullptr,*rightnullptr;tree…

flink源码分析之功能组件(二)-kubeclient

简介 本系列是flink源码分析的第二个系列,上一个《flink源码分析之集群与资源》分析集群与资源,本系列分析功能组件,kubeclient,rpc,心跳,高可用,slotpool,rest,metrics,future。其中kubeclient上一个系列介绍过,为了系列完整性,这里“copy”一下。 kubeclient组件…

ffmpeg下载与配置环境变量

FFmpeg 是一个强大的多媒体框架&#xff0c;可以让用户处理和操纵音频和视频文件。具有易于使用的界面&#xff0c;用户可以在 Windows、Mac 或 Linux Ubuntu 系统上下载 FFmpeg 并将其提取到文件夹中。然后&#xff0c;该软件可以加入 PATH 环境变量中就可以快捷的使用软件了.…

中职组网络安全-Windows操作系统渗透测试 -20221219win(环境+解析)

B-4:Windows操作系统渗透测试 任务环境说明: 服务器场景:20221219win 服务器场景操作系统:Windows(版本不详)(封闭靶机) 1.通过本地PC中渗透测试平台Kali对服务器场景Server08进行系统服务及版本扫描渗透测试,并将该操作显示结果中1433端口对应的服务版本信息作为F…

深度学习之图像分类(十四)CAT: Cross Attention in Vision Transformer详解

IPSA和CPSA的处理流程、维度变换细节 FLOPs的计算方法、以及flops和划分的patch数目以及patch的维度计算关系 IPSA如何进行local attention、CPSA如何进行globe attention CAT的代码详细注释---需要学习完Transformer TNT、swin transformer、crossViT CAT: Cross Atten…

软件测试 | MySQL 非空约束详解

&#x1f4e2;专注于分享软件测试干货内容&#xff0c;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01;&#x1f4e2;交流讨论&#xff1a;欢迎加入我们一起学习&#xff01;&#x1f4e2;资源分享&#xff1a;耗时200小时精选的「软件测试」资…

JavaWeb学习(未完结)

文章目录 一、基本概念1.1 动态Web网站简介1.2 web应用程序1.3 静态web1.4 动态web 二、web服务器2.1 技术2.2 应用服务器2.3 安装 jdk8 三、Tomcat3.1 安装 Tomcat93.2 文件说明3.3 启动并使用Tomcat3.4 关闭Tomcat3.5 可能遇到的问题3.6 配置3.6.1 修改测试访问的网页地址3.6…

LLM之Agent(一):使用GPT-4开启AutoGPT Agent自动化任务完整指南

在ChatGPT引领的大模型时代&#xff0c;要想让大模型按照用户的指令执行&#xff0c;Prompt设计是一门艺术&#xff0c;由此还催生了一个职业”Prompt工程师“。其实&#xff0c;并不是所有人都可以设计出好的Prompt&#xff0c;甚至同样的Prompt应用在不同的大模型上表现的结果…

Mysql数据库 20.DCL数据控制语言

因这类SQL语言开发人员操作较少&#xff0c;主要是数据库管理员&#xff08;DBA&#xff09;使用&#xff0c;所以前文没有提及&#xff0c;这篇文章进行补充说明 DCL数据控制语言 用来管理数据库用户&#xff0c;控制数据库的访问权限 1.管理用户 1.1 查询用户 select * f…

想当老师应该去学什么专业

专业选择是决定未来职业发展的重要步骤&#xff0c;如果你也想成为一名老师&#xff0c;那么这五个专业可能会适合你&#xff01; 教育学专业 教育学专业是培养教育理论和方法的学科&#xff0c;这些理论知识将帮助你理解教学过程、学生发展、课程设计和评估。该专业将让你全面…

从入门到精通:JMeter接口测试全流程详解!

利用Jmeter做接口测试怎么做呢&#xff1f;过程真的是超级简单。 明白了原理以后&#xff0c;把零碎的知识点填充进去就可以了。所以在学习的过程中&#xff0c;不管学什么&#xff0c;我一直都强调的是要循序渐进&#xff0c;和明白原理和逻辑。这篇文章就来介绍一下如何利用…

互联网时代的身份标识有哪些?

在互联网时代&#xff0c;我们的在线活动几乎都与IP地址相关。无论是浏览网页、观看视频&#xff0c;还是进行在线交易和沟通交流&#xff0c;我们的设备都会分配到一个独特的IP地址。然而&#xff0c;你可能并未意识到的是&#xff0c;IP地址不仅标识了我们在网络中的身份&…