【C++】——多态

embedded/2024/9/20 13:46:29/

文章目录

  • 多态的概念
  • 多态的定义和实现
  • 虚函数
    • 虚函数的重写(覆盖)
      • 虚函数重写的例外
  • override 和 final关键字
  • 重载、重写和重定义(隐藏)
  • 纯虚函数和抽象类
  • 多态的原理
  • 动态绑定和静态绑定

多态的概念

多态就是多种形态,在执行某个行为时,当不同对象去完成时,执行结果不同。
就好比买火车票:普通人买全价票、学生买学生票、儿童买半价票。虽然大家都是人,但是在买票时结果却不同。这就是典型的多态。

多态的定义和实现

  1. 必须通过基类的指针或者引用才能调用虚函数,这是因为只有基类的指针或引用能够同时指向基类和派生类的对象,从而实现多态的效果。
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。

举例:

#define   _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;
class Person
{
public:virtual void BuyTicket() // 虚函数{cout << "全价票" << endl;}
};class Child : public Person 
{
public:virtual void BuyTicket() // 虚函数重写{cout << "半价票" << endl;}
};void Fun(Person& p) // 基类的引用
{p.BuyTicket();
}
int main()
{Person p1;Child c1;Fun(p1);Fun(c1);return 0;
}

在这里插入图片描述

虚函数

被virtual修饰的类成员函数才是虚函数。非成员函数不能被virtual修饰。

class Person
{
public:virtual void BuyTicket(){cout << "全价票" << endl;}
};

虚函数的重写(覆盖)

派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

class Person
{
public:virtual void BuyTicket()// 虚函数{cout << "全价票" << endl;}
};class Child : public Person
{
public:virtual void BuyTicket()// 虚函数重写{cout << "半价票" << endl;}
};

举例:

// 派生类 Dog
class Dog : public Animal {
public:void talk() const override {cout << "汪" << endl;}
};// 派生类 Cat
class Cat : public Animal {
public:void talk() const override {cout << "喵" << endl;}
};// 测试函数,用于展示多态
void test_animal_talk(const Animal& animal) {animal.talk();
}int main() {Dog d1;Cat c1;// 使用基类引用调用多态函数test_animal_talk(d1);  // 输出 "汪"test_animal_talk(c1);  // 输出 "喵"return 0;
}

来一道虚函数的题:


// 基类 A
class A {
public:virtual void func(int val = 1) {cout << "A->" << val << endl;}virtual void test() {func();}
};// 派生类 B
class B : public A {
public:void func(int val = 0)  {cout << "B->" << val << endl;}
};int main() {B* p = new B();p->test();delete p; // 释放动态分配的内存return 0;
}

A: A->0 | B: B->1 | C: A->1 | D: B->0 | E: 编译出错 | F: 以上都不正确
我第一次做选的D,后来分析发现了错误:
派生类B并没有对test进行虚函数重写,所以当我使用派生类对象调用test时,如果在派生类里没有,他会去基类里寻找,因为没有进行虚函数重写,所以答案是B。

虚函数重写的例外

  1. 协变
    重写(Override)虚函数时,如果基类函数返回一个指向基类对象的指针或引用,派生类中的重写函数可以返回一个指向派生类对象的指针或引用。这被称为返回类型的协变。(这里的基类对象可以是可以来自自身的继承体系,也可以来源于其他继承体系。)
class A {};
class B :public A{};//不同的继承
class Person 
{
public:virtual A* f() { return new A; }
};
class Student : public Person 
{
public:virtual B* f() //协变{ return new B; }
};// 同一继承
class Person 
{
public:virtual Person* f(){return new Person;}
};
class Student : public Person
{
public:virtual Student* f() //协变{return new Student;}
};
  1. 析构函数的重写
    为什么要重写虚析构函数呢?
    当你有一个基类,并且从这个基类派生出多个子类时,如果基类有一个析构函数(无论是否为虚),而你想通过基类的指针来删除派生类的对象,那么你应该将基类的析构函数声明为虚的。这样做是为了确保当通过基类指针删除派生类对象时,能够调用到派生类的析构函数,从而正确释放派生类特有的资源。

我们在前面学习继承时就知道编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,所以只要基类的析构函数加了virtual关键字,它就一定会形成重写。

class Person 
{
public://~Person()virtual ~Person() {cout << "~Person()" << endl; }
};
class Student : public Person 
{
public:virtual ~Student() //构成重写{cout << "~Student()" << endl;}
};
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

在这里插入图片描述

override 和 final关键字

override
override 关键字用于在派生类中明确标记一个成员函数是要重写(Override)基类中的虚函数。
好处:

  1. 如果没有构成虚函数的重写在编译时就会报错,能够及时发现错误。
  2. 增强代码可读性,可以让阅读的人清楚的知道该函数是重写了某个基类的函数。
