《21天学通C++》(第十一章)多态

news2024/11/24 12:39:49

为什么需要多态?
为了最大限度地减少代码,提高可读性

1.虚函数

虚函数是C++中的一种特殊成员函数,它允许在派生类(也称为子类)中重写(覆盖)基类的实现,使用virtual进行声明

在C++中,如果基类中的成员函数不是虚函数,派生类中的同名函数并不会覆盖或重写基类中的函数,而是产生函数隐藏,意味着如果你通过基类类型的指针或引用调用该函数,实际上调用的是基类中的版本,而不是派生类中的版本。

不使用虚函数:

#include <iostream>
using namespace std;

class Base {
public:
    // 普通函数,不是虚函数
    void func() {
        cout << "Base func" << endl;
    }
};

class Derived : public Base {
public:
    // 看起来像是重写,实际上是函数隐藏
    void func() {
        cout << "Derived func" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->func(); // 调用 Base::func,而不是 Derived::func
    //输出结果为Base func
    delete basePtr;
    system("pause");
    return 0;
}

使用虚函数

#include <iostream>
using namespace std;

class Base {
public:
    // 声明为虚函数
    virtual void func() {
        cout << "Base func" << endl;
    }
};

class Derived : public Base {
public:
	//真正地重写
    void func() {
        cout << "Derived func" << endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->func(); // 正确调用 Derived::func
    //输出结果为Derived func
    delete basePtr;
    system("pause");
    return 0;
}

2.使用虚函数实现多态行为

通过函数引用实现

#include <iostream>
using namespace std;

// 基类 Fish,定义了鱼类的通用行为
class Fish {
public:
    // 虚函数 swim,允许派生类重写,实现多态
    virtual void swim() const {
        cout << "Fish is swimming" << endl;
    }
};

// 派生类 Tuna,继承自 Fish
class Tuna : public Fish {
public:
    // 重写 Fish 类的 swim 函数,实现 Tuna 类特有的游泳行为
    void swim() const override {
        cout << "Tuna is swimming fast" << endl;
    }
};

// 派生类 Carp,继承自 Fish
class Carp : public Fish {
public:
    // 重写 Fish 类的 swim 函数,实现 Carp 类特有的游泳行为
    void swim() const override {
        cout << "Carp is swimming slowly" << endl;
    }
};

// 函数,使用 Fish 类的引用参数来实现多态
void makeFishSwim(const Fish& fish) {
    fish.swim(); // 根据传入对象的实际类型调用相应的 swim 方法
}

int main() {
    Tuna tuna;
    Carp carp;

    // 通过引用传递给函数,实现多态
    makeFishSwim(tuna); // 输出 "Tuna is swimming fast"
    makeFishSwim(carp); // 输出 "Carp is swimming slowly"
    system("pause");
    return 0;
}

通过指针实现:

#include <iostream>
using namespace std;

// 基类 Fish,定义了鱼类的通用行为
class Fish {
public:
    // 虚函数 swim,允许派生类重写,实现多态
    virtual void swim() {
        cout << "Fish is swimming" << endl;
    }

    // 虚析构函数
    virtual ~Fish() {
        cout << "Fish is deconstructed" << endl;
    }
};

// 派生类 Tuna,继承自 Fish
class Tuna : public Fish {
public:
    // 重写 Fish 类的 swim 函数,实现 Tuna 类特有的游泳行为
    void swim() override {
        cout << "Tuna is swimming fast" << endl;
    }

    // Tuna 类的析构函数
    ~Tuna() {
        cout << "Tuna is deconstructed" << endl;
    }
};

// 派生类 Carp,继承自 Fish
class Carp : public Fish {
public:
    // 重写 Fish 类的 swim 函数,实现 Carp 类特有的游泳行为
    void swim() override {
        cout << "Carp is swimming slowly" << endl;
    }

