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

news2024/11/18 1:38:05

目录

前言

一、子类型

二、静态联编和动态联编

三、虚函数

四、纯虚函数和抽象类

五、虚析构函数

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

 


前言

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

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

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

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

相关文章

JWT身份验证相关安全问题

前言&#xff1a;工作中需要基于框架开发一个贴近实际的应用&#xff0c;找到一款比较合适的cms框架&#xff0c;其中正好用到的就是jwt做身份信息验证&#xff0c;也记录一下学习jwt相关的安全问题过程。 JWT介绍 Json web token (JWT), 是为了在网络应用环境间传递声明而执行…

如何遍历并处理不平衡的Python数据集

目录 一、引言 二、不平衡数据集的概念与影响 三、处理不平衡数据集的策略 重采样策略 集成学习方法 代价敏感学习 一分类方法 四、Python工具与库 五、案例分析与代码实现 案例一&#xff1a;使用imbalanced-learn库进行上采样 案例二&#xff1a;使用scikit-learn…

知识付费小程序源码系统 界面支持万能DIY装修,一站式运营 附带完整的源代码以及搭建教程

系统概述 这是一款功能强大的知识付费小程序源码系统&#xff0c;它为用户提供了一个全面的平台&#xff0c;能够满足各种知识付费场景的需求。其界面支持万能 DIY 装修&#xff0c;让用户可以根据自己的品牌形象和风格进行个性化定制&#xff0c;打造出独具特色的小程序界面。…

台灯护眼是真的吗?台灯怎么选对眼睛好?看这一篇就够了

随着现代生活方式的改变&#xff0c;孩子们面临着越来越多的视力挑战。我们一直使用普通台灯&#xff0c;往往忽略了不合适的台灯也会给孩子眼部健康带来危害。普通台灯&#xff0c;尤其是使用白炽灯或荧光灯作为光源的台灯&#xff0c;会发射出紫外线。这些紫外线辐射对孩子的…

HTML新春烟花盛宴

目录 写在前面 烟花盛宴 完整代码 修改文字

内存卡频频提示格式化?数据恢复全攻略

内存卡提示需要格式化 在数字时代&#xff0c;内存卡作为我们存储数据的常用设备&#xff0c;广泛应用于手机、相机、无人机等多种设备中。然而&#xff0c;不少用户在使用过程中会突然遭遇一个令人头疼的问题——内存卡提示需要格式化。这一提示往往伴随着数据的丢失风险&…

端午节趣味互动小游戏的作用是什么

端午节吃粽子&#xff0c;多数行业商家都可借势进行品牌营销&#xff0c;而一场营销效果的优劣&#xff0c;除了好方案外&#xff0c;还需要好的工具/渠道及运营等&#xff0c;围绕粽子元素的互动小游戏是营销互动的主要形式之一。 运用【雨科】平台拥有多款端午节粽子主题互动…

STM32-10-定时器

STM32-01-认识单片机 STM32-02-基础知识 STM32-03-HAL库 STM32-04-时钟树 STM32-05-SYSTEM文件夹 STM32-06-GPIO STM32-07-外部中断 STM32-08-串口 STM32-09-IWDG和WWDG 文章目录 一、STM32 基础定时器1. 基本定时器简介2. 基本定时器框图3. 基本定时器相关寄存器4. 定时器溢出…

单链表,双向链表,循环链表

文章目录 链表单链表双向链表循环链表链表的底层结构链表的实现代码 链表 链表分为单链表&#xff0c;双向链表&#xff0c;循环链表。 单链表 如上图所示&#xff0c;链表是由多个节点组成&#xff0c;节点由数据域与指针域组成&#xff0c;数据域用于存储数据&#xff0c;指…

服务器主机托管一站式托管服务有哪些?

服务器主机托管一站式托管服务&#xff0c;作为现代企业信息化建设的重要一环&#xff0c;为企业提供了一种高效、安全、可靠的服务器运行环境。下面&#xff0c;我们将从多个方面详细介绍这一服务的内容。 一、硬件与基础设施 服务器主机托管服务首先涵盖了服务器硬件和网络基…

windows环境redis未授权利用手法总结

