多态的原理

前言:以下的内容均是在VS2019的环境中,32位平台下的

目录

1.多态的实现条件

虚函数重写的两个例外

一个题加深理解 

总结

重载 重写 重定义区别

2.多态的实现原理

单继承

多继承

动态多态和静态多态

多态的好问题


1.多态的实现条件

虚函数:被virtual修饰的成员函数,和虚拟继承没有一点关系

a.虚函数的重写(三同:函数名,返回值,参数类型)

b.父类的指针或引用调用虚函数

虚函数的重写(覆盖):子类继承了父类的虚函数的接口,对其函数主体进行重写,父类和子类具有相同的虚函数,相同是指三同:函数名,返回值,参数类型,函数主体不同。

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{ p.BuyTicket(); }
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}

构成多态:不同的对象去调用同一函数,呈现出不同的状态。

不构成多态:指针或引用类型是什么就调用谁的

虚函数重写的两个例外

协变

子类重写父类虚函数时可以返回值类型不同,但是子类虚函数必须返回子类对象的指针或引用(不一定是本身子类对象,可以是其他子类对象),父类虚函数必须返回父类对象的指针或引用(不一定是本身父类对象,可以是其他父类对象)

class A{};
class B : public A {};
class Person {
public:
virtual Person* f() {return new Person;}
};
class Student : public Person {
public:
virtual Student* f() {return new Student;}
};
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};

析构函数的重写

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

class Person {
public:virtual~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

一个题加深理解 

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

答案是什么?

解释:B 

改了一下 

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

又选什么?

解释:D

总结

多态就是硬套条件,符合多态就按多态走,不符号多态就按类型走

重载 重写 重定义区别

2.多态的实现原理

单继承

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};int main()
{Base B;cout << sizeof(Base) << endl;return 0;
}

运行发现Base的大小是8字节,为什么呢?

通过调试我们发现,Base对象除了有_b成员变量,还存在一个_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 _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

多态实现原理:父类的指针或者引用指向父类就通过虚表指针找到父类的虚表在调用相应的虚函数,指向子类通过虚表指针找到子类的虚表在调用相应的虚函数。

反思一下为啥满足多态的条件有一个是父类的指针或引用,不能是父类对象。

虚函数表的问题

1.VS系列编译器虚表最后面放的是nullptr,g++编译器没有

2.虚表是在编译阶段产生的

3.虚表指针是在构造函数初始化列表初始化 

4.虚表存的是虚函数的地址,不是虚函数,虚函数和普通函数一样在代码段中,虚表也存在带码中

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

打印虚表

虚表怎么打印呢?

 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

1.先取b的地址,强转成一个int*的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。

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; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :%p\n", i, vTable[i]);}cout << endl;
}
int main()
{Base b;Derive d;VFPTR * vTableb = (VFPTR*)(*(int*)&b);PrintVTable(vTableb);VFPTR* vTabled = (VFPTR*)(*(int*)&d);PrintVTable(vTabled);return 0;
}

多继承

菱形继承不做解释,太复杂了,实际也不会用。

动态多态和静态多态

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

多态的好问题

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

可以,内联函数是就地展开的,认为没有地址,也就不能放进虚表里,但是编译器会忽略inline属性,内联函数只是给编译器的一个建议,编译器可以不采取,成员函数要么是内联要么是虚函数,两者只能有一个

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

不可以,静态成员函数是所有类共享的,通过类型::成员函数来访问的,没有this指针,也就不能访问虚表,不能放进虚表里面

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

不可以,虚表指针就是在构造函数的初始化列表初始化的,拷贝构造也不行,但是赋值重载是可以的,最好不要是

4.析构函数可以是虚函数吗?

可以,并且最好把基类的析构函数定义成虚函数

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

a.如果有虚函数(不构成多态),普通调用,那么和普通函数一样快

b.如果有虚函数和普通函数,通过指针或引用调用,不管构不构成多态,调用虚函数时都会到虚表里面去找,那么普通函数就快了

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/682185.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2-6 任务 猜数小游戏(单次版)

本任务要求编写一个猜数小游戏&#xff08;单次版&#xff09;&#xff0c;游戏规则是计算机产生一个0到100之间的随机整数&#xff0c;用户通过输入猜测的数字进行猜测&#xff0c;根据猜测情况给出提示&#xff0c;直到猜对为止。编程思路是利用while循环和多分支结构实现永真…