    // Carp 类的析构函数
    ~Carp() {
        cout << "Carp is deconstructed" << endl;
    }
};

int main() {
    // 创建派生类对象
    Fish* fish = new Tuna();
    fish->swim(); // 调用 Tuna::swim,输出 "Tuna is swimming fast"

    Fish* carp = new Carp();
    carp->swim(); // 调用 Carp::swim,输出 "Carp is swimming slowly"

    // 删除对象,调用相应的析构函数
    delete fish;
    delete carp;
	system("pause");
    return 0;
}

3.虚函数的工作原理——虚函数表

虚函数表(通常称为vtable)是C++中实现运行时多态的一种机制。当一个类包含至少一个虚函数时,编译器会为这个类创建一个虚函数表,这张表包含了类中所有虚函数的地址。

工作流程如下:

1.虚函数表的创建: 当一个类中包含至少一个虚函数时,编译器会为这个类创建一个虚函数表。这个表包含了该类所有虚函数的地址。

2.虚函数表指针: 编译器为每个对象添加一个指针,指向其类的虚函数表。这个指针通常存储在对象的内存布局的最前面。

3.调用虚函数: 当你通过一个基类指针或引用调用一个虚函数时,编译器生成的代码首先会访问对象的虚函数表指针,然后查找并调用表中对应的函数。

4.动态绑定: 由于虚函数表的存在,函数调用的解析是在运行时进行的,这称为动态绑定或晚期绑定。这意味着即使基类指针指向的是派生类对象,调用的也是派生类中重写的函数版本。

class Base {
public:
    virtual void show() {
        std::cout << "Base show" << std::endl;
    }
    virtual ~Base() {}  // 虚析构函数
};

class Derived : public Base {
public:
    void show() override {  // 重写基类中的虚函数
        std::cout << "Derived show" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();  // 创建Derived对象的指针,但声明为Base类型
    basePtr->show();  // 调用show(),虽然basePtr是Base类型,但实际调用的是Derived的show()
    delete basePtr;
    return 0;
}

4.抽象基类和纯虚函数

抽象基类: 至少包含一个纯虚函数,而且无法被实例化,只能用于派生其他类,简称为ABC

纯虚函数: 它在基类中声明但故意不提供实现,其声明的函数体部分使用 = 0 来标识

virtual ReturnType FunctionName() = 0;

抽象基类使用方法如下:

#include <iostream>
using namespace std;

// 抽象基类
class Shape {
public:
    // 纯虚函数,用于定义绘制形状的接口
    virtual void draw() const = 0;

