C++三大特性之一:多态

一、多态

1、通过指针创建对象(动态分配)

#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived class show" << endl;}
};int main() {Base* b = new Derived();b->show(); // 输出:Derived class showdelete b;return 0;
}

在这个程序中,Base 类指针 b 被分配为指向一个 Derived 类对象。因为 show 是一个虚函数,并且 b 实际上指向的是一个 Derived 类对象,所以当调用 b->show() 时,会调用 Derived 类中的 show 方法,输出 "Derived class show"。

2.直接创建对象(静态分配)

#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived class show" << endl;}
};int main() {Base b;b.show(); // 输出:Base class showreturn 0;
}

在这个程序中,Base 类对象 b 是静态分配的。虽然 Base 类有一个虚函数 show,但是 bBase 类的对象,而不是 Derived 类的对象。因此,当调用 b.show() 时,会调用 Base 类中的 show 方法,输出 "Base class show"。 

2.1静态分配具体怎么实现的

在C++中,静态分配意味着对象的内存分配在编译时就已经确定了。这种分配方式通常在栈上进行,当函数执行结束时,这些对象的内存会自动释放。以下是静态分配的详细解释和实现方式:

静态分配的实现

  1. 声明对象:在函数内部或类的成员变量中直接声明对象。这些对象在声明时就分配了内存,并在作用域结束时自动释放。

  2. 作用域管理:静态分配的对象在它们声明的作用域内是有效的。一旦超出作用域,对象的析构函数(如果有)将自动调用,并释放内存。

  3. 示例代码

    以下是一个使用静态分配的示例代码:

#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() {cout << "Base destructor" << endl;}virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}void show() override {cout << "Derived class show" << endl;}
};int main() {Base b;b.show(); // 输出:Base class showreturn 0;
}

对象声明

  1. Base b;
  2. 这行代码在栈上分配一个 Base 类对象 b

  3. 构造函数调用

    • 当对象 b 被声明时,Base 类的构造函数被调用,输出 "Base constructor"。
  4. 虚函数调用

    • 当调用 b.show() 时,因为 bBase 类的对象,而不是 Derived 类的对象,所以调用的是 Base 类的 show 方法,输出 "Base class show"。
  5. 析构函数调用

    • main 函数结束时,b 超出其作用域,Base 类的析构函数被调用,输出 "Base destructor"。

静态分配的特点

  • 内存管理简单:静态分配的对象在作用域结束时自动释放,不需要显式删除。
  • 生命周期受作用域控制:对象的生命周期由它们声明的作用域决定。
  • 栈上分配:静态分配通常在栈上进行,这意味着对象的分配和释放速度很快,但受限于栈的大小。

与动态分配的对比

动态分配则在堆上进行,需要显式的内存管理(例如 newdelete)。它允许在运行时决定对象的生存周期,但也增加了内存泄漏的风险。如果没有正确释放动态分配的内存,程序会导致内存泄漏。

int main() { Base* b = new Derived(); b->show(); // 输出:Derived class show delete b; // 手动释放内存,调用 Derived 和 Base 的析构函数 return 0; }

在上面的动态分配示例中,需要手动释放通过 new 分配的内存(delete b),否则会导致内存泄漏。

 2.2调用构造函数是静态分配吗?

     调用构造函数本身不是静态分配,但构造函数的调用与对象的创建方式有关。静态分配和动态分配分别决定了构造函数在何时以及如何被调用。让我们更详细地探讨静态分配的过程。

静态分配的详细过程

静态分配指的是在编译时确定内存分配,并在栈上分配内存。当在函数内部声明对象时,会自动调用其构造函数进行初始化。静态分配的对象在作用域结束时会自动调用析构函数释放内存。

  • 静态分配:在栈上分配,编译时确定,作用域结束时自动释放。
  • 动态分配:在堆上分配,运行时确定,需要手动释放。

构造函数的调用与对象的创建方式(静态或动态分配)相关,在对象创建时自动调用,无论是静态分配还是动态分配。

2.3对象的创建时,构造函数就自动执行吗?

是的,在C++中,当对象被创建时,构造函数会自动执行以初始化该对象。无论是静态分配还是动态分配,构造函数都会在对象创建时被调用。

#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() {cout << "Base destructor" << endl;}virtual void show() {cout << "Base class show" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}void show() override {cout << "Derived class show" << endl;}
};int main() {Base* b = new Derived(); // 动态分配,b 指向 Derived 对象b->show(); // 输出:Derived class showdelete b;  // 释放内存,调用 Derived 和 Base 的析构函数return 0;
}

