【第五节】C++的多态性与虚函数

目录

前言

一、子类型

二、静态联编和动态联编

三、虚函数

四、纯虚函数和抽象类

五、虚析构函数

六、重载,重定义与重写的异同

 


前言

        面向对象程序设计语言的三大核心特性是封装性、继承性和多态性。封装性奠定了基础,继承性是实现代码重用和扩展的关键,而多态性则是功能的扩充。多态性体现在对不同类的对象发送相同的消息时,会产生不同的行为。这里所说的消息主要是指对类成员函数的调用,而不同的行为则对应着不同的实现方式。在C++中,实现多态性的方法包括:

  • 函数重载

  • 运算符重载

  • 模板

  • 虚函数

        函数重载是多态性的一种基本形式,它允许在同一作用域内,相同的函数名对应不同的实现。函数重载的实现条件是函数参数的类型或个数必须有所区别。

        除了函数重载这种简单的多态形式,C++还提供了更为灵活的特性——虚函数。虚函数使得函数调用与函数体的绑定可以在运行时动态确定,这对于实现同一接口、多种实现的场景尤为重要。在深入探讨虚函数的概念之前,我们需要先了解子类型、静态联编和动态联编等相关概念。

一、子类型

        在继承的框架下,如果类B以公有继承的方式从类A派生而来,那么类B不仅继承了类A的行为,还可能拥有自身独特的行为。在这种情况下,我们称类B为类A的一个子类型。具体来说,如果存在一个类型S,它至少提供了类型T的行为,那么我们称类型S是类型T的子类型。

        当类B是类A的子类型时,类A对象能够调用的函数,类B的对象同样能够调用。这种情况下,我们称类B与类A兼容,或者说类B适应于类A。子类型的一个重要作用是实现类型兼容性,即在公有继承的模式下,派生类的对象、指向对象的指针以及对象的引用,都能够无缝地适应于基类的对象、指向对象的指针和对象引用所适用的场合。

        需要注意的是,子类型关系是单向且不可逆的。如果已知类B是类A的子类型,那么认为类A也是类B的子类型是不正确的。子类型的概念强调的是派生类对基类的兼容性和扩展性,而不是基类对派生类的依赖。

二、静态联编和动态联编

        联编是程序中各个部分相互关联的过程。根据联编发生的时机,它可以分为静态联编和动态联编两种类型。静态联编,又称为早期联编,发生在程序的编译和链接阶段。在这种联编方式中,函数调用与执行该函数的代码之间的对应关系在程序运行之前就已经确定,这意味着所有关联工作都在程序执行前完成。

示例代码:

class CBase {
public:
    void fun() {
        cout << "CBase:fun" << endl;
    }
};

class CMyClass :public CBase {
public:
    void fun() { cout << "CMyClass:fun" << endl; }
};

int main() {
    CBase* p;
    CBase objA;
    CMyClass objB;
    p = &objA;
    p->fun();
    p = &objB;
    p->fun();
    return 0;
}

        在静态联编的情况下,如果存在一个指向基类的指针p,那么在程序运行之前,p->fun()就已经被确定为调用基类的成员函数fun()。因此,无论指针p指向的是基类对象还是派生类对象,p->fun()都将调用基类的成员函数,并且结果保持一致。这是静态联编的特性。

        相比之下,动态联编,又称为晚期联编,是在程序运行时进行的联编过程。动态联编要求在运行时确定函数调用与执行该函数代码之间的对应关系。以之前的例子为例,如果采用动态联编,那么随着指针pobjA指向的对象不同,pobjA->fun_a()将能够调用不同类中fun_a()的不同版本。这意味着,通过一个统一的接口pobjA->fun_a(),可以访问多个不同的实现版本,即函数调用取决于运行时pobjA所指向的对象,从而展现出多态性。使用虚函数可以实现动态联编,允许在不同的联编情况下选择不同的实现,这正是多态性的体现。

        继承是实现动态联编的基础,而虚函数则是动态联编的关键所在。通过虚函数,可以在运行时根据对象的实际类型来调用相应的函数版本,从而实现多态行为。

