当前位置: 首页 > web >正文

09_多态

多态

封装,继承,多态是c++学习中重要的三个知识,而我们之前已经学习过了封装和继承,现在我们来了解一下多态

多态的基本概念

多态可以分为编译时的多态和运行时的多态。

前者主要指 函数的重载(包括运算符的重载),对重载函数的调用在编译时就能根据实参确定应该调用哪个函数,这就是编译时的多态

而运行时的多态则与继承,虚函数等概念有关

举个生动的栗子

想象一下你家里有个“宠物”这个大类(基类),然后你有具体的“狗”和“猫”这两个小类(派生类)

  1. 共同的行为: 你知道所有宠物都会“叫”(MakeSound()方法)。作为主人,你只需要知道“叫”这个指令,不用太操心具体怎么叫。

  2. 不同的实现:

    • 你喊“狗,叫!”,它就会“汪汪汪”。

    • 你喊“猫,叫!”,它就会“喵喵喵”。

  3. 关键点 - 通过指针/引用: 多态最神奇的地方在于,你不需要确切知道手里牵着的是狗还是猫(具体类型)。你只需要用一个宠物类的指针或者引用来指着它。

    • 比如:Pet* myPet = new Dog(); (现在myPet指向一只狗)

    • 或者:Pet* myPet = new Cat(); (现在myPet指向一只猫)

  4. 魔法发生 - 虚函数: 当你想让宠物叫时,你调用myPet->MakeSound()

    • 如果MakeSound()在基类Pet中被声明为virtual(虚函数),那么:

      • myPet实际指向狗时,myPet->MakeSound()执行的是Dog类里定义的“汪汪汪”。

      • myPet实际指向猫时,myPet->MakeSound()执行的是Cat类里定义的“喵喵喵”。

    • 程序在运行时(Runtime) 才根据myPet实际指向的对象类型来决定调用哪个版本的MakeSound()。这就是“动态绑定”或“后期绑定”。

多态的使用

我们从上面的例子可以提炼出构成多态的条件

1.必须存在继承关系

2.继承关系中必须有同名的虚函数,并且它们是覆盖关系。

3.存在基类的指针,通过该指针调用虚函数。

好的我们现在就以这三个点,写一个人与学生的例子