运行结果: 

Base constructor
Derived constructor
Derived class show
Derived destructor
Base destructor

解释

  1. 动态分配对象

    Base* b = new Derived();
    

    这行代码在堆上分配了一个 Derived 类对象,并将 b 指针指向该对象。在 new Derived() 执行时,首先调用 Base 类的构造函数,然后调用 Derived 类的构造函数,依次输出 "Base constructor" 和 "Derived constructor"。

  2. 调用成员函数

    调用 b->show() 时,因为 b 实际上指向一个 Derived 类对象,并且 show 是一个虚函数,所以调用 Derived 类的 show 方法,输出 "Derived class show"。
  3. 释放内存

    delete b;

    这行代码释放 b 指向的内存,首先调用 Derived 类的析构函数,然后调用 Base 类的析构函数,依次输出 "Derived destructor" 和 "Base destructor"。

总结

  • 静态分配:对象在栈上分配,声明时自动调用构造函数,作用域结束时自动调用析构函数。
  • 动态分配:对象在堆上分配,new 操作符执行时自动调用构造函数,delete 操作符执行时自动调用析构函数。

在对象创建时,构造函数会自动执行,无论是静态分配还是动态分配。这是对象初始化过程的一个重要部分,确保对象在使用前被正确初始化。

2.4构造函数在项目中的具体运用

1. 初始化成员变量

在对象创建时通过构造函数初始化成员变量,以确保对象在使用前处于有效状态。

2. 资源分配和管理

构造函数可以用于分配资源,例如打开文件、分配动态内存、建立数据库连接等。析构函数用于释放这些资源,防止内存泄漏和资源泄漏。

3. 依赖注入

构造函数可以接受参数,用于注入对象依赖的其他对象。这样可以提高代码的灵活性和可测试性。

4. 单例模式

构造函数可以用于实现设计模式,例如单例模式,确保一个类只有一个实例,并提供一个全局访问点。

2.5纯虚函数和虚函数的区别

(1)虚函数的使用