三、虚函数

虚函数的概念:在基类中冠以关键字 virtual 的成员函数
虚函数的定义:
virtual<类型说明符>函数名>(<参数表>)
{
//<函数体>
}

virtual void fun a()
{
//<函数体>
}

虚函数的定义与特性如下:

  1. 若在基类中将某一成员函数声明为虚函数,则该函数在所有派生类中均保持其虚函数的属性,即使派生类中未显式使用virtual关键字。

  2. 动态绑定(或动态联编)仅在通过基类指针或引用调用虚函数时发生,这是实现多态性的关键机制。

  3. 虚函数不能被声明为静态函数,也不能是友元函数,因为这些类型的函数不支持动态绑定。

  4. 在基类中声明为虚函数的成员函数,在派生类中即便没有使用virtual关键字,仍然保持其虚函数的特性。

  5. 当一个成员函数在基类中被声明为虚函数时,它允许在派生类中拥有不同的实现版本,这为多态性的实现提供了可能。

  6. 由于虚函数的存在,编译器会在运行时进行动态联编,确保调用虚函数的对象在运行时根据实际对象类型来确定,从而实现动态联编的多态性。

说了半天虚函数,它到底有什么特性??特性如下:

当一个父类指针指向子类对象的时候,调用一个虚函数,将调用子类的虚函数。

示例代码:

#include <iostream>
using namespace std;

class Base {
public:
    virtual void Fun1() {
        cout << "Base::Funl ..."<<endl ;
    }

    virtual void Fun2() {
        cout << "Base::Fun2 ..." << endl;
    }

    void Fun3() {
        cout << "Base::Fun3 ..." << endl;
    }
};

class Derived : public Base {
public:
    /*virtual*/void Fun1()//不加virtual Fun1 也是虚函数。
    {
        cout << "Derived::Fun1 ..."<< endl; 
    }
    /*virtual*/void Fun2()
    {
        cout << "Derived::Fun2 ..." << endl;
    }
    void Fun3()
    {
        cout << "Derived::Fun3 ..." << endl;
    }

};
int main() {
    Base* p;
    Derived d;
    p = &d;
    p->Fun1(); //Fun1是虚函数,基类之指针指向派生类对象,调用的是派生类对象的虚函数
    p->Fun2();
    p->Fun3(); //Fun3非虚函数,根据p指针实际类型来调用相应类的成员函数
    return 0;
}

运行结果:

        这是一个很厉害的特性,我们知道调用什么函数,一般是和类型绑定的,类A的指针调用fun这个函数,本身调用的应该是类A的函数。但是有了虚函数之后,看这个指针指向哪一个子类对象了。
        这给我们提供了很多想象力,比如一个函数的参数是父类指针,我们往里面传递不同的子类对象,就可以在函数中调用到不同的虚函数。
        再比如,我们在一个数组中存储不同的子类对象,统一的去调用虚函数,大家的行为都是不相同的。

四、纯虚函数和抽象类

        虚函数机制赋予了基类指针指向派生类对象的能力,并确保调用的是派生类中相应的虚函数,这一特性使得我们能够以统一的方式处理不同派生类的对象。这种动态绑定确保了函数入口在运行时才得以确定。然而,当面临基类接口无法实现的情况时,我们该如何应对?以形状类为例,它定义了一个求面积的函数,而圆形和矩形作为其派生类,各自拥有计算面积的方法。但形状本身作为一个抽象概念,并不具备计算面积的具体方法。在这种情况下,纯虚函数便派上了用场。包含纯虚函数的类被称为抽象类,它们不能被实例化。纯虚函数是一种特殊的虚函数,它没有具体的实现,仅作为接口存在,强制派生类提供必要的实现细节。

其定义格式如下:

class <类名>
{
    virtual <函数类型> <函数名>(<参数表>)=0;
    //...
}

class CClassA
{
    virtual <函数类型> <函数名>(<参数表>)=0;
    //...
}

        在众多场景中,基类可能无法为虚函数提供实质性的实现,此时将其声明为纯虚函数,将其实现的责任转交给派生类,这正是纯虚函数的核心作用。当一个类中包含了纯虚函数,它便成为了抽象类。根据C++的规范,抽象类无法直接创建对象。

        由于纯虚函数缺乏具体的实现,包含此类函数的类自然无法直接实例化,这一点显而易见——因为无法调用未实现的纯虚函数。因此,这类类被形象地称为抽象类。抽象类若要摆脱其抽象的本质,唯有依赖派生类来充实这些虚函数的具体实现。

示例代码:

#include <iostream>
using namespace std;

class Shape {
public:
    virtual void Draw() = 0;
    virtual ~Shape() {}
};

class Circle :public Shape{
public:
    void Draw() {
        cout << "Circle::Draw()..." << endl;
    }
    ~Circle() {
        cout << "~Circle ..." << endl;
    }

};

class Square : public Shape {
public:
    void Draw() {
        cout << "Square::Draw()" << endl;
    }
    ~Square() {
        cout << "~Square ..." << endl;
    }
};
int main() {
    //Shape obj; //错误的,抽象类不能定义对象
    Shape* pobj = NULL;
    Circle objcirele;
    pobj = &objcirele;
    pobj->Draw();
    return 0;
}

        抽象类仅能作为基类被继承,而不能直接声明抽象类的实例。在类的构造与析构过程中,构造函数不可设为虚函数,而析构函数则允许为虚函数。

        抽象类本身不具备直接创建对象实例的能力,但可以声明抽象类的指针或引用。通过指向抽象类的指针,我们能够实现运行时的多态性。派生类有义务实现基类中的纯虚函数,若未能履行这一职责,该派生类仍将被视为抽象类。

五、虚析构函数

        构造函数不可声明为虚函数,而析构函数则具备这一特性,通过在析构函数前添加关键字virtual来实现。一旦基类的析构函数被声明为虚函数,其派生类的析构函数默认也成为虚析构函数,此时可省略virtual关键字。

        将析构函数声明为虚函数的原因在于,当基类指针指向派生类对象时,这是多态性的常见应用场景。在释放内存时,若通过delete操作符删除基类指针,通常只会触发基类的析构函数,而派生类的析构函数则不会被调用,这可能导致内存泄漏。

        然而,若基类的析构函数是虚函数,且派生类提供了自定义的析构函数实现,那么在delete基类指针时,将同时调用派生类的析构函数。在派生类执行析构过程时,会自动调用基类的析构函数,确保所有相关资源得到妥善清理。

示例代码:

#include <iostream>
using namespace std;

class CClassA{
public :
    CClassA(){
        cout << "CClassA" << endl; 
    }
    virtual ~CClassA() {
        cout << "~CClassA" << endl; 
    }
};

class CClassB : public CClassA {
public:
    CClassB() { cout << "CClassB" << endl; }
    virtual ~CClassB() { cout << "~CClassB" << endl; }
};

int main() {
    CClassA* pobjA = new CClassB;
    delete pobjA;
    return 0;
}

六、重载,重定义与重写的异同

        在面向对象编程中,"重载"(overload)、"重写"(override)和"重定义"(redefine)是三个重要的概念,它们在处理成员函数时有着不同的应用和特征。

重载(Overload)

  • 发生在同一个类中。

  • 函数名称相同。

  • 参数列表必须不同。

  • 是否使用virtual关键字是可选的。

重写(Override)

  • 发生在派生类与基类之间。

  • 函数名称相同。

  • 参数列表相同。

  • 基类中的函数必须声明为virtual