    // 虚析构函数,确保派生类的析构函数被正确调用
    virtual ~Shape() {}
};

// 派生类 Circle,表示圆形
class Circle : public Shape {
public:
    // 实现 Circle 的 draw 方法
    void draw() const override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

// 派生类 Rectangle,表示矩形
class Rectangle : public Shape {
public:
    // 实现 Rectangle 的 draw 方法
    void draw() const override {
        std::cout << "Drawing a rectangle." << std::endl;
    }
};


int main() {
    // 创建一个指向 Shape 的指针数组,用于存储不同形状的指针
    Shape* shapes[] = { new Circle(), new Rectangle() };

    // 使用基类指针调用 draw 方法,实现多态
    for (Shape* shape : shapes) {
        shape->draw(); // 根据对象的实际类型调用相应的派生类的 draw 方法
    }

    // 释放动态分配的内存
    for (Shape* shape : shapes) {
        delete shape;
    }
    system("pause");
    return 0;
}

5.使用虚继承解决菱形问题

菱形问题: 即一个派生类继承自两个中间基类,而这两个中间基类又都继承自同一个基类时。这种继承结构在类图上看起来像一个菱形,因此得名。
在这里插入图片描述
田园犬类同时继承狗类和哺乳类,而哺乳类和狗类又同时继承动物类,呈现一个菱形结构。

在这个例子中田园犬类会分别从狗类和哺乳类中各自继承一个动物类,导致内存浪费和潜在的一致性问题,所以为了解决这个问题,可以使用虚函数继承来解决

#include <iostream>
using namespace std;

// 定义基类 Animal
class Animal {
public:
    // 动物的呼吸方法
    virtual void breathe() { cout << "Animal breathes" << endl; }
    // 虚析构函数,确保派生类可以正确释放资源
    virtual ~Animal() {}
};

// 定义中间基类 Mammal,使用虚继承自 Animal
class Mammal : virtual public Animal {
public:
    // 哺乳动物特有的哺育行为
    void nurse() { cout << "Mammal nurses its young" << endl; }
    // 虚析构函数
    virtual ~Mammal() {}
};

// 定义中间基类 Dog,使用虚继承自 Animal
class Dog : virtual public Animal {
public:
    // 狗的吠叫行为
    void bark() { cout << "Dog barks" << endl; }
    // 虚析构函数
    virtual ~Dog() {}
};

// 定义派生类 Poodle,同时继承自 Dog 和 Mammal
class Poodle : public Dog, public Mammal {
public:
    // 贵宾犬特有的行为
    void prance() { cout << "Poodle prances" << endl; }
    // 虚析构函数
    virtual ~Poodle() {}
};

// 主函数
int main() {
    // 创建 Poodle 对象
    Poodle myPoodle;
    // 调用从各个基类继承来的方法
    myPoodle.bark();    // Dog 类的 bark 函数
    myPoodle.nurse();   // Mammal 类的 nurse 函数
    myPoodle.breathe();  // Animal 类的 breathe 函数
    myPoodle.prance();   // Poodle 类的 prance 函数
    system("pause"); // 用于在控制台程序结束前暂停,以便查看输出
    return 0;
}

6.表明覆盖意图的限定符override

使用override关键字有助于编译器检查函数签名是否与基类中的虚函数相匹配,从而提高代码的可读性和安全性。

使用方法如下:

class Base {
public:
    virtual void function() {
        // 基类
    }
};

class Derived : public Base {
public:
    void function() override { // 使用 override 明确指出重写
        // 派生类
    }
};

7.使用final禁止覆盖函数

final关键字用于阻止派生类进一步重写(覆盖)基类中的虚函数。当你希望某个虚函数在派生类中保持最终实现,不允许任何进一步的重写时,可以使用final关键字。

class Base {
public:
    virtual void function() final {//使用final禁止覆盖
        // 基类实现
    }
};

class Derived : public Base {
public:
    void function() override { // 这里会编译错误,因为 Base::function() 被声明为 final
        // 派生类实现
    }
};

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

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

相关文章

包装类的基础知识

JAVA 是面向对象编程的 但是基本数据类型没有对象的 基本数据类型 byte------>>Byte short------>>Short int------>>Integer long------>>long char------>>Character double------>>Double boolean------>>Boolean 包装类…

ton-http-api安装部署

1、拉取github代码 mkdir /data git clone https://github.com/toncenter/ton-http-api.git cd ton-http-api2、创建环境变量 ./configure.py cat .env TON_API_CACHE_ENABLED0 TON_API_CACHE_REDIS_ENDPOINTcache_redis TON_API_CACHE_REDIS_PORT6379 TON_API_CACHE_REDIS_T…

【酱浦菌-爬虫项目】爬取百度文库文档

1. 首先&#xff0c;定义了一个变量url&#xff0c;指向百度文库的搜索接口 ‘https://wenku.baidu.com/gsearch/rec/pcviewdocrec’。 2. 然后&#xff0c;设置了请求参数data&#xff0c;包括文档ID&#xff08;docId&#xff09;和查询关键词&#xff08;query&#xff09;。…

Unity镂空图像做法

问题和解决方案 现在要完成一个需求&#xff0c;即镂空中间部分的image&#xff0c;外围image可以定义颜色并可选屏蔽点击&#xff0c;而中间的image需要透明且可以穿透&#xff0c;必须不能屏蔽点击。 由此拆分成了两个问题&#xff1a; 1.定义外围image颜色&#xff0c;内…

政安晨:【Keras机器学习示例演绎】(二十五)—— 使用具有三重损失的连体网络进行图像相似性估计

目录 简介 设置 加载数据集 准备数据 设置嵌入生成器模型 建立连体网络模型 将一切整合在一起 训练 检查网络的学习成果 摘要 政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras机器学习实战 希望政安晨的博客能够…

Django框架之ORM操作

一、选择数据库 1、默认数据库 Django默认的数据库是sqlite3数据库 DATABASES {default: {ENGINE: django.db.backends.sqlite3,NAME: BASE_DIR / db.sqlite3,} }2、指定数据库 修改连接到MySQL数据库 DATABASES {default: {ENGINE: django.db.backends.mysql,# 数据库名…

鸿蒙OpenHarmony【标准系统 烧录】(基于RK3568开发板)

烧录 烧录是指将编译后的程序文件下载到芯片开发板上的动作&#xff0c;为后续的程序调试提供基础。DevEco Device Tool提供一键烧录功能&#xff0c;操作简单&#xff0c;能快捷、高效的完成程序烧录&#xff0c;提升烧录的效率。 RK3568的镜像烧录通过Windows环境进行烧录&…

Oracle-OCR掉盘无法启动集群问题

问题背景&#xff1a; 用户在对一套Oracle11.2.0.4的RAC集群进行OCR掉盘测试&#xff0c;验证NORMAL冗余的OCR磁盘组的可用性。测试通过将udev配置里面的一块OCR盘注释&#xff0c;然后重启服务器集群模拟OCR磁盘组出现掉盘的情况。用户在测试中&#xff0c;注释掉udev配置里面…

嵌入式开发_DMA详解

在嵌入式开发的广阔天地中&#xff0c;MCU是众多开发者手中的利器。而DMA&#xff08;Direct Memory Access&#xff0c;直接内存访问&#xff09;技术&#xff0c;则如同一位高效的快递小哥&#xff0c;穿梭于处理器与外设之间&#xff0c;无需CPU亲自出马&#xff0c;高效地搬…

正点原子[第二期]Linux之ARM(MX6U)裸机篇学习笔记-8.1

前言&#xff1a; 本文是根据哔哩哔哩网站上“正点原子[第二期]Linux之ARM&#xff08;MX6U&#xff09;裸机篇”视频的学习笔记&#xff0c;在这里会记录下正点原子 I.MX6ULL 开发板的配套视频教程所作的实验和学习笔记内容。本文大量引用了正点原子教学视频和链接中的内容。…

【工具】--- Adobe Illustrator 下载-入门绘图

文章目录 软件下载入门项目可看课程 尝试使用Adobe Illustrator&#xff08;设计师常用软件&#xff09;进行科研绘图。 软件下载 阿里云盘下载 入门项目 绘制一个箭头并保持为SVG&#xff0c; 直线->画线->窗口->描边->选择想要的箭头样式->颜色->改为蓝…

众筹商城源码 众筹商品平台 商城加共识元富之路 网上商城众筹

众筹商城源码 众筹商品平台 商城加共识元富之路 网上商城众筹 前端是编译后的&#xff0c;后端PHP&#xff0c;带商城 源码下载&#xff1a;https://download.csdn.net/download/m0_66047725/89161734 更多资源下载&#xff1a;关注我。

mybatis工程需要的pom.xml,以及@Data 、@BeforeEach、@AfterEach 的使用,简化mybatis

对 “mybatis - XxxMapper.java接口中方法的参数 和 返回值类型&#xff0c;怎样在 XxxMapper.xml 中配置的问题” 这篇文章做一下优化 这个pom.xml文件&#xff0c;就是上面说的这篇文章的父工程的pom.xml&#xff0c;即&#xff1a;下面这个pom.xml 是可以拿来就用的 <?…

Node.js 版本升级方法

在构建vue项目时&#xff0c;依赖npm&#xff08;Node Package Manager&#xff09;工具&#xff0c;类似于Java项目需要maven管理。而npm是node.js的管理工具&#xff0c;npm依赖node.js环境才能执行。 有时候使用voscode或者其他工具安装vue项目依赖&#xff0c;显示一直处于…

【自然语言处理】Word2VecTranE的实现

作业一 Word2Vec&TranE的实现 1 任务目标 1.1 案例简介 Word2Vec是词嵌入的经典模型&#xff0c;它通过词之间的上下文信息来建模词的相似度。TransE是知识表示学习领域的经典模型&#xff0c;它借鉴了Word2Vec的思路&#xff0c;用“头实体关系尾实体”这一简单的训练目…

【Vue3】Ref与Reactive

3.1【ref 创建&#xff1a;基本类型的响应式数据】 作用&#xff1a;定义响应式变量。语法&#xff1a;let xxx ref(初始值)。返回值&#xff1a;一个RefImpl的实例对象&#xff0c;简称ref对象或ref&#xff0c;ref对象的value属性是响应式的。注意点&#xff1a; JS中操作数…

【在线名字作画HTML源码】

在线名字作画HTML源码 效果图部分源码领取源码下期更新预报 效果图 部分源码 index.htm <!DOCTYPE html> <html> <head> <title>在线名字作画|民间花鸟字|多彩花鸟虫鱼组合书法|藏字画|字谜语|飞帛板书|意匠文字</title> <meta http-equiv&…

Profinet转Modbus网关接称重设备与1200PLC通讯

Profinet转Modbus网关&#xff08;XD-MDPN100&#xff09;是一种能够实现Modbus协议和Profinet协议之间转换的设备。Profinet转Modbus网关可提供单个或多个RS485接口&#xff0c;使用Profinet转Modbus网关将称重设备与西门子1200 PLC进行通讯&#xff0c;可以避免繁琐的编程和配…

【C++】深入了解C++内存管理

个人主页&#xff1a;救赎小恶魔 欢迎大家来到小恶魔频道 好久不见&#xff0c;甚是想念 今天我们要深入讲述类与对象的初始化列表以及隐式类型转换 目录 1.C的内存分布 2.C/C言中动态内存管理方式 1.C语言的管理方式 2.C的管理方式 new delete 3.operator new与ope…

如何删除BigKey

1.2.3、如何删除BigKey BigKey内存占用较多&#xff0c;即便时删除这样的key也需要耗费很长时间&#xff0c;导致Redis主线程阻塞&#xff0c;引发一系列问题。 redis 3.0 及以下版本 如果是集合类型&#xff0c;则遍历BigKey的元素&#xff0c;先逐个删除子元素&#xff0c;…