【C++】多态——实现、重写、抽象类、多态原理

news/2024/11/28 10:51:16/

文章目录

    • 一、多态概念
    • 二、多态定义及实现
    • 三、析构函数的重写
    • 四、重载、重写、重定义总结
    • 五、C++11 override 和 final
    • 六、抽象类
    • 七、多态原理
    • 八、单继承和多继承关系的虚函数表
      • 1.单继承虚函数表
      • 2.多继承虚函数表
      • 3.菱形继承、菱形虚拟继承
    • 九、经典题目
    • 十、总结

一、多态概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

举个例子:比如在日常生活中,我们去买票:当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票 。同一个买票的动作不同的对象去完成时却是不同的。


二、多态定义及实现

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为,代码体现:

class Person
{
public://虚函数virtual void BuyTicket(){cout << "普通人买票——全价" << endl;}
};
class Student :public Person
{
public://虚函数的重写/覆盖//三同:函数名、参数、返回值virtual void BuyTicket(){cout << "学生买票——半价" << endl;}
};class Soldier :public Person
{
public:virtual void BuyTicket(){cout << "军人买票——优先" << endl;}
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person pn;Student st;Soldier sd;Func(pn);Func(st);Func(sd);return 0;
}

image-20230202185217094

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

  1. 必须通过基类的指针或者引用调用虚函数

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

这两个条件我们来仔细看一看:

虚函数:即被virtual修饰的类成员函数称为虚函数,比如上面基类的虚函数:

virtual void BuyTicket(){cout << "普通人买票——全价" << endl;}

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

virtual void BuyTicket(){cout << "学生买票——半价" << endl;}virtual void BuyTicket(){cout << "军人买票——优先" << endl;}

对于普通调用:跟调用对象类型有关,对于多态调用:通过指针/引用指向对象有关。刚开始的代码体现中我们用的就是引用,如果没有引用就不符合多态了:

image-20230202202701558

为什么父类对象不能实现多态?因为切片的时候不会把虚表拷贝过去,虚表是什么?继续往下看把

而通过指针是符合多态条件的:

image-20230202203415041

注意:

1.子类虚函数可以不加virtual

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

image-20230202203914182

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

在三同中,返回值可以换不同,但是要求返回值必须是一个父子类关系的指针或者引用

image-20230202205449178


三、析构函数的重写

析构函数如果不加virtual:

image-20230202213217936

delete的行为:1.使用指针调用析构函数 2.operator delete(ptr).此时的ptr1和ptr2都是Person的类型,普通调用跟调用类型有关,是什么类型就调用什么析构函数。但是Person指针除了指向父类对象,还要指向子类对象。而上面并没有调用子类的析构函数,后果严重要造成内存泄漏。这里我们希望的是调用析构函数是一种多态调用而不是普通调用,与指针类型无关,跟指向的对象有关,所以要想多态调用,必须得是虚函数:

image-20230202214359950

虽然析构函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

所以实现父类的时候,我们可以无脑给析构函数加上virtual


四、重载、重写、重定义总结

对于重载,两个函数要在同一个作用域之中,同时函数名/参数不同,与返回值无关!

重写(覆盖):两个函数分别在基类和派生类的作用域,同时,函数名/参数/返回值都必须相同(协变除外),并且两个函数必须是虚函数!

重定义(隐藏):两个函数分别在基类和派生类的作用域,同时,函数名相同,如果两个基类和派生类的同名函数不构成重写那就是重定义!


五、C++11 override 和 final

在继承时说过,类定义时加上final就定义了一个不能被继承的类。而final既可以修饰类也可以修饰虚函数

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

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

image-20230203092512802

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

class Car
{
public://父类不是虚函数void Drive(){}
};
class Benz :public Car
{
public:virtual void Drive() override//检查{ cout << "Benz-舒适" << endl; }
};int main()
{return 0;
}

image-20230203092940430


六、抽象类

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

image-20230203093545284

//抽象类——不能实例化出对象
class Car
{
public://纯虚函数virtual void Derive() = 0;
};class BMW :public Car
{
public://派生类重写纯虚函数virtual void Derive(){cout << "hehe" << endl;}
};int main()
{BMW b;return 0;
}

接口继承和实现继承 :

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

1.来做个题目练习一下把,以下程序输出结果是什么()

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;
}

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

答案是选B,func()函数是重写(缺省值没有要求),test()函数调用func()函数,此时的func()函数是this调用的,this是A*类型,func()是多态调用,此时this指向的是子类对象,同时根据接口继承,完成重写,用的是父类的接口,缺省值用的是父类的,所以是1.

2.sizeof(Base)是多少?

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

答案是根据成员然后内存对齐算出8吗?不是的,如果没有成员变量,我们可能会认为虚函数是1个字节,因为没有成员变量,根据我们前面所说的,空类进行占位,1个字节。但是结果是4个字节:

image-20230203111656548

在32位平台下:虚表指针占4个字节。所以结果是12:

image-20230203111936410