重定义(Redefine)

  • 发生在派生类与基类之间。

  • 当函数名和参数都相同时,基类函数不需要virtual关键字。

  • 当函数名相同但参数不同时,是否使用virtual关键字是可选的。

这些概念的理解和正确应用对于掌握面向对象编程至关重要,它们帮助开发者以更加灵活和高效的方式设计和实现类和对象。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

AC/DC电源模块:可靠持久的能源供应

BOSHIDA AC/DC电源模块&#xff1a;可靠持久的能源供应 AC/DC电源模块是一种被广泛应用于工业、通信、医疗等领域的电源设备。其可靠持久的能源供应能够保证设备的正常运行和稳定性能&#xff0c;具有重要的意义。在本文中&#xff0c;我们将详细介绍AC/DC电源模块的特点和优势…

实用新型专利申请被驳回原因

实用新型专利作为知识产权的重要组成部分&#xff0c;对推动技术创新和产业发展具有重要意义。然而&#xff0c;在申请实用新型专利的过程中&#xff0c;有时会遇到被驳回的情况。 实用新型专利被驳回的一个常见原因是技术方案不具备新颖性、创造性和实用性等专利授权条件。专利…

Vue从入门到实战Day12~14 - Vue3大事件管理系统

一、用到的知识 Vue3 compositionAPIPinia / Pinia持久化处理Element Plus(表单校验&#xff0c;表格处理&#xff0c;组件封装)pnpm 包管理升级Eslint prettier 更规范的配置husky&#xff08;Git hooks工具&#xff09;&#xff1a;代码提交之前&#xff0c;进行校验请求模…

【408真题】2009-24

“接”是针对题目进行必要的分析&#xff0c;比较简略&#xff1b; “化”是对题目中所涉及到的知识点进行详细解释&#xff1b; “发”是对此题型的解题套路总结&#xff0c;并结合历年真题或者典型例题进行运用。 涉及到的知识全部来源于王道各科教材&#xff08;2025版&…

每天学点小知识:图床搭建 + CDN简介

前言&#xff1a; 本章内容帮你解决&#xff0c;本地图片不能分享到网上的问题。需要工具github JSDelivr 知识点 Q&#xff1a;什么是JSDelivr&#xff1f; JSDelivr是一个免费且公开的内容分发网络&#xff08;CDN&#xff09;&#xff0c;专门用于加速开源项目和静态网站…

武汉城投城更公司与竹云科技签署战略协议,携手构建智慧城市新未来!

2024年5月16日&#xff0c;武汉城投城更公司与深圳竹云科技股份有限公司&#xff08;以下简称“竹云”&#xff09;签订战略合作协议&#xff0c;双方将深入推进产业项目合作。 签约现场&#xff0c;双方围绕产业项目合作方向、路径和内容等进行了全面深入交流。城投城更公司党…

AI视频智能分析引领智慧园区升级:EasyCVR智慧园区视频管理方案

一、系统概述与需求 随着信息技术的不断发展&#xff0c;智慧园区作为城市现代化的重要组成部分&#xff0c;对安全监控、智能化管理提出了更高的要求。智慧园区视频智能管理系统作为实现园区智能化管理的重要手段&#xff0c;通过对园区内各关键节点的视频监控和智能分析&…

破解面试难题:面试经典150题 之双指针法详解与代码实现

面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 125. 验证回文串 125. 验证回文串 STL容器 思路&#xff1a; 遍历输入的字符串&#xff0c;将其中的字母字符转换为小写&am…

FL Studio21.2.8中文版让你的音乐创作如鱼得水

在音乐的世界里&#xff0c;我们都是探索者&#xff0c;追求着无尽的创新和可能性。而在这个过程中&#xff0c;我们往往会遇到各种挑战和困扰。如何快速高效地创作出满意的音乐作品&#xff1f;如何将我们的创意完美地呈现出来&#xff1f;这些问题可能一直困扰着你。今天&…

详解 HTML5 服务器发送事件(Server-Sent Events)

HTML5 服务器发送事件&#xff08;server-sent event&#xff09;允许网页获得来自服务器的更新。 EventSource 是单向通信的&#xff08;是服务器向客户端的单向通信&#xff0c;客户端接收来自服务器的事件流&#xff09;、基于 HTTP 协议&#xff08;EventSource 是基于标准…

基于振弦采集仪的岩土工程振弦监测技术研究与应用

基于振弦采集仪的岩土工程振弦监测技术研究与应用 岩土工程振弦监测技术是一种基于振弦采集仪的测试方法&#xff0c;用于对岩土体的力学特性进行监测和分析。振弦采集仪是一种先进的测试设备&#xff0c;能够准确测量岩土体中的振动响应&#xff0c;并通过分析振动信号来获取…

2024广东省赛 G.Menji 和 gcd

题目 #include <bits/stdc.h> using namespace std; #define int long long #define pb push_back #define fi first #define se second #define lson p << 1 #define rson p << 1 | 1 #define ll long long const int maxn 1e6 5, inf 1e12, maxm 4e4 …

华为OD机试 - 项目排期 - 贪心算法(Java 2024 C卷 200分)

华为OD机试 2024C卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;每一题都有详细的答题思路、详细的代码注释、样例测试…

[C++]红黑树

一、概念 红黑树&#xff0c;是一种二叉搜索树&#xff0c;但在每个结点上增加一个存储位表示结点的颜色&#xff0c;可以是Red或 Black。 通过对任何一条从根到叶子的路径上各个结点着色方式的限制&#xff0c;红黑树确保没有一条路 径会比其他路径长出俩倍&#xff0c;因而是…

【vscode篇】1-VScode设置语言为中文,2-解决中文注释乱码问题。

设置语言为中文 在前端开发中&#xff0c;Visual Studio Code(简称vscode)是一个非常好用的工具&#xff0c;但第一次打开vscode会发现界面为英文&#xff0c;这对很多开发者来说会很不友好&#xff08;比如我&#xff09;&#xff0c;把界面设置成中文只需要安装一个插件即可&…

【stm32/CubeMX、HAL库】嵌入式实验六:定时器(1)|定时器中断

参考&#xff1a; 【【正点原子】手把手教你学STM32CubeIDE开发】 https://www.bilibili.com/video/BV1Wp42127Cx/?p13&share_sourcecopy_web&vd_source9332b8fc5ea8d349a54c3989f6189fd3 《嵌入式系统基础与实践》刘黎明等编著&#xff0c;第九章定时器。 实验内容…

7777777777777

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;贝叶斯滤波与Kalman估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能&#xff0c…

python-合并排列数组 I

问题描述&#xff1a;合并两个按升序排列的整数数组a和b&#xff0c;形成一个新数组&#xff0c;新数组也要按升序排列。 问题示例&#xff1a;输入A[1],B[1],输出[1,1],返回合并后的数组。输入A[1,2,3,4],B[2,4,5,6],输出[1,2,2,3,4,4,5,6],返回合并所有元素后的数组。 完整代…

一个交易者的自白:念念不忘的交易,10个日内9个亏

一、新手: 面对爆仓,我像个白痴 我是在2012年开始接触的&#xff0c;这些年里我尝到了残酷失败的滋味&#xff0c;更品尝过胜利带来的喜悦。刚刚接触时很自信&#xff0c;总想着自己有一天一定会变成千万富翁的&#xff0c;用杠杆获取暴利。 在我首次爆仓的时候&#xff0c;我的…

前端Vue自定义轮播图组件的设计与实现

摘要 随着技术的发展&#xff0c;前端开发的复杂性日益增加。传统的整块应用开发方式在面对频繁的功能更新和修改时&#xff0c;往往导致整体逻辑的变动&#xff0c;从而增加了开发和维护的难度。为了应对这一挑战&#xff0c;组件化开发应运而生。本文将以Vue中的自定义轮播图…