C++多态详解

embedded/2024/9/23 2:15:09/

1. 多态的概念

多态就是函数调用的多种形态,使用多态能够使得不同的对象去完成同一件事时,产生不同的动作和结果。
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包支付给奖励金的 活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5 毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 =random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

2. 多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。

那么在继承中要构成多态还有两个条件:

1.父子类完成虚函数重写

2.父类的指针或者引用去调用虚函数

用下面的等价的    ||

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
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;
}

注意 注意 注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因 为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用

2.2 虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

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

2.3虚函数的重写

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

指针版本

void Func(Person* p)
{//不同的人做一件事结果不一样p->BuyTicket();
}

引用版本

void Func(Person& p)
{//不同的人做一件事结果不一样p.BuyTicket();
}

当然这样是不行的,必须是指针和引用

void Func(Person p)
{//不同的人做一件事结果不一样p.BuyTicket();
}

虚函数重写的两个例外:

1. 协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。

如果把继承去掉,那么便构不成斜变,返回值不同要求父子类的指针或引用

2. 析构函数的重写(基类与派生类析构函数的名字不同)

普通函数下,先子后父

class Person {
public:~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:~Student() { cout << "~Student()" << endl; }
};int main()
{Person(p);Studend(s);return 0;
}

如果使用指向谁,就无法调用子类的了

那我们这样岂不是会内存泄漏

我们加上虚函数就好了

只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函 数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。

这样也没有内存泄漏了

class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student(){cout << "delete[]" << _ptr << endl;delete[] _ptr;cout << "~Student()" << endl;}
private:int* _ptr = new int[10];
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

小贴士:建议析构函数加上virtual

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor。

注意:派生类可以不加virtual,但是父类不能不加virtual,我们建议都写上。

派生类不加virtual,继承他的函数,重写他的实现。

来一个小题练练手吧:

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

再来一个吧

class Base1 {  public:  int _b1; };
class Base2 {  public:  int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

这道题主要考了切片和怎么放的

我们只需要修改一个public base1 ,public base2就可以分辨出来了

2.4 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数 名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有 得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

1. final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};

    C++98的方法:父类的构造函数私有
    子类的构造无法生成和实现,导致子类对象无法实例化

class Car final
{
public:private://C++98的方法:父类的构造函数私有//子类的构造无法生成和实现,,导致子类对象无法实例化Car() {}
};
class Benz :public Car
{
public:};int main()
{Benz b;return 0;
}

final 修饰的类叫做最终类不能被继承

2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car {
public:virtual void Drive() {}
};
class Benz :public Car {
public:virtual void Drive() override { cout << "Benz-舒适" << endl; }
};

2.5 重载、覆盖(重写)、隐藏(重定义)的对比

3. 抽象类

3.1 概念

虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生 类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

某种程度上强制了派生类的重写

class Car
{
public:virtual void Drive() = 0;
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz-舒适" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-操控" << endl;}
};
void Test()
{/*Benz b1;b1.Drive();BMW b2;b2.Drive();*/Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

3.2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实 现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

4.多态的原理

4.1虚函数表

我们先看一道面试题:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

通过观察测试我们发现b对象是8bytes,为什么是8呢???按照正常来说不应该是4吗??除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表。那么派生类中这个表放了些什么呢?我们接着往下分析~~~~~

通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表 中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数 的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函 数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:
  • a.先将基类中的虚表内容拷贝一份到派生类虚表中
  • b.如果派生 类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
  • c.派生类自己 新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

这里还有一个童鞋们很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在 虚表,虚表存在对象中。注意上面的回答的错的。但是很多童鞋都是这样深以为然的。注意 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是 他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的 呢?

4.2多态的原理

上面分析了这个半天了那么多态的原理到底是什么?还记得这里Func函数传Person调用的 Person::BuyTicket,传Student调用的是Student::BuyTicket

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 Mike;Func(Mike);Student Johnson;Func(Johnson);return 0;
}

满足多态条件
那么这里调用生成的指令,就会指向对象的虚表中对应虚函数进行调用

运行时决定调用谁

指向父类调用父类的虚函数

执行子类调用子类的虚函数

打印虚函数表
typedef void(*VFPTR)();void PrintVFT(VFPTR* vft)
{for (size_t i = 0; i < 4; i++){printf("%p\n", vft[i]);VFPTR pf = vft[i];(*pf)();}
}
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class DeriveDerive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; } //func1重写了virtual void func3() { cout << "Derive::func3" << endl; } //没有重写virtual void func4() { cout << "Derive::func4" << endl; } //没有重写
private:int b;
};
int main()
{Base b;DeriveDerive dd;VFPTR* ptr = (VFPTR*)(*((int*)&dd));PrintVFT(ptr);return 0;
}


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

相关文章

CCFF:RT-DETR中的CCFF结构代码详解(Pytorch)

代码链接 lyuwenyu/RT-DETR: [CVPR 2024] Official RT-DETR (RTDETR paddle pytorch), Real-Time DEtection TRansformer, DETRs Beat YOLOs on Real-time Object Detection. &#x1f525; &#x1f525; &#x1f525; (github.com)https://github.com/lyuwenyu/RT-DETR 模…

CentOS7安装Docker教程(含最新镜像地址)

文章目录 1 安装前必读2 安装Docker的详细步骤3 配置镜像加速 1 安装前必读 在安装 Docker 之前&#xff0c;先说一下配置&#xff0c;我这里是Centos7 Linux 内核&#xff1a;官方建议 3.10 以上&#xff0c;3.8以上貌似也可。 注意&#xff1a;本文的命令使用的是 root 用户…

C# 字符串扩展方法

功能 1.判断一个字符串是否为null或者空字符串 2.判断一个字符串是否为null或者空白字符 3.判断一个字符串是否为数字 4.判断一个字符串是否为邮件 5.判断一个字符串是否为字母加数字 6.判断一个字符串是否为手机号码 7.判断一个字符串是否为电话号码 8.判断一个字符串是否为网…

Next.js中的客户端渲染和服务端渲染

前言 为什么会想着探究这个呢&#xff1f;因为我在学习的过程中发现&#xff1a;在next中默认是"use server"也就是如果使用服务端的话是不需要标明的&#xff0c;只有客户端才需要标明"use client",但是在我去掉代码中的"use server"后会报错c…

C#中的多线程

c#中的Task 在C#中&#xff0c;Task 是一个表示异步操作的类&#xff0c;它是 System.Threading.Tasks 命名空间下的一部分。Task 用于实现异步编程模型&#xff0c;允许开发者编写不会阻塞调用线程的代码&#xff0c;从而提高应用程序的响应性和性能。 以下是 Task 的一些关…

基于python的文件销毁工具设计与实现

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

安卓开发中的AppCompat框架简介

文章目录 安卓开发中的AppCompat框架简介如何在AppCompat中使用Toolbar&#xff1f; 安卓开发中的AppCompat框架简介 AppCompat 是 Android 开发中的一个支持库&#xff0c;旨在帮助开发者在不同版本的 Android 系统上实现一致的用户界面和功能。它提供了向后兼容的支持&#…

数字化营销在公域场景中的无限可能

在如今的商业领域&#xff0c;公域场景为企业提供了广阔的发展空间&#xff0c;而数字化营销则成为了企业在这些场景中脱颖而出的关键利器。 ​ 一、电商平台营销 当企业在淘宝、京东等大型电商平台开设店铺&#xff0c;数字化营销便开始大显身手。 企业不仅能踊跃参与像双十…