class Person
{
public:virtual	void func() {// ...}
};class Student : public Person
{
public:virtual	void func() override {// ...}
};

final

final 关键字用于防止类被继承或防止类中的虚函数被进一步重写。
好处:

  1. 防止虚函数被重写,确保某些成员函数不会因为继承而意外被改写
  2. 如果一个类被声明为final,它就不能被继承,有助于保护类不被扩展
class Person
{
public:virtual	void func() final{// ...}
};class Student : public Person
{
public:virtual	void func() {// ...}
};

在这里插入图片描述

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

在这里插入图片描述

纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。
如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

class Person
{
public:virtual	void func() =0{// ...}
};class Student : public Person
{
public:virtual	void func() { // 必须重写,不然也是抽象类,无法实例化// ...}
};

多态的原理

虚函数表
下图中_vfptr就是虚函数表指针,一个包含虚函数的类中最少都有一个虚函数表指针。
因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
在这里插入图片描述

class Person
{
public:virtual void BuyTicket() // 虚函数{cout << "全价票" << endl;}
};class Child : public Person
{
public:virtual void BuyTicket() // 虚函数重写{cout << "半价票" << endl;}
};void fun(Person& p)
{p.BuyTicket();
}
int main()
{Person p1;Child c1;fun(p1);fun(c1);return 0;
}

虚函数表的原理

通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。在这里插入图片描述

动态绑定和静态绑定

静态绑定
重载就是典型的静态绑定(前期绑定)
对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤
函数的地址,叫做静态绑定。
动态绑定
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数
的地址,也就做动态绑定。


http://www.ppmy.cn/embedded/114184.html

相关文章

UWA支持鸿蒙HarmonyOS NEXT

华为在开发者大会上&#xff0c;宣布了鸿蒙HarmonyOS NEXT将仅支持鸿蒙内核和鸿蒙系统的应用&#xff0c;不再兼容安卓应用&#xff0c;这意味着它将构建一个全新且完全独立的生态系统。 为此&#xff0c;UWA也将在最新版的UWA SDK v2.5.0中支持鸿蒙HarmonyOS NEXT&#xff0c…

【2025】基于微信小程序的人工智能课程学习平台的设计与实现(源码+文档+解答)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

GCP容器镜像仓库使用

GCP容器镜像仓库产品为&#xff1a;Artifact Registry。 1&#xff09;用户账号认证 GCP需要前置在控制台登陆对应环境账号。然后执行以下命令操作&#xff1a; $ gcloud auth login 2&#xff09;登陆镜像仓库 $ gcloud auth configure-docker us-west1-docker.pkg.dev …

C++: set与map容器的介绍与使用

本文索引 前言1. 二叉搜索树1.1 概念1.2 二叉搜索树操作1.2.1 查找与插入1.2.2 删除1.2.3 二叉搜索树实现代码 2. 树形结构的关联式容器2.1 set的介绍与使用2.1.1 set的构造函数2.1.2 set的迭代器2.1.3 set的容量2.1.4 set的修改操作 2.2 map的介绍与使用2.2.1 map的构造函数2.…

PHP智能化云端培训考试系统小程序源码

智能化云端培训考试系统&#xff1a;重塑学习评估的未来 &#x1f31f; 引言&#xff1a;迈向智能教育的新时代 在这个日新月异的数字时代&#xff0c;教育也在经历着前所未有的变革。智能化云端培训考试系统的出现&#xff0c;正是这一变革的生动体现。它不仅打破了传统教育的…

C++异常

1.C语言传统的处理错误的方式 传统的错误处理机制&#xff1a; 1. 终止程序&#xff0c;如assert&#xff0c;缺陷&#xff1a;用户难以接受。如发生内存错误&#xff0c;除0错误时就会终止程序。 2. 返回错误码&#xff0c;缺陷&#xff1a;需要程序员自己去查找对应的错误…

linux在工作中常用命令

简介 记录在日常工作中&#xff0c;常用linux命令 查日志篇 1.统计特定内容出现的行数和次数 1.查询特定内容出现的行数 grep -c "关键词" 文件名-c:只显示匹配的行数。 2.查询特定内容出现的次数 grep -o "关键词" 文件名 | wc -l-o&#xff1a;只显示…

spring MVC 拦截器

一、Spring MVC拦截器的作用 拦截器是Spring MVC框架中处理 HTTP请求 的一种机制&#xff0c;通常用于在请求到达 控制器&#xff08;Controller&#xff09; 之前或从控制器返回结果之后进行额外的逻辑处理。可以用于以下场景&#xff1a; 日志记录&#xff1a;记录每次请求…