Redis未授权产生原因 1.redis绑定在0.0.0.0:6379默认端口&#xff0c;直接暴露在公网&#xff0c;无防火墙进行来源信任防护。 2.没有设置密码认证&#xff0c;可以免密远程登录redis服务 漏洞危害 1.信息泄露&#xff0c;攻击者可以恶意执行flushall清空数据 2.可以通过ev…

前端Vue自定义个性化导航栏菜单组件的设计与实现

摘要&#xff1a; 随着前端技术的飞速发展和业务场景的日益复杂&#xff0c;组件化开发已成为提升开发效率和降低维护成本的关键手段。本文将以Vue uni-app平台为例&#xff0c;介绍如何通过自定义导航栏菜单组件&#xff0c;实现业务逻辑与界面展示的解耦&#xff0c;以及如何…

25 使用MapReduce编程了解垃圾分类情况

测试数据中1表示可回收垃圾&#xff0c;2表示有害垃圾&#xff0c;4表示湿垃圾&#xff0c;8表示干垃圾。 统计数据中各类型垃圾的数量&#xff0c;分别存储可回收垃圾、有害垃圾、湿垃圾和干垃圾的统计结果。 &#xff08;存储到4个不同文件中&#xff0c;垃圾信息&#xff0…

高效记录收支明细,预设类别账户,智能统计财务脉络,轻松掌握个人财务!

收支明细管理是每位个人或企业都必须面对的财务任务&#xff0c;财务管理已经成为我们生活中不可或缺的一部分。如何高效记录收支明细&#xff0c;预设类别账户&#xff0c;智能统计财务脉络&#xff0c;轻松掌握个人财务&#xff1f;晨曦记账本为您提供了完美的解决方案&#…

JVM的垃圾回收机制--GC

垃圾回收机制&#xff0c;是java提供的对于内存自动回收的机制。java不需要像C/C那样手动free()释放内存空间&#xff0c;而是在JVM中封装好了。垃圾回收机制&#xff0c;不是java独创的&#xff0c;现在应该是主流编程语言的标配。GC需要消耗额外的系统资源&#xff0c;而且存…

“2024深圳数字能源展”共同探讨数字能源未来发展方向和挑战

在数字化浪潮的推动下&#xff0c;新一轮科技革命与产业革命正以前所未有的速度加速兴起。在这个时代背景下&#xff0c;数字化技术与能源行业的高度融合&#xff0c;使得能源数字化成为未来发展的必然趋势。数字经济浪潮的蓬勃兴起&#xff0c;为能源行业的数字化转型提供了强…

网络安全-钓鱼篇-利用cs进行钓鱼

一、环境 自行搭建&#xff0c;kill&#xff0c;Windows10&#xff0c;cs 二、原理 如图所示 三、钓鱼演示 首先第一步&#xff1a;打开System Profiler-分析器功能 选择克隆www.baidu.com页面做钓鱼 之后我们通过包装域名&#xff0c;各种手段让攻击对象访问&#xff1a;h…

XXE漏洞详解——进阶篇

读取文件时有特殊符号 在读取文件时&#xff0c;文件中包含"<,>,&"等这些特殊符号时&#xff0c;会被xml解析器解析&#xff0c;报错从而导致读取失败&#xff0c;例如尝试读取以下文件 C:\test.txt 内容&#xff1a; <Baize Sec> payload: <…

搜维尔科技:Movella Xsens用于动画,CG,短视频制作案例

用户名称 广州百漫文化传播有限公司 应用场景 基于Xsens MVN Link 动作捕捉系统的动画制作、CG制作、短视频制作、快速动画MAYA插件、影视动漫实时合成预渲染。 现场照片 《西行纪》内容简介&#xff1a;在远古神明的年代&#xff0c;世间存在着天众、龙众、阿修罗等八部众…

探索 ChatboxAI:智能对话的新时代

在人工智能迅速发展的今天&#xff0c;智能对话已经成为了我们日常生活中不可或缺的一部分。从智能助理到聊天机器人&#xff0c;AI 技术正在改变我们与世界互动的方式。今天&#xff0c;我们要介绍的是一个全新且功能强大的平台——ChatboxAI。 什么是 ChatboxAI&#xff1f;…