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

C++之多态(从0到1的突破)

世间百态,每个人都扮演着不同的角色,都进行着不同的行为。C++更是如此,C++中也会出现有着不同行为的多种形态的出现,那就让我们一起进入C++的多态世界吧!!!

一. 多态的概念

多态,顾名思义,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。编译时
多态(静态多态)就是函数重载和函数模板,它们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为它们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。运⾏时多态,就是传不同的对象来完成不同的⾏为,从而达到多种形态。

二. 多态的定义和实现

1.多态的构成条件

多态是⼀个继承关系下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。

注意: 实现多态还有两个必须重要条件

① 必须是基类的指针或者引⽤调⽤虚函数。

② 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。

原因:要实现多态,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类
对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派
⽣类之间才能有不同的函数,多态的不同形态效果才能达到。

2. 虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修
饰。

示例:

class Person
{
public:virtual void show() { cout << "show()" << endl;}
};virtual void f1()  //报错,非成员函数不能用virtual修饰
{}

3. 虚函数的重写/覆盖

派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名、参数类型列表完全相同),则称派⽣类的虚函数重写/覆盖了基类的虚函数。

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写但是该种写法不是很规范,不建议这样使⽤。

示例:

#define _CRT_SECURE_NO_WARNINGS  1
#include <iostream>
using namespace std;//多态的概念、定义和实现class Person {      //Person是父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {   //Student是子类
public://虚函数重写virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void Func(Person* ptr)
{//这里的函数BuyTicket()的行为和ptr指向的对象有关ptr->BuyTicket();
}//virtual void f1()  //报错,非成员函数不能用virtual修饰
//{}int main()
{Person ps;Student st;Func(&ps);Func(&st);ps.BuyTicket();st.BuyTicket();return 0;
}

多态示例:

class Animal  //父类
{
public:virtual void talk() const{}
};class Dog : public Animal  //子类
{
public:virtual void talk() const{cout << "汪汪" << endl;}
};class Cat : public Animal  //子类
{
public:virtual void talk() const{cout << "喵喵" << endl;}
};void letsHear(const Animal& animal)
{animal.talk();
}int main()
{Cat cat;Dog dog;letsHear(cat);letsHear(dog);return 0;
}

多态应用:

class A
{
public:virtual void func(int val = 1) { cout << "A->" << val <<endl; }virtual void test() { func(); }  //test()的A* this,把B*对象传给this
};class B : public A
{
public:void func(int val = 0) { cout << "B->" << val << endl; }
};int main(int argc, char* argv[])
{B* p = new B;//A* p1 = new A;p->test();  //输出B->1p->func();  //输出B->0return 0;
}

4. 虚函数重写中的协变

派⽣类在重写基类虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际运用很少。

示例:

class A {};class B : public A {};class Person {
public://基类虚函数返回基类对象的指针或引用virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person {
public://派生类虚函数返回派生类对象的指针或引用virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

5. 析构函数的重写

当基类的析构函数为虚函数时,派⽣类的析构函数只要定义了,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,但实际上编译器对这里的析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了virtual修饰,派⽣类的析构函数就构成重写。

下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤
B的析构函数,就会导致内存泄漏问题,因为~B()中有资源需要释放。

示例:

class A
{
public://~A()  //有问题,会导致内存泄漏virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};// 只有子类Student的析构函数重写了父类Person的析构函数,下⾯的delete对象调⽤析构函数时才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数,防止内存泄漏。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

6. override和final关键字

C++11提供了override关键字,可以帮助用户检查虚函数是否重写。如果我们不想让子类重写这个虚函数,那么可以⽤final去修饰基类的虚函数。

示例1:

class Car {
public:virtual void Dirve(){}
};class Benz :public Car {
public://报错,override修饰的成员函数不能重写基类成员//virtual void Drive() override { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}

示例2:

class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public://报错,基类中被final修饰的成员函数不能被子类重写//virtual void Drive() { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}

7. 重载/重写/隐藏的对比

在这里插入图片描述

注意: 针对多态,重定义也叫隐藏。

三. 纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数就为纯虚函数,纯虚函数可以定义实现但是没有必要,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写派生类不能实例化对象。

示例:

class Car   //抽象类
{
public:virtual void Drive() = 0; //纯虚函数
};class Benz :public Car
{
public://若父类是抽象类,则未重写父类纯虚函数的子类也是抽象类virtual void Drive(){cout << "Benz-comfort" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-control" << endl;}
};int main()
{//Car car;  //报错,抽象类不能实例化对象Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

四.多态的原理

1. 虚函数表指针

示例:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;//12byte,每个有虚函数的类对象上都会在前面存一个_vfptr的虚函数表指针,指向一个虚函数表//虚函数表存储该类的所有虚函数地址cout << sizeof(b) << endl;return 0;
}

如下图:

在这里插入图片描述

2. 多态的原理

示例:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void func1() {}void func2() { }
private:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }virtual void func1() {}void func3() {}
private:string _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:string _codename;
};void Func(Person* ptr)
{//这里的函数BuyTicket()的行为和ptr指向的对象有关ptr->BuyTicket();
}int main()
{//多态也会发⽣在多个派生类之间Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}

通过以上代码和下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象来确定函数的地址,⽽是运⾏时到对象指向的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

在这里插入图片描述
3. 动态绑定和静态绑定

① 对不满⾜多态条件的函数调⽤是在编译时绑定,也就是编译时确定所调⽤函数的地址,叫做静态绑定。

② 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指针指向对象的虚函数表中找到所调⽤函数的地址,也就做动态绑定。

4. 虚函数表

①基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对
象各自有独⽴的虚表,所以基类和派⽣类有各自独立的虚表。

② 派⽣类成员由两部分构成,继承下来的基类成员和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,派生类自己就不会再⽣成虚函数表指针。但是这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员一样是独立的。

③ 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函
数地址。

④ 派⽣类的虚函数表中包含三个部分:(1)基类的虚函数地址。(2)派⽣类重写的虚函数地址完成覆盖。(3)派⽣类自己的虚函数地址。

⑤ 虚函数表本质就是⼀个存储虚函数表指针的指针数组。

⑥ 虚函数和普通函数⼀样,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。

⑦ vs下虚函数表是存储在在代码段(常量区)的。

示例:

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b1;Derive d1;Base* p3 = &b1;Derive* p4 = &d1;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("Base虚函数地址:%p\n", &Base::func1);printf("Base普通函数地址:%p\n", &Base::func5);return 0;
}

如图:

在这里插入图片描述

How time flies!!! 相信看到了这里的老铁一定对C++的多态有了初步甚至说是更深的理解,那各位我们就后会有期了!!!

http://www.dtcms.com/a/341730.html

相关文章:

  • 【clion】cmake脚本1:调试脚本并构建Fargo项目win32版本
  • python 可迭代对象相关知识点
  • “无纸化办公”加速推进,房产证智能识别技术或成行业标配
  • Linux高效备份:rsync + inotify实时同步
  • 服务器硬盘进行分区和挂载
  • SpringBoot3后端项目介绍:mybig-event
  • 【MySQL的卸载】
  • 5.k8s控制器-Replicaset-Deployment、pod 反亲和性
  • VLN领域的“ImageNet”打造之路:从MP3D数据集、MP3D仿真器到Room-to-Room(R2R)、RxR、VLN-CE
  • Adobe Acrobat 表单创建与设置
  • 8.20 打卡 DAY 47 注意力热图可视化
  • 不会写 SQL 也能出报表?积木报表 + AI 30 秒自动生成报表和图表
  • JVM讲解
  • leetcode7二分查找_69 and 34
  • Linux正则表达式
  • 2D水平目标检测数据增强——旋转任意指定角度
  • RK3568 Linux驱动学习——设备树下 LED 驱动
  • Redisson最新版本(3.50.0左右)启动时提示Netty的某些类找不到
  • PowerShell脚本检查业务健康状态
  • 解决Docker 无法连接到官方镜像仓库
  • Lecture 6 Kernels, Triton 课程笔记
  • JVM基础知识总结
  • Docker 核心技术:Linux Cgroups
  • GDB 的多线程调试
  • 针对具有下垂控制光伏逆变器的主动配电网络的多目标分层协调电压/无功控制方法的复现
  • 音频读写速度优化 音频格式
  • Transformer内容详解(通透版)
  • pip install -e中e 参数解释
  • 八辊矫平机·第三篇
  • 卸载win10/win11系统里导致磁盘故障的补丁