【二十四】【C++】多态

news2024/11/26 10:18:29

多态的基本概念

多态是一种允许使用相同的接口来访问不同的底层形式(类型)的对象的能力。C++中的多态主要通过以下两种方式实现:

编译时多态(静态多态):通过函数重载运算符重载实现。

运行时多态(动态多态):通过虚函数继承实现。

编译时多态(静态多态)

编译时多态是在编译期决定的,主要通过函数重载和运算符重载来实现。

函数重载:在同一个作用域内,可以声明几个功能相似的同名函数,但是这些函数的参数类型和/或数量不同,编译器根据函数调用时的参数类型和数量来决定具体调用哪个函数。

运算符重载:允许定义或重新定义大部分C++内置的运算符作用于自定义类型的操作,这使得运算符可以根据操作数的类型来执行不同的操作。

运行时多态(动态多态)

运行时多态是在程序运行时决定的,主要依靠虚函数(virtual function)和继承机制来实现。

虚函数:在基类中用virtual关键字声明的函数,可以在派生类中被重写(override)。这样,基类指针或引用指向派生类对象时,调用的是派生类中的函数,这种机制称为动态链接或后期绑定。

纯虚函数:在基类中声明为纯虚函数(通过virtual 返回类型 函数名() = 0;的方式声明),这样的基类称为抽象基类,不能直接实例化。纯虚函数必须在派生类中被实现,除非派生类也是一个抽象类。

多态的使用

多态使得我们可以使用基类的指针或引用来操作不同的派生类对象,而具体调用哪个类的哪个方法,则是在运行时决定的。这种机制使得代码更加灵活和通用,易于扩展和维护。

 
#include <iostream>
using namespace std;

class Shape {
public:
    virtual void draw() = 0; // 纯虚函数
};

class Circle : public Shape {
public:
    void draw() override {
        cout << "Drawing Circle" << endl;
    }
};

class Rectangle : public Shape {
public:
    void draw() override {
        cout << "Drawing Rectangle" << endl;
    }
};

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();
    
    shape1->draw(); // 输出: Drawing Circle
    shape2->draw(); // 输出: Drawing Rectangle

    delete shape1;
    delete shape2;
    return 0;
}

在这个示例中,通过Shape类的指针调用draw()函数时,具体调用哪个版本的draw()(是Circle的还是Rectangle的),取决于指针实际指向的对象类型。这就是运行时多态的体现。

Shape类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。但是虚函数重写,导致虚基类中的draw函数变化,Shape类的指针在地址上找到的相符合的信息部分是被重写过的函数。

不使用多态的版本(隐藏)

 
#if 1
#include <iostream>
using namespace std;

class Shape {
public:
    void draw() {
        cout << "Drawing Shape" << endl;
    };
 };

class Circle : public Shape {
public:
    void draw()  {
        cout << "Drawing Circle" << endl;
    }
 };

class Rectangle : public Shape {
public:
    void draw()  {
        cout << "Drawing Rectangle" << endl;
    }
 };

int main() {
    Shape* shape1 = new Circle();
    Shape* shape2 = new Rectangle();
    
     shape1->draw(); 
     shape2->draw(); 
     

     return 0;
 }
#endif

Shape类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。派生类和基类有名字相同的成员,会发生隐藏,隐藏基类的成员,此时派生类有两个相同的成员,如果要访问基类成员需要使用作用域限定符。

虚函数

虚函数是C++中支持多态性的一种机制。它允许在派生类中重写基类的方法,使得通过基类指针或引用调用这些方法时,能够执行到派生类中相应的重写版本,实现运行时的多态性。这意味着在程序执行期间,可以根据对象的实际类型来决定调用哪个函数,而不是在编译时。

定义虚函数

在基类中,使用virtual关键字声明的成员函数就是虚函数。声明虚函数的目的是允许在派生类中对其进行重写(override)。

 
class Base {
public:
    virtual void show() {
        cout << "Base class show" << endl;
    }
};

在上面的代码中,show()函数是一个虚函数。

虚函数的重写

虚函数重写(Override)允许子类重新定义基类中的虚函数实现。这是实现多态性的关键机制之一。通过虚函数重写,可以在运行时根据对象的实际类型调用相应类的方法,而不是在编译时确定。

虚函数重写的实现

要实现虚函数重写,需要遵循以下几个步骤:

在基类中声明虚函数:首先,你需要在基类中使用 virtual 关键字声明一个虚函数。这表示该函数可以在任何派生类中被重写。

 
class Base {
public:
    virtual void show() {
        cout << "Base class show function called." << endl;
    }
};