#include <iostream>
using namespace std;class Person {protected:string name;int age;public:Person(){};Person(string name, int age):name(name),age(age){}virtual void info() {//注意此处虚函数 父类加了子类可以不加 但反过来不行cout << "person info: " << this->name<<" "<<this->age << endl;}
};
class Student : public Person {protected:double score;
public:Student(){};Student(string name, int age, double score):Person(name,age),score(score){}void info() {cout << "student info: " << this->name<<" "<<this->age<<" "<<this->score<<endl;}};int main() {Person *person= new Person("John", 23);person->info();person=new Student("wangwu",22,12.2);person->info();return 0;
}

结果

在这里插入图片描述

引用实现多态

在c++中,多态的实现,除了可以使用指针指向父类的对象之外,还可以是通过引用来实现多态,但是引用不像指针灵活,指针可以随时改变指向而引用只能指向固定的对象。

还是用我们刚才人和学生的例子

如果用引用的方式实现的话就是

Person p1("xiaoming",100);
Student s1("xiaozhang",120,90);
Person&rperson=p1;
Person&rstudent=s1;
rperson.info();
rstudent.info();

其实引用类似于常量,只能在定义的同时初始化,并且以后也要从一而终,不能再引用其他数据。所以在本例中我们要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。

结果和之前的一样

在这里插入图片描述

虚函数

在c++中,使用virtual关键词修饰的函数被称为虚函数虚函数对于多态有决定性作用,有虚函数才有多态。

那么我们什么时候用虚函数呢?

首先看成员函数所在的类是否作为基类。

然后看成员函数在类的继承后有无可能更改。

如果希望更改功能,将它声明为虚函数。如果成员函数在类被继承后功能不需要修改或者用不到,就不需要把它声明为虚函数了。

通俗一点说

核心思想:晚绑定

想象一下你要去一个地方:

  1. 早绑定: 就像提前买好了固定目的地的火车票。无论你实际上想去哪里,你只能坐这趟火车去那个固定的地方。编译器在编译时就根据指针/引用的声明类型(比如Pet*)决定了调用哪个函数。

  2. 晚绑定: 就像你有一张神奇的“任意门”车票(虚函数)。你告诉司机“开车!”(调用函数),司机(程序运行时)会根据你此时此刻真正要去的目的地(指针/引用实际指向的对象类型,比如DogCat)来决定走哪条路、开往哪里。这个决定发生在程序运行时

我们来做一个案例

定义一个Animal类,在定义一个虚函数eat,在定义一个Dog类和一个Cat类继承Animal,通过多态调用不同的eat函数。

代码如下

class Animal {
public:virtual void eat() {cout << "Animal eat" << endl;}
};
class Dog : public Animal {
public:virtual void eat() {cout << "Dog eat" << endl;}
};
class Cat : public Animal {virtual void eat() {cout << "Cat eat" << endl;}
};
int main() {Animal *a =new Animal;a->eat();a=new Dog;a->eat();a=new Cat;a->eat();return 0;
}

虚析构

在c++中构造函数不可以被声明为虚构函数,但析构函数可以被声明为虚构函数这种函数被称为虚析构。

当父类的析构函数不声明成虚析构函数的时候,当子类继承父类,父类的指针指向子类时,delete掉父类的指针,只调用父类的析构函数不调用子类的析构函数。

当父类的析构函数声明成虚析构函数时,当子类继承父类,父类的指针指向子类,delete掉父类的指针,先调用子类的析构函数再调用父类的析构函数。

我们用一个案例来看看.

class Person {protected:string name;int age;public:Person(){};Person(string name, int age):name(name),age(age){}~Person() {cout<<"person"<<endl;};virtual void info() {cout << "person info: " << this->name<<" "<<this->age << endl;}
};
class Student : public Person {protected:double score;
public:Student(){};Student(string name, int age, double score):Person(name,age),score(score){}~Student() {cout<<"student"<<endl;}void info() {cout << "student info: " << this->name<<" "<<this->age<<" "<<this->score<<endl;}};int main() {Person *p1= new Student("John", 23,90);p1->info();delete p1;return 0;
}

这是没有用虚继承的

在这里插入图片描述

它只用了父类的析构函数

那如果用虚析构函数呢?

class Person {protected:string name;int age;public:Person(){};Person(string name, int age):name(name),age(age){}virtual~Person() {cout<<"person"<<endl;};//此处用了虚析构函数virtual void info() {cout << "person info: " << this->name<<" "<<this->age << endl;}
};
class Student : public Person {protected:double score;
public:Student(){};Student(string name, int age, double score):Person(name,age),score(score){}~Student() {cout<<"student"<<endl;}void info() {cout << "student info: " << this->name<<" "<<this->age<<" "<<this->score<<endl;}};int main() {Person *p1= new Student("John", 23,90);p1->info();delete p1;return 0;
}

结果

在这里插入图片描述

虚函数表

在c++中,多态是由虚函数来实现的,而虚函数是由虚函数表来实现的,对象不包含虚函数表,只有对象包含虚函数表,派生类会生成一个兼容基类的虚函数表。

如果一个类中包含虚函数,那么这个类就会包含一张虚函数表虚函数表存储的每一项是一个虚函数的地址。

举个例子

class A{public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1,m_data2;
}

这个类上的虚函数就会形成一个虚函数表

在这里插入图片描述

虚表是一个指针数组,元素使虚函数的指针,每个元素对应一个虚函数的函数指针。

只有虚函数才会用到这张表,其他的非虚函数并不会经过这张虚表。

虚函数指针的赋值发生在编译器的编译阶段,可以说在代码的编译阶段虚标就可以构造出来了。

虚表是属于类的,而不是属于某个具体的对象的,一个类只需要一个虚表。

那么我们再来想一想c++是如何实现动态绑定的,也就是我们所说的多态的。

c++如何实现动态绑定

当编译器发现类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr)这个指针是指向对象的虚函数表在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。

说这么多

其实这只是原理,我们实际应用是如果当子类无重写基类虚函数时

那如果子类调用的话还是基函数的虚函数

class Animal {
public:virtual void func1() {cout << "Animal::func1()" << endl;}virtual void func2() {cout << "Animal::func2()" << endl;}
};
class Dog : public Animal {
public:virtual void func3() {cout << "Dog::func3()" << endl;}virtual void func4() {cout << "Dog::func4()" << endl;}
};
int main() {Animal *a = new Dog;a->func1();return 0;
}

在这里插入图片描述

但如果重写了

