1.先编译代码,然后再运行代码
2.父类的引用可以指向子类!
1.程序在运行的时候如果要调用函数的话就需要根据函数地址去找到函数的位置,然后进行调用
2.在默认情况下,函数地址都是在编译阶段确定的 -- 这种被称为静态多态的函数地址早绑定
3.我们也可以通过一些技术使得函数地址在运行阶段才确定 这种被称为动态多态的函数地址晚绑定
1.以上面这张图为例,由于我们没做什么特殊处理,所以speak函数和doSpeak函数 都是静态多态早绑定,speak绑定的是通过Animal类创建的animal形参对象,而doSpeak函数绑定的则是doSpeak函数的函数实现本身
2.当我们传一个通过Animal类的子类Cat创建的对象给dospeak函数作形参的时候,根据“c++中父类的引用可以指向子类”的规则,这个操作是合法的,那么问题就来了,我们传的是cat类的对象,但是这个引用又是Animal类的引用,那么通过这个引用调用的speak函数究竟是调用的cat的函数animal的呢?
3.答案就在第1点:第一点中说了speak函数是静态多态早绑定,且绑定的地址是animal类对象,所以在运行阶段无论传父类还是子类给引用,调用的speak函数时,都只会根据它早就绑定好的地址去找到函数并调用 --- 即 animal类处的地址。
4.但是如果我们想要传进来父类就调用父类的speak函数,传进来子类就调用子类的speak函数的话,我们该怎么办呢? --- 答案就是通过一项技术将静态多态函数改变为动态多态函数
5.这项技术的名字叫做虚函数 --- 操作方法是在静太早地址函数的函数返回类型左边加上关键字 virtual 来进行修饰,如图:
1.什么是动态多态?
首先子类中有一个和父类除了函数实现部分其它完全相同的成员函数A(函数返回类型,函数名,形参列表都向同级),然后我们通过父类的指针或引用来调用这个函数
当我们让父类的指针或引用指向一个父类对象的时候,就调用父类中的成员函数A
当我们让父类的指针或引用指向一个子类对象的时候(c++允许这么做),就调用子类中的成员函数A
我们把上面这种情况称为动态多态
2.实现动态多态的条件是什么呢?
一.得有继承关系
二.父类中成员函数A必须被关键字 virtual修饰为虚函数
三.子类中重写父类的虚函数 --- 注意这里的重写是指将父类中的那个虚函数去掉 virtual 后在子类中再写一份
3.动态多态的使用:
父类的指针或引用指向子类对象
这样Animal类相当于一个空类,因为成员函数和类的内存空间是分开的,所以此时Animal类的内存空间大小的蓝图规划是一个字节(这一个字节是用来占位的,表示这里有这么一个字节)
而当类中的成员函数被关键字 virtual 关键字修饰为虚函数之后,类的大小就会变为四个字节,原因是类中多了一个指针(1.类中装了东西后原本用来占位的一个字节就会被自动删除 2.所有指针的大小都是四个字节) ,也就是说 --- 虚函数的出现修改了类的结构
1.首先第一个要说明的:当函数变为虚函数之后,出现在类中的指针的指针名为:vfptr
v- virtual (虚拟的)
f - funtion(函数)
ptr - pointer
中文名为:虚函数指针 --- 有时候又被称为虚函数表指针
这个指针会指向一个虚函数表
2.这个虚函数表的名称是 : vftable --- table就是表格的意思
在表的内部会记录产生对应虚函数指针的虚函数的地址
如果是普通的全局虚函数的话,地址就直接用取地址符号&+函数名的方式记录
但如果是成员虚函数的话,在记录其地址前需要加上作用域,告诉编译器它是属于哪一个类的
即为取地址符号&+作用域+函数名
(继承就是子类拷贝了一份父类在自己的体内,只不过体内中的父类数据有的可访问有的不可访问)
3.子类继承父类之后(放了一个父类到自己体内之后),也会生成一个和父类一样的虚函数指针和虚函数表,虚函数表中放的地址也和父类一样,都为 &+作用域(父类类名::)+函数名
但是,当子类中出现复写的时候,情况就会发生改变
当子类对父类中的虚函数进行复写(将父类中的虚函数重新写一份到子类中)后,子类的虚函数指针指向的虚函数表中记录的函数地址就会新的函数地址覆盖,这个新的函数地址是:
&+作用域(子类类名::)+函数名
(要注意的是!!子类更新的只是自己继承(拷贝)的父类的虚函数表,原本的父类的虚函数表并没有改变)
4. 现在无论是子类还是父类都有了自己的speak函数地址
当我们用父类的引用或者指针指向子类对象的时候,如果调用speak函数的话,编译器就会在子类对象中找speak函数的地址,而找到的这个地址就是我们更新后的地址,而这个更新的地址指向的是子类对象中的speak函数,所以我们就能够调用子类对象中的speak函数
动态多态的本质其实就是利用虚函数来更新函数地址
1.在真实的开发中,我们提倡开闭原则 : 即对扩展进行开放,对修改进行关闭
2.
abc是一个指针,指向的是一个堆区的数据。这个delete销毁的是abc指针指向的堆区数据以及指针承接的地址,指针本身并没有被销毁。
3.多态优点分析:
一.代码组织结构清晰的好处就是,那一块组织出错我们就能够快速定位到这个组织
二.可读性强,因为组织结构清晰,所以我们读起来快且清楚
三.对于前期和后期扩展以及维护性高,多态的扩展只需要追加新的子类就行了,不需要修改源码
c++提倡我们使用多态去设计程序架构
1.抽象类无法实例化对象的意思就是无法通过抽象类来创建对象
2.类中只要有一个纯虚函数,这个类就是抽象类,除非我们在这个类中又重写了纯虚函数,重写纯虚函数时virtual可写可不写,然后纯虚函数的 = 0这个绝对不要写,就正常的写大括号+括号内的函数实现就行。
3.多态(一个函数多种形态)
1.堆区数据手动开辟手动释放!!!
2.多态多态 --- 同一个函数的多种形态被调用
1.使用多态的一个条件是用父类的指针或引用来指向子类对象,如果父类的指针指向的是一个开辟到堆区的子类对象的话,父类指针的释放(堆区手动开辟手动释放)是不能够调用子类的析构代码的
2.解决第一点的问题的方法:将父类中的析构函数改为虚析构或者纯虚析构函数
3.有纯虚析构函数的类也是抽象类,我们是无法通过这个类来创建对象的,只有虚函数或者虚析构函数的话则不是抽象类
5.delete释放完指针之后我们要让指针指向空 ”NULL“,避免指针称为野指针
6.注意在上图中我们是用父类的指针来指向和维护开辟在堆区中的子类对象的,当我们通过父类指针来对这个堆区中的子类对象进行释放的时候(即销毁子类对象的时候),我们是无法调用子类的析构函数的!!! --- 这就导致子类中如果有堆区的数据,就会使得堆区内存未被释放,进而出现内存泄漏问题
7.解决上面这个问题的方法就是将父类的析构函数修饰为虚析构函数
上面这个图中展示的就是纯虚析构函数 ,一个类中只能存在虚析构和纯虚析构中的一个
8.无论是虚析构函数还是纯虚析构函数都必须有函数的实现,因为它们都会在对象被销毁的时候被调用
9.在类中写了纯虚函数之后我们还需要在类外写这个纯虚函数的函数实现,写的方式是:
一.去掉virtual关键字
二.在被去掉的virtual关键字的位置上加上这个纯虚函数的作用域 --->纯虚函数所属的类的类名::
三.去掉 = 0 ,在函数底下加大括号写函数的实现。
10.纯虚函数不需要加函数的实现,而纯虚析构则需要在类外进行纯虚析构函数的实现