这里我们声明了一个基类 Base,其中包含一个虚函数 show()。使用 virtual 关键字标记了 show() 函数,表示它可以在派生类中被重写。

在派生类中重写虚函数:在派生类中,你可以重写基类中声明的虚函数。重写时,函数的返回类型、名称以及参数列表必须与基类中声明的虚函数完全相同。

 
class Derived : public Base {
public:
    void show() override { // C++11引入了override关键字,确保了函数重写的正确性
        cout << "Derived class show function called." << endl;
    }
};

这里我们创建了一个 Derived 类,它从 Base 类继承。在 Derived 类中,我们重写了 show() 函数。使用 override 关键字(C++11引入)表明这是一个重写的函数,这有助于编译器检查函数签名的一致性,确保我们正确地重写了函数。

虚函数重写的特定要求

函数签名必须匹配:重写的虚函数必须与基类中的原始虚函数有相同的函数签名(即相同的返回类型、函数名称和参数列表)。

基类函数必须是虚函数:只有被声明为 virtual 的基类函数才可以被派生类重写。

使用 override 关键字(可使用,也可不使用,但推荐):在C++11及以后的版本中,可以在派生类的函数声明后使用 override 关键字。这不是必需的,但它可以帮助编译器检查派生类是否正确地重写了基类的虚函数。

协变

在C++中,协变主要应用于虚函数的返回类型。协变允许派生类中重写的虚函数拥有与基类虚函数不同的返回类型,但这两个返回类型必须保持一定的继承关系。

协变的基本规则

只适用于返回类型:协变仅适用于方法返回类型的变化。

派生关系:派生类中重写的虚函数的返回类型,必须是基类对应虚函数返回类型的派生类(或同类型,同类型不是协变)。

协变的实现

假设有一个基类 Base 和一个从 Base 派生的类 Derived,同时有一个返回 Base 类型指针的虚函数。在派生类中重写该虚函数时,可以返回 Derived 类型的指针,这就是协变的体现。

示例代码

 
class Base {
public:
    virtual Base* clone() const {
        // 实现克隆自己的逻辑
        return new Base(*this);
    }
};

class Derived : public Base {
public:
    Derived* clone() const override {
        // 实现克隆自己的逻辑
        return new Derived(*this);
    }
};

基类 Base 有一个名为 clone 的虚函数,返回类型是指向 Base 类的指针。

派生类 Derived 重写了 clone 函数,并将返回类型改为指向 Derived 类的指针。这里展示了协变:返回类型从 Base* 变为了 Derived*

注意,这里 Derived::clone 函数正确地重写了基类中的 clone 函数,尽管返回类型不同。这是因为 Derived*Base* 的协变返回类型,且 DerivedBase 的派生类。

协变的特定要求和限制

只适用于返回类型:协变只能用于方法的返回类型。

继承关系:派生类方法的返回类型必须是基类方法返回类型的派生类型。

指针和引用:协变仅适用于返回类型为指针或引用的情况。对于返回具体对象的情况,不支持协变。

虚析构函数的重写

虚析构函数允许通过基类指针来正确地删除派生类对象。当一个基类声明了虚析构函数时,派生类的析构函数自动成为虚函数,无论是否显式地使用 virtual 关键字。虚析构函数确保了当通过基类指针删除派生类对象时,能够先调用派生类的析构函数,然后是基类的析构函数,从而正确地释放资源。

析构函数的重要性

如果基类的析构函数不是虚函数,则删除派生类对象时可能只会调用基类的析构函数,从而导致派生类分配的资源没有被正确释放,引发内存泄露。声明虚析构函数可以防止这种情况。

