[C++]多态

ops/2024/12/19 7:46:08/

1. 什么是多态性?

1.定义

多态性是指同一个函数或操作在不同对象上表现出不同的行为。

2.分类

C++ 中的多态性主要分为两种:

1.编译时多态性(静态多态性)

  • 编译时决定调用哪个函数。
  • 通过 函数重载运算符重载 实现。

2.运行时多态性(动态多态性)

  • 程序运行时根据对象的实际类型决定调用哪个函数。
  • 通过 虚函数 实现。

3.核心思想

多态的核心是 “一个接口,多种实现”

  • 基类定义一个公共接口。
  • 子类提供不同的实现。
  • 调用时,通过基类指针或引用操作不同的对象,表现出多种行为。

2. 多态的经典例子

例子 1:动物的叫声

一个基类 Animal,它有一个 makeSound() 方法:

  • 对于 Dog 类,makeSound() 表现为 “汪汪”。
  • 对于 Cat 类,makeSound() 表现为 “喵喵”。
实现多态的目标:

通过基类指针或引用,调用 makeSound(),实现动态决定调用哪种动物的声音。


3. 利用虚函数实现多态性

虚函数是实现运行时多态性的核心机制。核心点为

1.静态绑定与动态绑定:

1.普通函数是编译时绑定(静态绑定,又叫静态关联),即调用的函数在编译时确定。

在程序编译阶段,编译器就能确定要调用的函数具体是哪一个。例如,有下面这样简单的类和函数调用的代码:

#include<iostream>
using namespace std;
class Base {
public:void show() {cout << "Base::show()" << cout;}
};int main() {Base b;b.show();  // 这里编译器一看对象是Base类型,在编译的时候就确定调用Base类里定义的show函数return 0;
}

在上述代码中,当在 main 函数里调用 b.show() 时,编译器在编译阶段就能明确知道这里调用的就是 Base 类中定义的 show 函数,这种在编译时就确定具体调用哪个函数的方式就叫做静态绑定,也就是编译时绑定。 

2.虚函数是运行时绑定(动态绑定,又叫动态关联),即调用的函数在程序运行时根据实际对象的类型决定。

动态绑定是指在程序运行的时候,才根据实际对象的类型去决定调用哪个类的函数。这就需要用到虚函数了,看下面的代码示例:

#include<iostream>
using namespace std;class Base {
public:virtual void show() {cout << "Base::show()" << endl;}
};class Derived : public Base {
public:void show() override {cout << "Derived::show()" << endl;}
};int main() {Base* ptr = new Derived();  // 基类指针指向子类对象ptr->show();  // 这里在运行时,会根据指针所指向的实际对象(是Derived类的对象)来决定调用Derived类里重写的show函数,而不是Base类的show函数,这就是运行时绑定delete ptr;return 0;
}

 在这个例子里,虽然 ptr 是 Base 类型的指针,但是在运行时调用 show 函数时,会根据它实际指向的对象(这里是 Derived 类的对象)去决定调用 Derived 类中重写后的 show 函数,这种在运行时才能确定具体调用哪个函数的机制就是动态绑定,也就是运行时绑定。

2.通过基类指针或引用调用子类的实现:

1.使用基类指针或引用操作子类对象。

  • 基类指针:指针是一个变量,它存储的是另一个变量的内存地址。基类指针就是声明为指向基类类型的指针变量。例如:
    #include<iostream>
    using namespace std;
    class Animal {// 类的定义内容
    };class Dog : public Animal {// 类的定义内容
    };int main() {Animal* animalPtr;  // 这就是一个基类指针,它可以用来指向Animal类的对象,当然也可以指向Animal类的派生类(子类)的对象,像下面这样Dog dogObj;animalPtr = &dogObj;  // 让基类指针指向Dog类(子类)的对象,这里是合法的,因为Dog类是Animal类的派生类return 0;
    }
  •  引用操作子类对象:
    #include<iostream>
    using namespace std;
    class Shape {
    public:virtual void draw() {cout << "Drawing a generic shape" << endl;}
    };class Circle : public Shape {
    public:void draw() override {cout << "Drawing a circle" << endl;}
    };class Rectangle : public Shape {
    public:void draw() override {cout << "Drawing a rectangle" << endl;}
    };int main() {Shape* shapePtr1 = new Circle();  // 基类指针指向Circle子类对象Shape* shapePtr2 = new Rectangle();  // 基类指针指向Rectangle子类对象shapePtr1->draw();  // 运行时调用Circle类重写的draw函数,体现多态shapePtr2->draw();  // 运行时调用Rectangle类重写的draw函数,体现多态delete shapePtr1;delete shapePtr2;return 0;
    }

    在这个例子中,Shape 是基类,Circle 和 Rectangle 是它的子类。在 main 函数里定义了基类指针 shapePtr1 和 shapePtr2,并分别让它们指向不同的子类对象。当通过这些基类指针调用 draw 这个虚函数时,在运行时会根据指针实际指向的子类对象来决定调用子类中重写后的 draw 函数,这就是通过基类指针操作子类对象实现多态性。

