PS:资料来自慕课网视频。
一、什么是多态
多态: 指相同对象收到不同消息或不同对象收到相同消息时产生不同的动作。
1)静态多态(早绑定):主要通过函数和运算符重载来实现。程序在运行之前,在编译阶段就已经确定下来到底要使用哪个函数。
2)动态多态(晚绑定):对不同的对象下达相同的指令,对象做着不同的操作。
动态多态必须以封装和继承为基础。使用virtual修饰的函数是虚函数,定义子类的时候,在同名的成员函数前面也要加virtual。
子类中实现多态的虚函数要与父类中相应的函数完全相同,包括函数名、参数、返回值。
二、虚函数
1.普通虚函数
(1)子类未定义虚函数时:
当实例化一个Shape的对象时,对象中除了数据成员还会有另外一个数据成员,即虚函数表指针(指向一个虚函数表,存放虚函数表的起始位置),
虚函数表与Shape类的定义同时出现,在计算机中占用一定的空间。
父类的虚函数表只有一个,通过父类实例化出来的所有对象的虚函数表指针都是一个值。
虚函数表中定义了虚函数指针,即虚函数的入口地址。
Circle未定义虚函数,但从父类中继承了虚函数,所以在实例化Circle对象时也会产生一个虚函数表(与父类不是同一个,地址不同),
但是在Circle虚函数表中,从父类继承的虚函数指针与父类一样(继承的虚函数入口地址与父类一样)
(2)子类中定义了与父类相同的虚函数时:
2.虚析构函数
虚析构函数特点:在父类中通过virtual修饰析构函数后,通过父类的指针指向子类的对象,然后通过delete接父类指针就可以释放掉子类对象。
1)、什么时候需要用到虚析构函数?
当存在继承关系的时候,使用父类的指针指向堆中的子类对象,并且使用父类的指针去释放这块内存,这个时候需要在父类定义(.h文件)中的析构函数前面加上 virtual
virtual关键字可以被继承下去,即是说,该父类的子类的析构函数也是虚析构函数,即使子类虚构函数没写virtual(建议写上)
2)、
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。
如果某个类不包含虚函数,那一般是表示它将不作为一个基类来使用。当一个类不准备作为基类使用时,使析构函数为虚一般是个坏主意。因为它会为类增加一个虚函数表,使得对象的体积翻倍,还有可能降低其可移植性。
所以基本的一条是:无故的声明虚析构函数和永远不去声明一样是错误的。实际上,很多人这样总结:当且仅当类里包含至少一个虚函数的时候才去声明虚析构函数。
抽象类是准备被用做基类的,基类必须要有一个虚析构函数,纯虚函数会产生抽象类,所以方法很简单:在想要成为抽象类的类里声明一个纯虚析构函数。
3.virtual使用限制
三、覆盖与隐藏
隐藏:不涉及多态(没有多态时),定义了父类和子类,父类和子类出现同名函数,称之为函数的隐藏。
覆盖:涉及多态。如果没有在子类中定义同名的虚函数,那么在子类虚函数表中就会写上父类相应虚函数的函数入口地址;
如果在子类中定义了同名的虚函数,那么在子类虚函数表中就会把原来父类的虚函数地址覆盖成子类的虚函数地址。
父类子类有同名函数时,有virtual修饰=覆盖,无virtual=隐藏
课程评论记录1:
先说个函数指针的概念,每个类(除了空类,就是没有方法也没有属性的类)在创建的时候,就会生成一个虚函数表指针,这个指针与普通的指针一样,存的是函数的入口地址,这是在类生成的时候就建立的。下来说几种情况:
1)父类实现了非virtual修饰的方法一,子类继承父类,子类没有再实现方法一,这样父类与子类的关于方法一在各自虚函数表中的地址是一样的,也就是子类可以直接用父类的方法,而不用再去实现;
2)父类实现了非virtual修饰的方法一,子类继承父类,子类重写了方法一,这样子类的虚函数表的方法一的地址与父类的虚函数表的方法一的地址是不同的。
这时候父类指针指向子类对象的时候,调用方法一时会用父类虚函数表中方法一的入口,这样执行的就是父类方法一的实现;而子类调用方法一时,使用的是子类虚函数表中的方法一的入口,这样执行的就是子类方法一的实现。这种情况叫隐藏。
3)父类实现了virtual修饰的方法一,子类继承父类,子类没有再实现方法一,这样父类与子类的关于方法一在各自虚函数表中的地址是一样的,也就是子类可以直接用父类的方法,也不用再去实现;
4)父类实现了virtual修饰的方法一,子类继承父类,子类重写了方法一,这样子类的虚函数表的方法一的地址与父类的虚函数表中方法一的入口地址也是不同的。
与第二种情况不同的是,采用virtual修饰的方法,在父类指针指向子类对象时,子类的同名方法会覆盖父类的方法的入口,也就是父类的虚函数表方法一的入口地址会被子类的虚函数表的方法一的入口覆盖,这时候父类指针执行的就是子类的方法一的实现,从而实现多态。这种情况叫覆盖。
下来说 父类 * p = new 子类; 这样的操作之后到底执行哪块代码,就得分具体情况了,但是在面向对象的编程中,多态是一个很重要的特性,
所以一般建议大家对有继承关系的类加上virtual修饰。
课程评论2:
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。
比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
四、函数的本质
函数的本质就是一段写在内存中的二进制代码,可以通过指针指向这段代码的开头(入口),这就是函数指针。
五、对象的大小
1.综述
1)对象的大小:在类实例化出的对象中,数据成员所占据的内存大小。【注意:不包括成员函数】
2)对象的地址:通过一个类实例化了一个对象,该对象在内存当中会占有一定的内存单元,这个内存单元的第一个内存单元的地址即对象的地址。
3)对象成员的地址:通过一个类实例化一个对象后,对象中每一个数据成员所占据的地址即对象成员的地址。
4)虚函数表指针:在具有虚函数的情况下,实例化一个对象时,对象的第一块内存中所存储的指针。
对象的地址:通过一个类实例化的一个对象,这个对象在内存当中占有的第一个内存单元的地址就是这个对象的地址。
对象成员的地址:当用一个类实例化一个对象之后,这个对象中可能与一个或多个数据成员,每一个数据成员所占据的地址就是这个对象的成员地址,对象的数据成员由于数据类型不同那么占据的内存大小也不同,地址也是不同的。
虚函数表指针:在具有虚函数的情况下实例化对象时,这个对象的第一个内存存储的是一个指针,即虚函数表的指针,占四个内存单元。
2.无数据成员的对象占用内存大小:
对于没有数据成员的对象,其内存单元也不是0,c++用一个内存单元来表示这个实例对象的存在,如果有了数据或虚函数(虚析构函数),则相应的内存替代1标记自己的存在。
3.关于虚函数表指针
虚函数表指针占据的是每个对象的前四个内存单元
有虚函数时,对象中首先存虚函数表指针,再存数据成员地址;
没有虚函数时,首先存的是数据成员的地址。
六、纯虚函数与抽象类
1.纯虚函数
2.抽象类
定义:存在一些类,不生成对象,而是当成基类去派生子类;如果子类实现了基类的全部虚函数,则其不是抽象类。
抽象类无法实例化对象,抽象类的子类只有把抽象类当中的所有纯虚函数都做了实现才可以实例化对象。
【注意:抽象类不能实例化,但是可以定义指针和引用,通过指针和引用访问子类对象的虚函数,实现多态性。】
抽象类无法实例化对象;
抽象类的子类也可以是抽象类;
3.接口类
接口类全是纯虚函数,没有实现代码,所以没有.cpp文件。
接口类无构造函数与析构函数。接口类更多的表达一种能力或协议。
七、运行时类型识别(RTTI)
RTTI:Run - Time Type Identification
主要用到:typeid与dynamic_cast
1.实例:
2.关于typeid
(1)typeid能够看任何一个对象或者指针的类型,包括基本数据成员的类型,比如int、double等基本类型
(2)typeid能够判断指针的类型,是指这个指针本身的类型,比如定义一个父类指针(含有虚函数),该指针指向子类的对象,使用typeid(该指针).name()后,结果是指向父类的指针类型
eg:
class Flyable
{
virtual void x()=0;
};
class Bird:public Flyable
{
virtual void x(){}
};
Flyable *p=new Bird;
cout<<typeid(p),name()<<endl;
cout<<typeid(*p),name()<<endl;
运行后输出的是:
class Flyable *
class Bird
3.dynamic_cast注意事项