重写虚析构函数

 
/*虚析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:
    virtual ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
 };

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
 };

int main(){
    Base* p1=new Base;
    Base* p2=new Derived;
    
     delete p1;
    delete p2;
 }

#endif

基类 Base 有一个虚析构函数。使用 virtual 关键字确保了析构函数是虚拟的。

派生类 Derived 重写了析构函数。虽然没有显式使用 virtual 关键字,但由于基类的析构函数是虚的,派生类的析构函数自动成为虚函数。

派生类Derived 析构函数的调用顺序,是先调用派生类的析构函数再调用基类的虚构函数。

不重写析构函数

 
/*不重写析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:
     ~Base() {
        std::cout << "Base destructor called." << std::endl;
    }
 };

class Derived : public Base {
public:
    ~Derived() {
        std::cout << "Derived destructor called." << std::endl;
    }
 };

int main(){
    Base* p1=new Base;
    Base* p2=new Derived;
    
     delete p1;
    delete p2;
 }

#endif

析构函数的特定要求

正确的资源释放:当通过基类指针删除派生类对象时,虚析构函数确保派生类和基类的析构函数都会被正确调用,从而释放所有相关资源。顺序是先调用派生类的析构函数,再调用基类的析构函数。

基类应该有虚析构函数:如果一个类被设计为基类,并且预期它会被其他类继承,则应该声明一个虚析构函数,即使析构函数不执行任何操作。

注意点

虚析构函数的性能影响:虚函数(包括虚析构函数)可能会引入轻微的性能开销,因为它们需要通过虚表(vtable)来解析调用。但在绝大多数情况下,这种开销是可以接受的,特别是考虑到它带来的正确性和灵活性。

避免内存泄漏:即使派生类没有分配任何动态内存,如果基类有虚析构函数,派生类也应正确重写析构函数,以避免潜在的资源管理问题。

C++中 override 和 final 关键字

在C++中,overridefinal 是两个与虚函数重写相关的关键字,它们在C++11标准中引入。这两个关键字提供了额外的语义,帮助程序员更明确地表达他们的设计意图,同时也使得编译器能够提供更好的检查和优化。

override

当你在派生类中重写基类的虚函数时,可以在派生类的函数声明后面使用 override 关键字。这表明该函数旨在重写一个基类中的虚函数。如果声明的函数没有重写任何基类中的虚函数(比如由于函数签名不匹配),编译器将报错。这有助于捕捉到可能的错误,比如拼写错误或者错误的参数类型。

 
class Base {
public:
    virtual void func() {}
};

class Derived : public Base {
public:
    void func() override {} // 正确重写
    // void fun() override {} // 编译错误,因为Base中没有fun()函数
};

在这个例子中,Derived 类通过 override 关键字标明它意图重写 Base 类中的 func() 函数。如果 Derived 类中的函数签名与任何基类中的虚函数都不匹配,编译器将报错。

final

final 关键字可以用于防止类被进一步派生或虚函数被进一步重写。当你将一个类标记为 final 时,任何尝试从这个类派生出新类的行为都会导致编译错误。同样,将虚函数标记为 final 会阻止任何派生类重写该函数。

 
class Base {
public:
    virtual void func() final {} // 防止进一步重写
};

class Derived final : public Base { // 防止进一步派生
public:
    // void func() override {} // 编译错误,因为Base::func()被标记为final
};

// class MoreDerived : public Derived {}; // 编译错误,因为Derived被标记为final

在这个例子中,Base 类中的 func() 被标记为 final,这意味着任何尝试在派生类中重写 func() 的操作都会导致编译错误。同时,Derived 类被标记为 final,防止任何进一步的派生。

使用 override 和 final 的好处

提高代码清晰度:使用 overridefinal 关键字可以让你的代码意图更加明确,使其他开发者更容易理解你的设计意图。

编译时检查:它们允许编译器在编译时进行额外的检查,捕捉潜在的错误,比如签名不匹配或错误地重写了不应该重写的函数。

优化支持:明确指出哪些函数不会被进一步重写,可以帮助编译器做出更好的优化决策。

重载、重写、重定义(隐藏)

重载 (Overloading)

重载指的是在相同作用域有两个或多个函数拥有相同的名称,但是它们的参数列表不同(参数类型、个数或者顺序不同)。重载使得函数可以根据不同的参数执行不同的任务。

  • 两个函数在同一作用域

  • 函数名相同

  • 参数列表不同

 
void print(int i) {
    std::cout << "Printing int: " << i << std::endl;
}

void print(double f) {
    std::cout << "Printing float: " << f << std::endl;
}

void print(const std::string &s) {
    std::cout << "Printing string: " << s << std::endl;
}

在这个例子中,print 函数被重载了三次,分别接受 intdoublestd::string 类型的参数。

重写 (Overriding)

重写是面向对象编程中的一个概念,指的是派生类中的函数重写了基类中具有相同名称相同参数列表相同返回值(或协变)的虚函数。重写用于实现运行时多态。

  • 两个函数分别在基类和派生类的作用域

  • 函数名、参数列表、返回值必须相同(协变、虚析构函数除外)

  • 两个函数必须是虚函数

 
class Base {
public:
    virtual void display() {
        std::cout << "Display Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() override {  // 使用override确保正确重写
        std::cout << "Display Derived class" << std::endl;
    }
};

在这个例子中,Derived 类重写了 Base 类中的 display 函数。

重定义 (Hiding or Redefinition)

重定义发生在派生类中,当派生类定义了一个与基类同名的成员(不论参数列表是否相同),该成员会隐藏(或称为重定义)基类中所有同名的成员,不论其参数列表。这不是多态,而是名字隐藏。

  • 两个函数分别在基类和派生类的作用域

  • 函数名相同

  • 不构成重写

 
class Base {
public:
    void display() {
        std::cout << "Display Base class" << std::endl;
    }
};

class Derived : public Base {
public:
    void display(int i) {  // 重定义,隐藏了基类的display()
        std::cout << "Display Derived class with int: " << i << std::endl;
    }
};

在这个例子中,尽管 Derived 类中的 display 函数的参数列表与基类中的不同,它仍然隐藏了基类中的 display() 函数。

小结论

重载

  • 两个函数在同一作用域

  • 函数名相同

  • 参数列表不同

重写

  • 两个函数分别在基类和派生类的作用域

  • 函数名、参数列表、返回值必须相同(协变、虚析构函数除外)

  • 两个函数必须是虚函数

重定义

  • 两个函数分别在基类和派生类的作用域

  • 函数名相同

  • 不构成重写

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

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

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

相关文章

基于数字双输入的超宽带Doherty功率放大器设计-从理论到ADS版图

基于数字双输入的超宽带Doherty功率放大器设计-从理论到ADS版图 参考论文: 高效连续型射频功率放大器研究 假期就要倒计时啦&#xff0c;估计是寒假假期的最后一个博客&#xff0c;希望各位龙年工作顺利&#xff0c;学业有成。 全部工程下载&#xff1a;基于数字双输入的超宽…

机器人初识 —— 定制AI

一、机器人设计难点 波士顿动力设计的机器人&#xff0c;尤其是其人形机器人Atlas和四足机器人Spot等产品&#xff0c;在技术上面临多重难点&#xff1a; 1. **动态平衡与稳定性**&#xff1a;双足或四足机器人在运动时需要维持极高的动态平衡&#xff0c;特别是在不平坦地面…

KMP算法简介以及相关例题的分析

一.KMP算法简介 KMP 算法是 D.E.Knuth、J,H,Morris 和 V.R.Pratt 三位神人共同提出的&#xff0c;称之为 Knuth-Morria-Pratt 算法&#xff0c;简称 KMP 算法。该算法相对于 Brute-Force&#xff08;暴力&#xff09;算法有比较大的改进&#xff0c;主要是消除了主串指针的回溯…

【Java面试】MongoDB

目录 1、mongodb是什么&#xff1f;2、mongodb特点什么是NoSQL数据库&#xff1f;NoSQL和RDBMS有什么区别&#xff1f;在哪些情况下使用和不使用NoSQL数据库&#xff1f;NoSQL数据库有哪些类型?启用备份故障恢复需要多久什么是master或primary什么是secondary或slave系列文章版…

【Vuforia+Unity】01实现单张多张图片识别产生对应数字内容

1.官网注册 Home | Engine Developer Portal 2.下载插件SDK&#xff0c;导入Unity 3.官网创建数据库上传图片&#xff0c;官网处理成数据 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 下载好导入Unity&#xff01; 4.在Unity设…

unity C#中的封装、继承和多态简单易懂的经典实例

文章目录 封装 (Encapsulation)继承 (Inheritance)多态 (Polymorphism) C#中的封装、继承和多态是面向对象编程&#xff08;OOP&#xff09;的三大核心特性。下面分别对这三个概念进行深入解释&#xff0c;并通过实例来说明它们在实际开发中的应用。 封装 (Encapsulation) 实例…

11. Springboot集成Dubbo3(二)示例demo

目录 1、前言 2、注册中心 3、快速开始 3.1、添加dubbo3依赖 3.2、dubbo3-api ​编辑 3.3、dubbo3-server 3.3.1、添加依赖 3.3.2、实现IUserService 3.3.3、添加配置文件application.properties 3.3.4、修改Application启动类 3.3.5、出错解决 3.4、dubbo3-porta…

世界顶级名校计算机专业,都在用哪些书当教材?

前言 在当今信息化、数字化时代&#xff0c;计算机科学已成为全球最为热门和重要的学科之一。世界顶级名校的计算机专业&#xff0c;更是培养未来行业领袖和创新人才的重要基地。那么&#xff0c;这些名校的计算机专业究竟使用哪些教材呢&#xff1f;这些教材又具有哪些特色和…

智能化机械生产引擎:亿发制造ERP系统助帮助工厂真正把控车间管理

工厂的制造管理过程以车间管理为核心&#xff0c;而车间管理涉及到生产的下达、派工、汇报等复杂流程&#xff0c;几乎包含了生产的全过程。这种繁琐性使得车间管理变得异常困难&#xff0c;因此&#xff0c;引入一款专业的制造ERP软件成为解决难题的有效途径。 在制造业引入E…

文件IO及目录IO——day05

文件IO还剩下一个知识点&#xff0c;今天主要内容是目录IO 文件IO lseek lseekoff_t lseek(int fd, off_t offset, int whence); 功能:重新设定文件描述符的偏移量 参数:fd:文件描述符offset:偏移量whence:SEEK_SET 文件开头SEEK_CUR 文件当前位置SEEK_END 文件末尾…

【PyQt6] 框选截图功能

1 简介 书接上回, 全屏截图实现起来很简单, 来点稍微复杂点的, 框选截图 原理很简单, 弄个控件实现全屏半透视, 在全屏控件上画一个选框或者再弄一个几乎全透的子控件,实现鼠标拖动,缩放,移动, 键盘wasd 微调 用一个控件实现起来会很完美, 但是逻辑全部堆砌在一起,看代码会很…

PWM功能介绍 和配置

泰山派默认提供了3组PWM的GPIO &#xff0c; 为了检测PWM的输出&#xff0c;我们可以配合逻辑分析仪来查看效果&#xff0c;或者搭配STC8的LED灯 PWM 测试 列举所有的PWM设备&#xff1a; # 查找所有有pwm名称的文件 find / -name "pwm" # pwm4: pwmfe6e0000 edp屏幕…

VPX信号处理卡设计原理图:9-基于DSP TMS320C6678+FPGA XC7V690T的6U VPX信号处理卡 信号处理 无线电通信

一、概述 本板卡基于标准6U VPX 架构&#xff0c;为通用高性能信号处理平台&#xff0c;系我公司自主研发。板卡采用一片TI DSP TMS320C6678和一片Xilinx公司Virtex 7系列的FPGA XC7V690T-2FFG1761I作为主处理器&#xff0c;Xilinx 的Aritex XC7A200T作为辅助处理器。XC7A2…

OpenAI视频生成模型Sora的全面解析:从ViViT、扩散Transformer到NaViT、VideoPoet

前言 真没想到&#xff0c;距离视频生成上一轮的集中爆发(详见《视频生成发展史&#xff1a;从Gen2、Emu Video到PixelDance、SVD、Pika 1.0、W.A.L.T》)才过去三个月&#xff0c;没想OpenAI一出手&#xff0c;该领域又直接变天了 自打2.16日OpenAI发布sora以来(其开发团队包…

30分钟快速上手LaTex

文章目录 30 分钟快速上手 LATEX1.什么是LATEX?2.为什么学习LATEX?3.编写第一个LATEX程序4.LATEX文档的序言5.LATEX文档的标题、作者和日期信息6.LATEX文档的注释7.LATEX文档的粗体、斜体和下划线8.LATEX文档中添加图片9.LATEX中对图像进行标注、标签化和引用10.在LATEX中创建…

105.网游逆向分析与插件开发-网络通信封包解析-分析接收到的对话数据包

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;接收数据的初步逆向分析 通过上一个内容&#xff0c;找到了数据包出现的一个很重要的位置&#xff0c;只要hook之后就能很好的得到这个数据了 然后来到明文数据的位置&#xff0c;把数据包复制出来&…

跨境电商独立站是什么?为什么要做独立站?

独立站在近两年被推上风口&#xff0c;很多人跟风涌入赛道&#xff0c;但并不知道做独立网站的根本原因是什么&#xff1f;为什么跨境电商要做独立站&#xff1f; 今天分享这篇文章&#xff0c;希望能帮助正在建站或想要建站的朋友们建立起对独立站的优劣势、未来发展空间的一…

《剑指Offer》笔记题解思路技巧优化 Java版本——新版leetcode_Part_4

《剑指Offer》笔记&题解&思路&技巧&优化_Part_4 &#x1f60d;&#x1f60d;&#x1f60d; 相知&#x1f64c;&#x1f64c;&#x1f64c; 相识&#x1f622;&#x1f622;&#x1f622; 开始刷题1. LCR 148. 验证图书取出顺序——栈的压入、弹出序列2. LCR 14…

Linux:grep进阶(11)

Linux&#xff1a;shell脚本&#xff1a;基础使用&#xff08;4&#xff09;《正则表达式-grep工具》_shell grep 全角字符串-CSDN博客https://blog.csdn.net/w14768855/article/details/132338954?ops_request_misc%257B%2522request%255Fid%2522%253A%252217083360171680022…

状压dp,HDU1074.Doing Homework

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 Ignatius has just come back school from the 30th ACM/ICPC. Now he has a lot of homework to do. Every teacher giv…