ESP-WROOM-32配置Arduino IDE开发环境

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、下载Arduino IDE二、安装工具集三、测试样例1.选则开发板2.连接开发板3.示例程序 四、使用官方示例程序总结 前言 之前用了很多注入STM32、树莓派Pico和Ar…

用Rust解决鸡兔同笼问题

目录 一、什么是鸡兔同笼问题&#xff1f; 二、用Rust解决鸡兔同笼问题 三、鸡兔同笼问题在实际生活中的应用有哪些&#xff1f; 一、什么是鸡兔同笼问题&#xff1f; 鸡兔同笼问题是一种古代著名的数学问题&#xff0c;用于训练逻辑思维和解决方程的能力。 鸡兔同笼问题起…

ssm105基于JAVAEE技术校园车辆管理系统+jsp

校园车辆管理系统设计与实现 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本校园车辆管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短…

SpringBoot中使用RocketMQ实现事务消息来保证分布式事务的一致性(有代码)

前言 分布式事务是分布式系统中非常常见的问题。是非常必要钱常见的。实现的方式也是多种多样。今天这个视频主要来分享一下RocketMQ实现事务消息来保证分布式事务的一致性。不知道大家使用过这种方式没有。这种分布式事务的原理其实和本地消息表一样。 本地消息表实现分布式…

ARM(2)ARMv8基础知识

目录 一、异常 1.1异常等级的定义 1.2异常的种类 1.2.1同步异常和异步异常 1.3改变异常等级 1.4异常后的处理 1.4.1异常处理相关寄存器 1.4.2系统调用 1.4.3对EL2/EL3的系统调用 1.4.4异常返回 1.4.5异常处理流程 二、安全状态 三、执行状态 本文介绍以下内容&…

javaWeb快速部署到tomcat阿里云服务器

目录 准备 关闭防火墙 配置阿里云安全组 点击控制台 点击导航栏按钮 点击云服务器ECS 点击安全组 点击管理规则 点击手动添加 设置完成 配置web服务 使用yum安装heepd服务 启动httpd服务 查看信息 部署java通过Maven打包好的war包项目 Maven打包项目 上传项目 …

Redis(无中心化集群搭建)

文章目录 1.无中心化集群1.基本介绍2.集群说明 2.基本环境搭建1.部署规划&#xff08;6台服务器&#xff09;2.首先删除上次的rdb和aof文件&#xff08;对之前的三台服务器都操作&#xff09;1.首先分别登录命令行&#xff0c;关闭redis2.清除/root/下的rdb和aof文件3.把上次的…

分布式锁讲解

概括 分布式锁是一种用于在分布式系统中实现同步机制的锁。在单机系统中&#xff0c;我们可以使用如Java中的synchronized关键字或者 ReentrantLock来实现线程间的同步&#xff0c;但在分布式系统中&#xff0c;由于多个节点&#xff08;服务器&#xff09;之间的并发操作&am…

Redis持久化策略——Java全栈知识(17)

Redis持久化 1、Redis 持久化的三种方式 1、RDB&#xff1a; 以快照的方式将此刻 Redis 中的数据以二进制的文件形式保存在磁盘中。 RDB 的优点是&#xff1a;快照文件小、恢复速度快&#xff0c;适合做备份和灾难恢复。 RDB 的缺点是&#xff1a;定期更新可能会丢数据&#…

【stomp 实战】spring websocket 接收消息源码分析

后台消息的发送过程&#xff0c;我们通过spring websocket用户消息发送源码分析已经了解了。我们再来分析一下后端接收消息的过程。这个过程和后端发送消息过程有点类似。 前端发送消息 前端发送消息给服务端的示例如下&#xff1a; 发送给目的/app/echo一个消息。 //主动发…

mysql数据库调优篇章1

目录 1.认识数据库中日志的作用2.增加mysql数据库中my.ini 基本配置3.增加my.ini中参数配置4.查看已经执行过的sql语句过去执行时间5.找出慢查询的sql6. SHOW VARIABLES LIKE ‘innodb_read_io_threads’; SHOW VARIABLES LIKE ‘innodb_write_io_threads’; SHOW VARIABLES LI…