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

C++(进阶) 第2章 多态

C++(进阶) 第2章 多态


文章目录

  • 前言
  • 一、多态的概念
  • 二、多态的定义及实现
    • 1.虚函数
    • 2.虚函数的重写
    • 3.多态的条件
    • 4.多态的细节
  • 三、析构函数的重写
  • 四、重载/重写/隐藏的对比
  • 五、抽象类
    • 抽象类
  • 六、相关题目
      • 题目1
      • 题目2
  • 七、const修饰
  • 八、多态原理
  • 九、虚函数放在地方
  • 总结


前言

什么是多态?多态其实就是多种形态的简写这篇博客会详细的介绍多态


一、多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
通俗点来说就是不同的东西做不同的事情有多种形态


二、多态的定义及实现

1.虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
这是构成多态的必备条件之一

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

2.虚函数的重写

这里学生类继承了person类那么这里按之前的理解应该构成隐藏关系但是这里不一样,这里我们一般叫做重写

class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};

假如我现在要去买票

class Person{public:virtual void BuyTicket() { cout << "买票-全价" << endl; }};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
}

注意看这里传入的是父类

在这里插入图片描述

这里可能会有疑问为什么func函数参数写的父类但是这里传入子类也可以这是因为切片
在这里插入图片描述
这里可以看到传入不同的类型处理的方式也不一样这里就是一个简单的多态


3.多态的条件

  1. 虚函数的重写
  2. 必须是父类的指针或者是引用
  3. 上面俩个条件缺一不可

4.多态的细节

假如这里把父类的virtual去掉这里是多态吗?

class Person{public:void BuyTicket() { cout << "买票-全价" << endl; }};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Student s;func(p);func(s);return 0;
}

这里的答案是:不是多态
在这里插入图片描述


假如这里子类去掉virtual是多态吗?

在这里插入图片描述


答案是:是多态
在这里插入图片描述


面试题就很喜欢这样考,但是这里是为什么父类去就不行子类就行呢?

上面那样构成虚函数的重写就算不写virtual,因为继承了父类子类哪里会把父类的声明拿下来也是属于多态


多态调用:看指向对象类型,指向谁调用谁的虚函数
普通调用:看调用者的类型,调用调用者的函数

三、析构函数的重写

假如这里有1父1子

class person
{
public:~person(){cout << "~person()" << endl;}};class student:public person
{
public:~student(){cout << "~student()" << endl;}
};int main()
{person* p1 = new student;delete p1;
}

在这里插入图片描述
这里定义了一个指针指向了student这里
在这里插入图片描述
但是这里调用的是父类的析构函数

上面这里会出现非常大的问题上面的代码是为了方便理解

假如我把代码改成这样

class person
{
public:~person(){cout << "~person()" << endl;}};class student:public person
{
public:~student(){delete[] _ptr;cout << "~student()" << endl;}
protected:int* _ptr = new int[10];
};int main()
{person* p1 = new student;delete p1;
}

在这里插入图片描述
可以看到这里就出现了很严重的内存泄露


按正常关系来说studen继承了person,这studen就应该有俩个析构函数一个自己的一个父类的但是这里为什么只调用了一个

这里是因为,析构函数会被编译器统一处理成destructor()那么这里就有俩个destructor函数这里就会成隐藏关系
所以这里就只会调用父类的析构函数


这里的解决方式就是加上virtual
在这里插入图片描述

在这里插入图片描述

四、重载/重写/隐藏的对比

在这里插入图片描述


五、抽象类

抽象类

class Car
{
public:virtual void Drive() = 0;
};

这就是一个抽象类,在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。


抽象类也无法实例化,这里就会有一个疑问了,那有个蛋用呢?
在这里插入图片描述


如果我不想让别人实例化出car类出来但是可以实例化benz出来就可以用抽象派去重写,继承的benz类是可以实例化的

class Car
{
public:virtual void Drive() = 0;
};class Benz : public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;	}
};
int main()
{Benz a;
}

先来俩题热热身

六、相关题目

题目1

class A
{
public:virtual void func(int val = 1){cout << "A->" << val << endl;}virtual void test(){func();}
};class B : public A
{
public:void func(int val = 0){cout << "B->" << val << endl;}
};int main()
{B* p = new B;p->test();return 0;}

在这里插入图片描述

答案是B

在这里插入图片描述


这里就是c++语法的一个大坑,用的是B类,但是这里是继承的test会去找子类的,但是可以发现子类哪里是可以不写virtual的,用的是子类的定义,但是声明依旧用的是父类的


题目2

class Base
{
public:virtual void Fun1(){cout << "Func1" << endl;}
private:int _a = 1;char ch = '1';};int main()
{cout << sizeof(Base) << endl;
}

在这里插入图片描述
在这里插入图片描述
定义成虚函数了以后这里就多要一个指针
在这里插入图片描述


所以说写成虚函数就会付出代价,这个指针指向虚函数表,虚函数的地址都会放在这张表里面,多态就是靠这张虚函数表实现的


七、const修饰

纯虚函数无法实例化,这里可以强制别人用引用或者指针