#include <iostream>
using namespace std;// 抽象基类
class Shape {
public:virtual void draw() {};virtual double area() { return 0; };virtual ~Shape() {} // 虚析构函数
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) {}void draw()  {cout << "Drawing Circle" << endl;}double area()  {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {cout << "构造函数的实现" << endl;}void draw() {cout << "Drawing Rectangle" << endl;}double area()  {return width * height;}
};int main() {Shape* shapes[2];shapes[0] = new Circle(5.0);shapes[1] = new Rectangle(4.0, 6.0);for (int i = 0; i < 2; ++i) {shapes[i]->draw();cout << "Area: " << shapes[i]->area() << endl;delete shapes[i]; // 记得删除动态分配的对象}return 0;
}

运行结果:

(2)纯虚函数的使用

#include <iostream>
using namespace std;// 抽象基类
class Shape {
public:// 纯虚函数,必须在派生类中实现virtual void draw() const = 0;virtual double area() const = 0;virtual ~Shape() { cout << "基类析构函数的实现" << endl; } // 虚析构函数
};class Circle : public Shape {
private:double radius;
public:Circle(double r) : radius(r) { cout << "Circle构造函数的实现" << endl; }void draw() const override {cout << "Drawing Circle" << endl;}double area() const override {return 3.14159 * radius * radius;}
};class Rectangle : public Shape {
private:double width, height;
public:Rectangle(double w, double h) : width(w), height(h) {}void draw() const override {cout << "Drawing Rectangle" << endl;}double area() const override {return width * height;}
};int main() {Shape* shapes[2];shapes[0] = new Circle(5.0);shapes[1] = new Rectangle(4.0, 6.0);for (int i = 0; i < 2; ++i) {shapes[i]->draw();cout << "Area: " << shapes[i]->area() << endl;delete shapes[i]; // 记得删除动态分配的对象}return 0;
}

运行结果: 

区别

  1. 定义方式

    • 虚函数:virtual void show() { ... }
    • 纯虚函数:virtual void show() = 0;
  2. 实现

    • 虚函数:可以在基类中提供实现,也可以在派生类中重写。
    • 纯虚函数:不能在基类中提供实现,必须在派生类中实现。
  3. 实例化

    • 虚函数:基类可以实例化。
    • 纯虚函数:包含纯虚函数的类是抽象类,不能实例化。
  4. 用途

    • 虚函数:用于实现多态性。
    • 纯虚函数:用于定义接口,强制派生类实现特定的函数。
什么时候使用虚函数?
  • 多态行为:当你希望基类提供一个默认的行为,但允许派生类重写时,使用虚函数。
  • 可选重写:当派生类不一定需要提供自己的实现时,可以在基类中提供一个虚函数的默认实现。
什么时候使用纯虚函数?
  • 抽象接口:当你希望基类只定义接口,而不提供任何实现时,使用纯虚函数。
  • 强制实现:当你希望所有派生类都必须提供自己的实现时,使用纯虚函数。

2.6虚函数映射表

#include <iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base show" << endl;}virtual void display() {cout << "Base display" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived show" << endl;}void display() override {cout << "Derived display" << endl;}
};int main() {Base* b = new Derived();b->show(); // 输出:Derived showb->display(); // 输出:Derived displaydelete b;return 0;
}

Base* b = new Derived(); 

   当创建 Derived 对象时,编译器会为 Derived 类生成一个虚函数表,并在对象中存储一个指向该表的指针。虚函数表包含 Derived 类中虚函数 showdisplay 的地址。

多态调用
  • b->show()b->display() 是通过基类指针 b 调用的。
  • 程序会通过 b 的 V-Table Pointer 查找 Derived 类的虚函数表,然后从表中找到 showdisplay 函数的地址并调用它们。
注意事项
  • V-Table 的生成:虚函数表是在编译时生成的,每个类只有一张虚函数表。
  • 对象的 V-Table Pointer:每个对象包含一个指向相应虚函数表的指针(隐藏在对象中),该指针在对象构造时被初始化。
  • 内存开销:使用虚函数表会增加一些内存开销(存储虚函数表指针和虚函数表本身),并稍微增加函数调用的时间开销(间接调用)。

2.7为什么要使用虚析构函数

(1)通常情况的析构函数

#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}~Base() { // 虚析构函数cout << "Base destructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}
};int main() {Base* b = new Derived();delete b; // 正确调用派生类的析构函数return 0;
}

可以看到释放资源的时候,delete b只是调用了基类的析构函数,并没有对派生类进行内存释放。

(2)使用虚函数的析构函数