调试观察,我们发现除了成员变量外,还有多一个__vfptr :

image-20230203113719946

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。注意区分虚基本,虚基表是偏移量。

虚函数表本质是函数指针数组


七、多态原理

针对上面的代码,我们在来修改一下代码:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func()2" << endl;}void Func3(){cout << "Func()3" << endl;}
private:int _b = 1;char _ch;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;return 0;
}

调试观察:

image-20230203123059939

派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员

基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1()完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法

另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表

总结一下派生类的虚表生成:先将基类中的虚表内容拷贝一份到派生类虚表中,如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数,派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针,下面进行验证,取出头4个字节,强转为int*,在解引用:

image-20230203150913601

同一个类虚表是共享的:

image-20230203152727125

上面虚函数说了这么多,那多态的原理到底是个啥?

静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为也称为静态多态,比如:函数重载

动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

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 _b = 1;char _ch;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}void Func3(){cout << "Derive::Func3()" << endl;}
private:int _d = 2;
};int main()
{Base b;Derive d;//普通调用——编译时/静态 绑定Base* ptr = &b;ptr->Func3();ptr = &d;ptr->Func3();//多态调用——运行时/动态 绑定ptr = &b;ptr->Func1();ptr = &d;ptr->Func1();return 0;
}

image-20230203141609553


八、单继承和多继承关系的虚函数表

1.单继承虚函数表

class Base
{
public:virtual void func1(){cout << "Base::func1" << endl;}virtual void func2(){cout << "Base:func2" << endl;}
private:int _a;
};class Derive :public Base
{
public:virtual void func1(){cout << "Derive::func1" << endl;}virtual void func3(){cout << "Derive::func3()" << endl;}void func4(){cout << "Derive::func4()" << endl;}
private:int _b;
};int main()
{Base b;Derive d;return 0;
}

监视观察:

image-20230204095246631

但是并没有显示func3(),这里可以理解为VS做了优化,没有显示出来。导致我们看不出来,下面我们可以使用代码打印出虚表中的函数:

image-20230204101456127