class Animal {
public:virtual void func1() {cout << "Animal::func1()" << endl;}virtual void func2() {cout << "Animal::func2()" << endl;}
};
class Dog : public Animal {
public:virtual void func1() {cout << "Dog::func3()" << endl;}virtual void func2() {cout << "Dog::func4()" << endl;}
};
int main() {Animal *a = new Dog;a->func1();return 0;
}

就会被覆盖

在这里插入图片描述
)

抽象基类和纯虚函数

纯虚函数

  • 是什么:在基类中声明但没有实现的"特殊虚函数"

  • 作用:强制要求派生类必须自己实现这个函数

  • 语法:在函数声明末尾加 = 0

virtual void draw()=0//纯虚数

抽象基类

  • 是什么:包含至少一个纯虚函数的类

  • 关键特性

    • 不能直接创建对象(不能实例化)

    • 专门用来被其他类继承

    • 定义了一组必须实现的接口规范

举个栗子

抽象基类是泡饮品的抽象操作

子类 泡咖啡是泡饮品的具体操作

子类 泡茶是泡饮品的具体操作

泡茶和泡咖啡通过继承泡饮品的共同操作,完成实现

我们把泡咖啡的步骤计为1 2 3 4

泡茶的步骤计为 -1 -2 -3 -4

如下

//抽象基类,制作饮品
class AbstractDrinking {
public://烧水virtual void Boil()=0;//泡virtual void Brow()=0;//倒入杯子virtual void PourlnCup()=0;//辅料virtual void PutSomething()=0;//规定流程void MakeDrink() {Boil();Brow();PourlnCup();PutSomething();}
};
class Coffee : public AbstractDrinking {
public:virtual void Boil() {cout << "1" << endl;}virtual void Brow() {cout << "2" << endl;}virtual void PourlnCup() {cout<<"3"<<endl;}virtual void PutSomething() {cout<<"4"<<endl;}
};
class Tea : public AbstractDrinking {
public:virtual void Boil() {cout << "-1" << endl;}virtual void Brow() {cout << "-2" << endl;}virtual void PourlnCup() {cout<<"-3"<<endl;}virtual void PutSomething() {cout<<"-4"<<endl;}
};//业务函数
void DoBussiness(AbstractDrinking*drink) {drink->MakeDrink();delete drink;
}
int main() {DoBussiness(new Coffee());cout<<endl;DoBussiness(new Tea());return 0;cout<<endl;DoBussiness(new Tea());return 0;
}

结果

在这里插入图片描述

好的我们这篇博客到这里就结束了喜欢记得点点赞哦(๑′ᴗ‵๑)I Lᵒᵛᵉᵧₒᵤ❤

http://www.xdnf.cn/news/20512.html

相关文章:

  • `IntersectionObserver`延迟加载不在首屏的自动播放视频/图片/埋点/
  • Boost电路:稳态和小信号分析
  • Linux匿名管道和命名管道以及共享内存
  • C++并发编程指南 递归锁 介绍
  • Kimi K2-0905 256K 上下文 API 状态管理优化教程
  • 2.虚拟内存:分页、分段、页面置换算法
  • 分享一个基于Python+大数据的房地产一手房成交数据关联分析与可视化系统,基于机器学习的深圳房产价格走势分析与预测系统
  • Embedding上限在哪里?- On the Theoretical Limitations of Embedding-Based Retrieval
  • AI产品经理面试宝典第86天:提示词设计核心原则与面试应答策略
  • 《sklearn机器学习——聚类性能指标》Calinski-Harabaz 指数
  • Wisdom SSH 是一款搭载强大 AI 助手的工具,能显著简化服务器配置管理流程。
  • SSH服务远程安全登录
  • Linux系统shell脚本(四)
  • CodeSandbox Desktop:零配置项目启动工具,实现项目环境隔离与Github无缝同步
  • AI大模型应用研发工程师面试知识准备目录
  • 苍穹外卖优化-续
  • Java包装类型
  • Git 长命令变短:一键设置别名
  • Linux以太网模块
  • 【嵌入式】【科普】AUTOSAR学习路径
  • 《无畏契约》游戏报错“缺少DirectX”?5种解决方案(附DirectX修复工具)
  • 基于单片机智能行李箱设计
  • 云手机运行流畅,秒开不卡顿
  • 无拥塞网络的辩证
  • 24.线程概念和控制(一)
  • 贪心算法应用:数字孪生同步问题详解
  • B.50.10.10-微服务与电商应用
  • 关于退耦电容
  • 【LeetCode热题100道笔记】将有序数组转换为二叉搜索树
  • 3分钟快速入门WebSocket