class Animal
{
public:virtual void sound() = 0;
};class Cat : public Animal
{
public:virtual void sound(){cout << "Cat-喵喵" << endl;}
};
class Dog : public Animal
{
public:virtual void sound(){cout << "Dog-汪汪" << endl;}
};
void AnimalSound(Animal a)
{}

在这里插入图片描述


这里加上const可以使用匿名对象

class Animal
{
public:virtual void sound() const  = 0;
};class Cat : public Animal
{
public:virtual void sound()const {cout << "Cat-喵喵" << endl;}
};
class Dog : public Animal
{
public:virtual void sound()const {cout << "Dog-汪汪" << endl;}
};
void AnimalSound(const Animal& a)  
{a.sound();
}
int main()
{AnimalSound(Cat());return 0;
}

在这里插入图片描述


八、多态原理

上面说到多态必须是要虚函数并且形参还需要是父类的引用或者指针,那么这个虚函数和普通的函数有什么区别呢?

在这里插入图片描述

这里可以看到这里除了成员变量_a外还有一个_vfptr,这里面还放着成员函数的,这个东西其实就是一个指针数组,这个指针会指向一个表这个表就是虚表


下面俩个是虚函数一个不是,这里用监视窗口查看一下

class Base
{
public:virtual void Func1(){cout << "Base::Func1" << endl;}virtual void Func2(){cout << "Base::Func2" << endl;}void Func3(){cout << "Base::Func3" << endl;}
private:int _a;};int main()
{Base b;return 0;
}

在这里插入图片描述
这里可以通过上面看到这个表里面只有俩个函数


用内存窗口是可以看见的
在这里插入图片描述


多态其实就是动态确定地址

举个例子

class Base
{
public:virtual void Func1(){cout << "Base::Func1" << endl;}virtual void Func2(){cout << "Base::Func2" << endl;}void Func3(){cout << "Base::Func3" << endl;}
private:int _a;};
class Derive : public Base
{
public:virtual void  Func1(){cout << "Derive::Func1" << endl;}
private:int _b;
};
void Func1(Base* p)
{p->Func1();//虚函数p->Func3();//不是虚函数
}int main()
{Func1(new Base);Func1(new Derive);return 0;
}

在这里插入图片描述

在这里插入图片描述
这里可以看见func1是重写后的那个func1但是这里的func3是不会重写的

第二个func1是重写后的那个func也就是谁调用那个虚表就指向,
第二个调用的时候我这里给的参数是Derive,这里会发生切片,这里就会对func1进行重写,但是这里收到的参数始终都是一个base指针,只不过这里可能是子类切片出来的,切片和不是切片出来的这里就会出现虚表地址不同

所以这里就叫运行时绑定也叫多态绑定,这里就可以指向父类调用父类,指向子类就调用子类,它能调用不同的函数,它本质的原因就是因为那个虚表的地址不同

这也就是为什么函数的参数必需要父类的指针或者引用的原因
在这里插入图片描述

九、虚函数放在地方

  1. 虚函数 ------------代码段
  2. 虚函数表 --------代码段
  3. 虚函数表的指针-类对象

总结

多态看起来复杂其实只要理解了虚表这个东西就不会复杂,虚函数就有一个指针数组,这里面放着虚表的地址,然后这些虚表里面放着虚函数的地址,然后在通过切片来看是哪一个虚表的地址这就是多态的原理

http://www.lryc.cn/news/501820.html

相关文章:

  • mac删除程序坞(Dock)中“无法打开的程序“
  • 【Linux】vi/vim 使用技巧
  • Python自动化办公(系统维护及开发任务状态自动推送)
  • CentOS7 Apache安装踩坑
  • OpenMMlab导出MaskFormer/Mask2Former模型并用onnxruntime和tensorrt推理
  • 若依微服务中配置 MySQL + DM 多数据源
  • 一些前端组件介绍
  • python学opencv|读取图像(九)用numpy创建黑白相间灰度图
  • AtCoder Beginner Contest 383
  • 20. 内置模块
  • 《知识拓展 · 统一建模语言UML》
  • 计算机网络-Wireshark探索ARP
  • 减少30%人工处理时间,AI OCR与表格识别助力医疗化验单快速处理
  • 1.2.3计算机软件
  • 二、uni-forms
  • Android13开机向导
  • 软件测试丨Appium 源码分析与定制
  • 1.网络知识-IP与子网掩码的关系及计算实例
  • Android中Gradle常用配置
  • Linux操作系统3-文件与IO操作2(文件描述符fd与文件重定向)
  • k8s调度策略
  • uniapp中父组件传参到子组件页面渲染不生效问题处理实战记录
  • 螺丝螺帽缺陷检测识别数据集,支持yolo,coco,voc三种格式的标记,一共3081张图片
  • 一个简单带颜色的Map
  • kubeadm安装K8s集群之基础环境配置
  • 前端实现在线预览excel文件
  • 关于idea-Java-servlet-Tomcat-Web开发中出现404NOT FOUND问题的解决
  • SCRM私域流量管理工具助力企业微信电商转型升级
  • 三相异步电动机为什么能够旋转?
  • 优化移动端H5:常见问题与解决方案