typedef void(*pf)();
//函数指针数组
void PrintVFRTable(pf vft[],int n)
{//vft[i]!=nullptrfor (int i = 0; i < n; i++){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}
int main()
{Base b;PrintVFRTable((pf*)(*(int*)&b), 2);Derive d;PrintVFRTable((pf*)(*(int*)&d), 3);return 0;
}

image-20230204101028469

注意:上面用int*解引用在32位平台下是正确的,32位需要取头4个字节,但是64位平台就不能用int了,需要取头8个字节可以用double。用void就可以适应32位平台和64位平台:

PrintVFRTable((pf*)(*(void**)&b), 2);
//用int**、double**等都可以,解引用之后变成void*\int*\double*,在32位是4个字节,在64位是8个字节

2.多继承虚函数表

typedef void(*pf)();
//函数指针数组
void PrintVFRTable(pf vft[])
{for (int i = 0; vft[i]!=nullptr; i++){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}class Base1
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2
{
public:virtual void fucn1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};class Derive : public Base1, public Base2
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};

对于Base1和Base2:

image-20230204130756793

重点在于多继承的Derive:Base1,Base2各自对应自己的虚函数表,而Derive的没有重写的虚函数func3()究竟是在哪?放在Base1的虚函数表里面,还是Base2的虚函数表里面,只放入其中一个?还是都放?我们来打印出来:

image-20230204131034944

此时func3()就找到了,所以多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。另一个虚表有没有func3()呢?我们把Base2的虚表打印出来:如何打印出Derive的Base2?让指针偏移

Derive d;
//第一种方法
PrintVFRTable((pf*)(*(void**)((char*)&d + sizeof(Base1))));//第二种方法
Base2* ptr2 = &d;
PrintVFRTable((pf*)(*(void**)ptr2));

这两种方法都是一样的。

3.菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表我们就不看了,一般我们也不需要研究清楚,因为实际中很少用.下面我们只做简单的了解:

菱形继承:

class A
{
public:virtual void func1(){}
public:int _a;
};class B :public A
{
public:virtual void func1(){}int _b;
};class C :public A
{
public:virtual void func1(){}int _c;
};class D :public B, public C
{public:virtual void func1(){}int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

相比之前,菱形继承这个地方多了一个虚表,其他跟多继承并没有什么区别:

image-20230204134950812

菱形虚拟继承:

class A
{
public:virtual void func1(){}
public:int _a;
};class B :virtual public A
{
public:virtual void func1(){}int _b;
};class C :virtual public A
{
public:virtual void func1(){}int _c;
};class D :public B, public C
{public://必须重写,此时只有一份A,B、C都重写,A放B的虚函数还是C的虚函数不明确virtual void func1(){}int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

image-20230204142106096

此时就多了A自己的虚表。如果B增加了虚函数,C增加虚函数,此时又该放在哪里?

image-20230204153541957


九、经典题目

#include<iostream>
using namespace std;
class A {
public:A(char* s) { cout << s << endl; }~A() {}
};
class B :virtual public A
{
public:B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1){cout << s4 << endl;}
};
int main() {D* p = new D("class A", "class B", "class C", "class D");delete p;return 0;
}

A:class A class B class C class D

B:class D class B class C class A

C:class D class C class B class A

D:class A class C class B class D

解析:A只有一份,是在D里完成初始化的,且先走初始化列表,所以D是最后打印出来的,排除B和C,按照声明顺序,谁先继承先初始化谁:先初始化B,在初始化C。所以结果选A。

多继承中指针偏移问题?下面说法正确的是( )

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;
}

A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3

image-20230204194931870


十、总结

1.inline函数可以是虚函数吗?

可以编译通过的,因为编译器忽略了inline属性,这个函数就不在是内联函数了,因为虚函数地址要放到虚表中去

inline函数是在调用的地方进行展开,但是虚函数是要放到虚表中去的,这就矛盾了,因为inline函数没有地址,无法把地址放到虚函数表中,所以总结就是对于多态调用没有inline属性,普通调用可以继续保持inline属性

2.静态成员可以是虚函数吗?

不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。

3.构造函数可以是虚函数吗?

不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的

4.对象访问普通函数快还是虚函数更快?

对于普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找

5.虚函数表是在什么阶段生成的,存在哪的?

虚函数表是在编译阶段就生成的,在构造函数初始化列表中初始化虚表指针,一般情况下存在代码段(常量区)的

6.不要把虚函数表和虚基表搞混了:在多态中,虚函数表是存放虚函数的地址。在继承中,虚基表存储偏移量,解决菱形继承中的代码冗余与二义性

7.抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系


http://www.ppmy.cn/news/22199.html

相关文章

「自控元件及线路」1.2 电机中的磁性材料与磁场

本节介绍磁性材料的性能、分类 本节介绍电机中永磁材料的工作曲线 本节介绍电机中主磁极、电枢的磁场及电枢反应 文章目录磁性材料的基本概念磁性材料的磁性能高导磁性 饱和性 磁滞性 非线性温度特性 电阻率特性铁耗磁性材料的分类电机中的永磁材料永磁电机概述永磁材料的磁性能…

TCP协议面试灵魂12 问(三)

等待2MSL的意义 如果不等待会怎样&#xff1f; 如果不等待&#xff0c;客户端直接跑路&#xff0c;当服务端还有很多数据包要给客户端发&#xff0c;且还在路上的时候&#xff0c;若客户端的端口此时刚好被新的应用占用&#xff0c;那么就接收到了无用数据包&#xff0c;造成…

jquery方法学习及案例

JQ框架入手须知封装方法学习及应用插件&#xff08;白嫖超好用&#xff09;总结案例推荐网课链接入手须知 1.进官网点3.6版本 2.复制全部代码 3.建立文档名为jquery.min.js&#xff0c;粘贴代码 &#xff08;用的时候同cssjs引入&#xff09; 封装方法学习及应用 介绍联系…

元素的层叠顺序

层叠顺序&#xff0c;表示元素发生层叠时有着特定的垂直显示顺序。 下面是盒模型的层叠规则&#xff1a; 对于上图&#xff0c;由上到下分别是&#xff1a; &#xff08;1&#xff09;背景和边框&#xff1a;建立当前层叠上下文元素的背景和边框。 &#xff08;2&#xff09;负…

Kerberos协议与认证数据包分析

Kerberos协议 Kerberos是一种在开放的非安全网络中认证并识别用户身份信息的方法, 它旨使用密钥加密技术为客户端/服务端应用程序提供强身份认证。 目前主流的Kerberos版本是2005年的RFC4120标准的Kerberos v5, Windows、Linux和MacOs均支持Kerberos协议。Kerberos基础 Kerbe…

【JavaEE】第一个servlet程序

✨哈喽&#xff0c;大家好&#xff0c;我是辰柒&#xff01;✨ &#x1f6f0;️&#x1f6f0;️系列专栏:【JavaEE】 ✈️✈️本篇内容:如何写出第一个servlet程序&#xff01; &#x1f680;&#x1f680;代码存放仓库github&#xff1a;JavaEE代码&#xff01; ⛵⛵作者简介&…

Java和kotlin的对比

0、序言 在java的既有能力上学习kotlin&#xff0c;可快捷理解新语言特性。总体而言kotlin的语言设计思想是悲观谨慎&#xff0c;相对java的就比较乐观开放。 1、数据类型 Kotlin类型位宽度Java类型Double64doubleFloat32floatLong64longInt32intShort16shortByte8byteChar不…

尚医通(三)医院设置模块后端 | swagger | 统一日志 | 统一返回结果

目录一、医院设置模块需求二、医院设置表结构三、医院模块配置四、医院查询功能1、创建包结构&#xff0c;创建SpringBoot启动类2、编写controller代码3、创建SpringBoot配置类5、运行启动类6、统一返回的json时间格式五、医院设置逻辑删除功能1、HospitalSetController添加删除…