今天来总结C++语言中极为重要的知识点——虚函数,包含常考面试题┗|`O′|┛ 嗷~~,一起来学习吧~
一、虚函数与纯虚函数
1.1 虚函数
虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载。核心理念就是通过基类访问派生类定义的函数。只有指定为虚函数的成员函数才能进行动态绑定。
1.2 纯虚函数
纯虚函数:是一种特殊的虚函数,在基类中为其派生类保留一个函数的名字,在基类中没有定义,要求任何派生类根据需要对它进行定义。作为接口而存在,纯虚函数不具备函数的功能,一般不能直接被调用。
//纯虚函数
class <类名>
{
virtual <类型><函数名>(<参数表>)=0;
…
};
纯虚函数的作用:在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。将函数定义为纯虚函数,若要使派生类为非抽象类,则编译器要求在派生类中,必须对纯虚函数予以重载以实现多态性。
纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。
1.3 纯虚函数的引入原因
(1)为了方便使用多态特性,在基类中定义纯虚函数。
(2)在很多情况下,基类本身生成对象是不合情理的。
(3)为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
(4)为了编码的效率。
1.4 虚函数与纯虚函数的区别
相同:
(1)虚函数与纯虚函数都可以在子类中重写
区别:
(1)纯虚函数只有定义没有实现,虚函数既有定义又有实现
(2)含有纯虚函数的类不能创建对象,含有虚函数的类能定义对象
二、函数是否能为虚函数
2.1 构造函数能否为虚函数
构造函数不能是虚函数。
原因:
(1)创建一个对象时需要确定对象的类型,而虚函数是在运行时确定其类型的,在构造一个对象时,由于对象还未创建成功,编译器无法知道对象的实际类型。
(2)虚函数的调用需要虚函数表指针,而该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还未创建,还没有内存空间,更没有虚函数表地址用来调用虚构造函数。
(3)构造函数是用来初始化对象的。假如子类可以继承基类构造函数,那么子类对象的构造将使用基类的构造函数,而基类构造函数并不知道子类的有什么成员。从另外一个角度来讲,多态是通过基类指针指向子类对象来实现多态的,在对象构造之前并没有对象产生,无法使用多态特性,因此构造函数不允许是虚函数。
2.2 析构函数能否为虚函数
C++中基类采用virtual虚析构函数是为了防止内存泄漏。
如果派生类中申请了内存空间,假设基类中采用非虚析构函数,当删除基类指针指向的派生类对象时不会触发动态绑定,只会调用基类的析构函数,而不会调用派生类的析构函数。派生类中申请的空间就得不到释放从而产生内存泄漏。
2.3 基类里private成员函数可以声明为虚函数吗?
可以的,不过就和私有成员创建的本意有点相反,不建议实现。
2.4 哪些函数不可以为虚函数
(1)构造函数,构造函数初始化对象,派生类必须知道基类函数干了什么,才能进行构造;当有虚函数时,每一个类有一个虚表,每一个对象有一个虚表指针,虚表指针在构造函数中初始化;
(2)内联函数,内联函数表示在编译阶段进行函数体的替换操作,而虚函数意味着在运行期间进行类型确定,所以内联函数不能是虚函数;
(3)静态函数
<1>静态函数不属于任何对象或类实例,属于类
<2>静态成员函数没有this指针,虚函数依靠vptr和vtable来处理。vptr在类的构造函数中创建生成,并且只能用this指针来访问它,vptr指向保存虚函数地址的vtable,对于静态成员函数,它没有this指针,所以无法访问vptr。
(4)友元函数,友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
(5)普通函数,普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
三、抽象类
抽象类定义:带有纯虚函数的类为抽象类。抽象类不能声明对象,只是作为基类为派生类服务。
抽象类的主要作用是将有关的操作作为接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。
注意:抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类中没有重新定义纯虚函数,则这个派生类仍然还是一个抽象类,不能定义对象。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,可以建立对象。
四、虚函数的原理
编译时若基类中有虚函数,编译器为该类创建一个一维数组的虚函数表,存放是该类每个虚函数的地址。虚函数指针是在运行期——也就是构造函数被调用时进行初始化的。
基类和派生类都包含虚函数时,这两个类都建立一个虚函数表。在构造子类对象时,要先调用父类的构造函数,初始化父类对象的虚表指针,该虚表指针指向父类的虚表。执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
带有虚函数的每一个对象都有一个虚指针指向该类的虚函数表。
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
当用一个指针/引用调用一个函数的时候,被调用的函数是取决于这个指针/引用的类型。即如果这个指针/引用是基类对象的指针/引用就调用基类的方法;如果指针/引用是派生类对象的指针/引用就调用派生类的方法,如果派生类中没有此方法,就会到基类里面去寻找相应的方法。
当涉及到多态性的时候,采用了虚函数和动态绑定,此时的调用在运行时确定,是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数。
五、虚函数的代价
(1)带有虚函数的类,每一个类会产生一个虚函数表,用来存储指向虚成员函数的指针,增大类;
(2)带有虚函数的类的每一个对象,都会有一个指向虚表的指针,会增加对象的空间大小;
六、虚函数与派生类
(1)派生类继承的函数不能定义为虚函数。虚函数是希望派生类重新定义。如果派生类没有重新定义某个虚函数,则在调用的时候会使用基类中定义的版本。
(2)派生类中函数的声明必须与基类中定义的方式完全匹配。
(3)基类中声明为虚函数,则派生类也为虚函数。
虚函数总结完毕,今日知识摄入结束,如果有错误的地方还请批评指正,感谢~
明日更新C++的面向对象特性,一起期待一下吧~