#include <iostream>
using namespace std;class Base {
public:Base() {cout << "Base constructor" << endl;}virtual ~Base() { // 虚析构函数cout << "Base destructor" << endl;}
};class Derived : public Base {
public:Derived() {cout << "Derived constructor" << endl;}~Derived() {cout << "Derived destructor" << endl;}
};int main() {Base* b = new Derived();delete b; // 正确调用派生类的析构函数return 0;
}

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

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

相关文章

Python学习笔记18:进阶篇(七)常见标准库使用之OS模块

前言 入门到进阶的知识点基本都学习了&#xff0c;可能有一些遗漏的请谅解&#xff0c;不过只要坚持学习下去&#xff0c;在后面的学习中进行查缺补漏。 根据Python crash course书中的进度&#xff0c;要准备开始写小项目了。在这之前&#xff0c;我看了Python的官方教程&am…

房产平台系统小程序源码

&#x1f3e0; 一键解锁购房新体验 &#x1f513; 房产小程序 实现前端 发布二手房 租房 商品 求租 求购等信息&#xff1b; 后台发布 新房&#xff0c;二手房租房 商品 写字楼 求租求购等房源信息&#xff1b; 功能完善的一块房产小程序 &#x1f31f; 房产平台小程序的魅力…

第二证券:港交所上市24周年 市值增长38倍

香港交易及结算所有限公司&#xff08;下称香港交易所&#xff09;于近来举办庆典活动&#xff0c;庆祝上市24周年。 据介绍&#xff0c;自2000年起&#xff0c;香港交易所逐步发展成为全球领先的商场营运机构&#xff0c;也成为连接中国内地与国际商场的主要桥梁。到2024年6月…

Python处理消息队列库之kombu使用详解

概要 在现代应用程序开发中,消息队列是实现异步任务处理和微服务通信的重要组件。Kombu 是一个用于在 Python 中处理消息的库,它提供了一个统一的接口来访问不同的消息队列后端,如 RabbitMQ、Redis 等。Kombu 设计简洁、功能强大,使得开发者可以轻松地在应用中集成消息队列…

FPGA - DFT(离散傅里叶变换)—FFT(快速傅里叶变化)

一&#xff0c;DFT(离散傅里叶变换原理) 1&#xff0c;DFT(离散傅里叶变换原理)理论简介 在数字信号处理中有一个基本概念&#xff1a; 如果信号在频域是离散的&#xff0c;则该信号在时域就表现为周期性的时间函数&#xff1b;相反&#xff0c;如果信号在时域是离散的&#x…

Linux系统安装Lua语言及Lua外部库

安装Lua Lua语言是一种轻量级、高效且可扩展的脚本语言&#xff0c;具有简洁易学的语法和占用资源少的特点。它支持动态类型&#xff0c;提供了丰富的表达式和运算符&#xff0c;同时具备自动垃圾回收机制和跨平台性。Lua语言易于嵌入到其他应用程序中&#xff0c;并可与其他语…

Spring Boot基础入门

引言 Spring Boot是一个开源的Java框架&#xff0c;旨在简化Spring应用程序的创建和部署过程。它提供了一种快速和简便的方式来创建独立的、生产级别的基于Spring的应用程序。本文将介绍Spring Boot的基础知识&#xff0c;包括其核心特性、如何开始使用Spring Boot以及构建你的…

golang 实现继承方式

经常使用java或c同学应该比较了解纯面向对象&#xff0c;继承、接口、封装等特性&#xff0c;在go中并没有特别显示的表达出来&#xff0c;但是go隐含是支持的&#xff0c;只是支持的方式不一致&#xff0c;可以说go的方式更加灵活&#xff0c;go语言精髓是组合机制&#xff0c…

淘宝扭蛋机小程序:现在是否是最佳开发时机?

随着科技的飞速发展和移动互联网的普及&#xff0c;小程序作为一种新兴的互联网应用形态&#xff0c;已经深入到人们的日常生活中。淘宝扭蛋机小程序&#xff0c;作为结合了娱乐与电商的创新模式&#xff0c;近年来备受关注。那么&#xff0c;现在是否是开发淘宝扭蛋机小程序的…

为什么 Swift 没有原生的 subArray 方法?

为什么 Swift 没有原生的 subArray 方法&#xff1f; Swift 是一门设计精良的编程语言&#xff0c;以其灵活性和高性能著称。在 Swift 中截取数组的子数组是一个常见的操作&#xff0c;但你可能会发现 Swift 标准库中并没有直接提供一个 subArray 方法。这是为什么呢&#xff…

洗地机哪个牌子最好用?测评员总结4款高配置洗地机推荐

洗地机作为现代家居清洁的得力助手&#xff0c;它不仅能让我们在日常清扫中保持高效率和高便捷性&#xff0c;特别适合快节奏生活的都市家庭。面对市场上众多的洗地机品牌与型号&#xff0c;消费者往往面临选择难题&#xff0c;对于洗地机的综合考量需要太多时间&#xff0c;而…

Spring Boot整合Druid:轻松实现SQL监控和数据库密码加密

文章目录 1 引言1.1 简介1.2 Druid的功能1.3 竞品对比 2 准备工作2.1 项目环境 3 集成Druid3.1 添加依赖3.2 配置Druid3.3 编写测试类测试3.4 访问控制台3.5 测试SQL监控3.6 数据库密码加密3.6.1 执行命令加密数据库密码3.6.2 配置参数3.6.3 测试 4 总结 1 引言 1.1 简介 Dru…

​Python20 Numpy基础

NumPy&#xff08;Numerical Python&#xff09;是一个开源的Python库&#xff0c;广泛用于科学计算。它提供了一个高性能的多维数组对象&#xff0c;以及用于处理这些数组的工具和函数。NumPy是数据分析、机器学习、工程和科学研究中不可或缺的工具之一&#xff0c;因为它提供…

使用Hugging Face获取BERT预训练模型

【图书推荐】《从零开始大模型开发与微调&#xff1a;基于PyTorch与ChatGLM》_《从零开始大模型开发与微调:基于pytorch与chatglm》-CSDN博客 BERT是一个预训练模型&#xff0c;其基本架构和存档都有相应的服务公司提供下载服务&#xff0c;而Hugging Face是一家目前专门免费提…

推荐一款好用的编辑工具——onlyoffice桌面编辑器8.1

读者大大们好呀&#xff01;&#xff01;!☀️☀️☀️ &#x1f525; 欢迎来到我的博客 &#x1f440;期待大大的关注哦❗️❗️❗️ &#x1f680;欢迎收看我的主页文章➡️寻至善的主页 文章目录 &#x1f525;前言&#x1f680;版本更新概览&#x1f697;文档编辑PDF编辑器…

搭建盲盒小程序的必要性

随着移动互联网的飞速发展&#xff0c;小程序作为一种新型的应用形态&#xff0c;已经深入人们的生活。而盲盒&#xff0c;作为一种独特的销售方式&#xff0c;因其神秘感和趣味性而备受年轻消费者的喜爱。当盲盒遇上小程序&#xff0c;便产生了一种全新的商业模式&#xff0c;…

Qt项目天气预报(5) - 根据JSON刷新天气信息+城市匹配

刷新当天天气 我们使用如下api接入&#xff0c;当然需要自己去 易客云天气API免费天气API接口|天气预报接口|全球天气API接口|气象预警|空气质量 (tianqiapi.com)注册后生成自己的对应id才可以使用 //专业天气v61 http://v1.yiketianqi.com/api?unescape1&versionv61&…

STM32学习-HAL库 串口通信

学完标准库之后&#xff0c;本来想学习freertos的&#xff0c;但是看了很多教程都是移植的HAL库程序&#xff0c;这里再学习一些HAL库的内容&#xff0c;有了基础这里直接学习主要的外设。 HAL库对于串口主要有两个结构体UART_InitTypeDef和UART_HandleTypeDef&#xff0c;前者…

白帽子的海外第一单,750刀

国际惯例&#xff0c;给兄弟们看图 这是我们师傅挖国外SRC的部分赏金截图 就问你&#xff01;挖国外漏洞赚美金香不香&#xff01; 现在国内SRC越来越卷了&#xff0c;越来越多的白帽子开始挖海外漏洞赚美金。海外SRC真的比国内赏金高很多&#xff0c;不说高危漏洞&#xff0…

MoneyPrinterPlus:AI自动短视频生成工具-腾讯云配置详解

MoneyPrinterPlus可以使用大模型自动生成短视频&#xff0c;其中的语音合成和语音识别部分需要借助于一些第三发云厂商的语音服务。 很多小伙伴可能不知道应该如何配置&#xff0c;这里给大家提供一个详细的腾讯云语音服务的配置教程。 项目已开源&#xff0c;代码地址&#…