2.如果函数是虚函数,则调用子类的版本;如果不是虚函数,则调用基类的版本。

3.代码示例:利用虚函数实现多态

#include <iostream>
using namespace std;class Animal {
public:virtual void makeSound() {  // 虚函数cout << "Animal makes a sound." << endl;}
};class Dog : public Animal {
public:void makeSound() override {  // 子类重写虚函数cout << "Dog barks: Woof!" << endl;}
};class Cat : public Animal {
public:void makeSound() override {  // 子类重写虚函数cout << "Cat meows: Meow!" << endl;}
};int main() {Animal* animal;  // 基类指针Dog dog;Cat cat;animal = &dog;animal->makeSound();  // 调用 Dog 的 makeSound()animal = &cat;animal->makeSound();  // 调用 Cat 的 makeSound()return 0;
}//输出:
//Dog barks: Woof!
//Cat meows: Meow!
解释:
  • 虽然 animalAnimal* 类型,但因为 makeSound() 是虚函数,运行时根据对象的实际类型(DogCat)调用对应的版本。

4. 纯虚函数与抽象类

关于虚函数与纯虚函数和抽象函数的详细知识点讲解大家可以看这里

纯虚函数和抽象类是实现动态多态性的高级工具,用于设计 接口类

1.纯虚函数

1.定义:

  • 是没有实现的虚函数,用 = 0 声明。
  • 基类只规定接口(函数名、参数、返回值类型),具体实现由子类负责。
  • 纯虚函数的基类是抽象类。

2.语法:

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

2.抽象类

1.定义

  • 包含至少一个纯虚函数的类称为抽象类。
  • 不能实例化抽象类
  • 用于定义接口,让子类实现具体行为。

2.代码示例:纯虚函数与抽象类

#include <iostream>
using namespace std;// 抽象类:定义接口
class Shape {
public:virtual void draw() = 0;  // 纯虚函数,子类必须实现
};class Circle : public Shape {
public:void draw() override {  // 实现接口cout << "Drawing a Circle" << endl;}
};class Rectangle : public Shape {
public:void draw() override {  // 实现接口cout << "Drawing a Rectangle" << endl;}
};int main() {Shape* shape;  // 基类指针Circle circle;Rectangle rectangle;shape = &circle;shape->draw();  // 调用 Circle 的 draw()shape = &rectangle;shape->draw();  // 调用 Rectangle 的 draw()return 0;
}
/*
输出:
Drawing a Circle
Drawing a Rectangle
*/
解释:
  1. Shape 是抽象类,定义了 draw() 接口。
  2. CircleRectangle 实现了 draw()
  3. 使用基类指针 shape 调用子类的实现。

5. 静态多态性 vs 动态多态性

特性静态多态性动态多态性
绑定时间编译时绑定运行时绑定
实现机制通过函数重载、运算符重载实现通过虚函数实现
调用方式调用函数时直接确定根据对象的实际类型决定调用的函数
性能开销无运行时开销有运行时开销(需要通过虚函数表查找函数指针)

6. 多态的核心机制:虚函数表

1.对虚函数表的浅层理解:

虚函数表是一个存储虚函数地址的表格。当一个类包含虚函数时,编译器会为这个类创建一个虚函数表。这个表就像是一个函数指针数组,数组中的每个元素都是一个虚函数的地址。例如,假设有一个简单的类层次结构:

class Base {
public:virtual void func1() {}virtual void func2() {}
};

对于'Base'类,编译器会创建一个虚函数表。这个表中存储了'func1'和'func2'这两个虚函数的地址。如果有一个派生类'Derived'继承自'Base'并且重写了这些虚函数,那么'Derived'类的虚函数表中的相应函数指针会指向'Derived'类中重写后的函数。 假设'Base'类的虚函数表在内存中的地址是'0x1000','func1'的地址在虚函数表中的偏移量是0,'func2'的地址偏移量是1(这只是一个简单的假设,实际的偏移量和内存布局由编译器决定)。在内存中,可能会像这样存储(以简单的十六进制表示): 0x1000: [地址 of Base::func1, 地址 of Base::func2]。

2.虚函数通过虚函数表实现:

1.每个包含虚函数的类有一个虚函数表,记录了类中虚函数的地址。

2.每个对象有一个指针(虚指针,vptr),指向所属类的虚函数表。

3.调用虚函数时,程序根据对象的虚指针查找虚函数表,从而找到正确的函数地址。


7. 多态的优势与应用场景

1.优势

1.代码复用: 基类定义接口,子类实现,减少重复代码。

2.扩展性强: 新增子类时,不需要修改基类代码,遵循开放/封闭原则。

3.运行时灵活性: 通过基类指针操作不同类型的对象,行为不同。

2.应用场景

  • 图形库设计: 抽象类 Shape 定义 draw() 接口,具体形状如 CircleRectangle 实现它。
  • 文件处理: 基类 File 定义 open()read(),子类实现具体操作(如文本文件、二进制文件)。
  • 游戏开发: 基类 Character 定义 attack(),具体角色(战士、法师)实现不同攻击方式。

8. 总结

1.多态性

  • 是面向对象编程的核心概念,实现“一个接口,多种实现”。
  • 分为 静态多态性(编译时)和 动态多态性(运行时)。

2.动态多态性

1.通过虚函数实现:

  • 基类定义虚函数,子类重写。
  • 使用基类指针或引用调用子类的实现。

2.通过纯虚函数与抽象类:

  • 纯虚函数强制子类实现,抽象类用于设计接口。

http://www.ppmy.cn/ops/143128.html

相关文章

Linux下常用的网络编程函数详解

在网络编程中&#xff0c;我们经常需要处理 IP 地址和端口号等数据&#xff0c;这些数据需要在主机字节序&#xff08;Host Byte Order&#xff09;与网络字节序&#xff08;Network Byte Order&#xff09;之间进行转换。 什么是字节序&#xff1f; 字节序指的是多字节数据在…

fabric.js

目录 一、在canvas上画简单的图形 二、在canvas上用路径(Path)画不规则图形 三、在canvas上插入图片并设置旋转属性(angle) 四、让元素动起来(animate) 五、图像过滤器(filters)让图片多姿多彩 六、颜色模式(Color)和相互转换(toRgb、toHex) 七、对图形的渐变填充(Gradi…

常用的es操作

前言 我们前面写过ES基础操作和ES高级查询 写的都很细&#xff0c;但是很多时候我们仅仅是忘记具体的某个语法&#xff0c;去那两篇博客查找就很麻烦了&#xff0c;这篇博客就把常用的ES操作进行总结。 常用操作 建索引&#xff08;建表&#xff0c;不过并没有指定字段名和类…

【Qt】信号、槽

目录 一、信号和槽的基本概念 二、connect函数&#xff1a;关联信号和槽 例子&#xff1a; 三、自定义信号和槽 1.自定义槽函数 2.自定义信号函数 例子&#xff1a; 四、带参的信号和槽 例子&#xff1a; 五、Q_OBJECT宏 六、断开信号和槽的连接 例子&#xff1a; …

Java匿名类和lambda

匿名类 匿名类就是一个语法糖&#xff0c;可以帮我们省略定义类名这一步&#xff0c;直接创建对象&#xff0c;简化代码 匿名类规则&#xff1a; 匿名类必须继承一个父类或实现一个接口&#xff0c;并且最多继承一个父类或实现一个接口。匿名类可以继承父类的方法&#xff0c…

基于python绘制数据表(上)

利用python绘制各种数据图表 绘制柱形图-源码 from openpyxl import Workbook from openpyxl.chart import BarChart, Reference# 创建工作薄 wb Workbook(write_onlyTrue) # 创建工作表 ws wb.create_sheet(月收入)# 准备数据 rows [(月份, 销售额),(1, 23),(2, 43),(3, …

第十章 多表查询

一、概述 我们在前面几个章节讲解SQL语句的时候&#xff0c;涉及到了DQL语句即数据查询语句&#xff0c;但是之前讲解的查询都是单表查询&#xff0c;而本章节我们要学习的则是多表查询操作。多表查询‌是指在数据库查询中涉及多个表的操作&#xff0c;通过连接运算来获取多个…

(5)YOLOv3和yolov1、yolov2之间的差异

一、YOLOv3的技术原理和改进 ​ YOLOv3算法是在YOLOv1和YOLOv2的基础上进一步改进和发展的&#xff0c;它在目标检测领域取得了显著的性能提升。YOLOv3成为了一个强大的目标检测工具&#xff0c;广泛应用于各种计算机视觉任务中&#xff0c;包括但不限于图像中的目标检测、视频…