c++ - 第14节 - c++中的多态

1. 多态的概念

多态的概念:通俗来说,就是多种形态, 具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票。
再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块...,而有人扫的红包都是1毛,5毛....。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额=random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。


2. 多态的定义及实现

2.1.多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
那么在继承中要 构成多态还有两个条件
(1)子类虚函数重写的父类虚函数(重写:三同(函数名/参数/返回值)+虚函数)
(2)父类指针或者引用去调用虚函数(父类的对象调用不行)。

注:如果不满足两个条件不是多态调用,那么使用对象、指针、引用变量去调用函数,调用的是父类的函数还是子类的函数取决于对象、指针、引用变量的类型;如果满足两个条件构成了多态调用,那么虽然是用父类指针或者引用去调用虚函数,但是到底调用的是父类的虚函数还是子类的虚函数取决于父类指针(引用)指向(对应)的对象类型,如下面代码所示。

2.2.虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数
class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3.虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
虚函数的重写的本质:如果满足多态的两个条件,那么派生类中会自动将父类的所有接口函数声明继承下来,虚函数的重写就是在派生类中重写函数定义实现部分(因为满足多态条件的话父类的函数声明在派生类中是自动继承的,相当于在派生类中写函数默认就是声明和定义分离的)
注意:
1.如果不满足多态的两个条件,那么就不构成多态调用,派生类中不会自动将父类的接口函数声明继承下来,只有满足多态两个条件构成多态派生类中才会自动将父类的所有接口函数声明继承下来。
2.在重写基类虚函数时,如果基类和派生类都将virtual关键字删去,那么就构成了隐藏,在派生类中就调不了基类的该函数了。
3.在重写基类虚函数时,派生类的函数不加virtual关键字时,派生类的函数依旧是虚函数,因为派生类先继承了父类函数接口声明(继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),然后重写的是父类虚函数的定义实现(相当于在函数定义中没有写virtual,但是在函数声明中有virtual,其也是虚函数)。虽然这样也可以构成重写,但是该种写法不是很规范,不建议这样使用。

注:在Pay函数中形参使用的是基类的引用变量(使用基类的指针变量也可以),因为基类的指针可以接收指向基类的对象也可以接收指向派生类的对象

模拟一个买票系统,代码如下所示,左边代码为父类引用去调用虚函数,右边代码为父类指针去调用虚函数

注:switch case语句中,语法的限制case语句里面不支持定义对象,如果要在case语句里面定义对象需要使用大括号,相当于加了一个域,如上图二所示

虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回另一个基类对象的指针或者引用(对象不行),派生类虚函数返回另一个基类对应派生类对象的指针或者引用(对象不行)时,称为协变。(了解)
协变的代码如下图一二所示,下图一是基类虚函数和派生类虚函数返回另一个基类和派生类的指针,下图二是基类虚函数和派生类虚函数返回另一个基类和派生类的引用
2. 析构函数的重写(基类与派生类析构函数的名字不同)
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。如果基类的析构函数不是虚函数,那么派生类析构函数与基类的析构函数构成隐藏关系;如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字(规范起见最好加上),都与基类的析构函数构成重写,如下图所示。

注意:

1.这里虽然析构函数名不相同,但是编译后析构函数的名称统一处理成destructor,所以不违背重写的规则。

2.父类和子类是隐藏关系还是重写关系对于普通对象是没有影响的,但是如果是将子类对象的地址赋值给父类类型的指针变量,如下图所示,那么如果是隐藏关系的话,因为不构成多态,delete ptr释放该子类对象空间的时候,delete会去调用析构函数并且释放空间,因为不构成多态,调用父类还是子类的析构函数取决于指针类型,指针类型是父类类型,delete时调用的是父类的析构函数,delete该子类对象调用的却是父类的析构函数,因为没有调到子类的析构函数,所以没有释放子类成员_name指向的空间,造成内存泄漏。

注:下图所示,将_name强转成void*是因为_name本身是char*类型的,会将字符串打印出来,我们想打印出来的是字符串的首元素地址,所以强转成void*

因此我们期望delete ptr调用析构函数是一个多态调用,使得指针ptr调用父类还是子类的析构函数取决于指针ptr指向对象的类型,这样如果是父类的对象那么ptr就去调用父类的析构函数,如果是子类的对象那么ptr就去调用子类的析构函数。想要delete ptr调用析构函数是一个多态调用只需要满足多态调用的两个条件即可,这里我们只需要在父类的析构函数前面加上virtual关键字即可,如下图所示。

因此,如果设计一个类,可能会作为基类,那么其析构函数最好定义为虚函数。

编译器在编译后将所有析构函数的名称统一处理成destructor的原因:
如果不将所有析构函数的名称统一处理成destructor,面对像上面例子一样的场景,将子类对象的地址赋值给父类类型的指针变量,然后delete父类的指针变量,调用的就是父类的析构函数而无法调用子类的,这种场景的官方的解决方式就是将所有析构函数的名称统一处理成destructor来配合实现多态调用进行处理,处理后delete ptr(父类指针变量)时,就可以统一翻译成ptr->destructor() + operator delete(ptr),因为这里构成了多态,ptr->destructor()会去调用ptr指向对象类型的析构函数,问题得到解决。

2.4.C++11 override final

2.4.1.final关键字(c++11)

关键字final修饰虚函数,表示该虚函数不能再被重写
关键词final修饰类,表示该类是最终类,不能被继承

注:前面讲过如何设计一个不能被继承的类,我们的答案是将类的构造函数设置成private私有即可,这是c++98的实现方式,这种方式巧用了语法规则来间接实现的。在c++11中我们可以给该类后面直接加final,那么该类不能再被继承了,这是一种直接方式。

2.4.2.override关键字(c++11)

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

2.5.重载、覆盖(重写)、隐藏(重定义)的对比


3.抽象类

3.1.概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,抽象类在现实一般没有具体对应的是实体。
派生类继承抽象类后也不能实例化出对象,因为派生类继承了父类的纯虚函数,那么派生类也是抽象类。
派生类只有重写纯虚函数,才能实例化出对象(可以理解为这里必须重写才能将父类虚函数隐藏)。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
抽象类虽然没不能定义对象,但是可以定义指针和引用。派生类中如果进行了虚函数重写,然后将派生类对象的指针或引用赋值给抽象类的指针或引用,那么就满足多态的条件可以进行多态调用(满足多态条件,派生类中会自动继承父类的函数声明,父类的函数声明中有=0,那么派生类中重写的函数在多态调用下也是虚函数,但是虚函数可以被调用不影响)。
抽象类的直接功能:抽象类不能实例化出对象
抽象类的间接功能:要求子类需要重写,才能实例化出对象(强制子类重写来继承接口)
注:纯虚函数可以定义内容,但是对于抽象类的父类来说,其纯虚函数定义内容是没有意义的,因为没有人能调用父类的纯虚函数。在多态调用时,子类重写父类的纯虚函数因为继承了父类的函数声明也变成了纯虚函数,多态时会调用该子类的纯虚函数,因此子类重写父类的纯虚函数其定义是需要的,不是多态调用的情况下为了隐藏父类的纯虚函数而使用自己的函数也需要重写定义。

3.2.接口继承和实现继承

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


4.多态的原理

4.1.虚函数表

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

如下图所示,通过观察测试我们发现b对象是8bytes(32位平台),除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

从下图一可以看出,基类的虚表里面只存虚函数的地址。其实不管是虚函数还是普通函数,编译好了之后都在代码段中,在编译汇编阶段完之后普通函数地址会被放进符号表方便链接,而虚函数的地址会被放进虚函数表里面,基类base类的对象模型如下图二所示。

从下图一可以看出,派生类对象中存储的内容可以分为两部分,一部分是从父类继承下来的,一部分是自己的,派生类Derive类的对象模型如下图二所示。如下图三所示,对比基类类Base类对象模型和派生类Derive类对象模型,我们发现派生类Derive类对象虚表中func1函数地址和基类Base对象虚表中func1函数地址不同,而func2函数地址相同,这是因为派生类中我们重写了func1而func2没有重写。

虚函数重写:语法层概念,派生类对继承的基类虚函数实现部分进行了重写。

虚函数覆盖:原理层的概念,子类的虚表拷贝父类的虚表并进行了修改,覆盖了重写的那个虚函数。

4.2.多态的原理

多态原理剖析:

如下图一所示,两个p->Func1()代码,p是父类的指针对象,从结果第一个调用了父类的Func1函数第二个调用了子类的Func1函数可知到底调用父类还是子类的Func1函数不是由p的类型决定的。多态调用的实现,依靠运行时,去指向对象的虚表中查调用函数地址。

如下图二所示,我们将代码进行修改,给Derive子类写一个普通Func3函数,同样用两个p->Func3()代码进行调用,调用的结果可以看出两次调用的都是父类的Func3函数,这里有两个原因,原因一是父类和子类中的Func3不是虚函数,没有进入虚表,原因二是p->Func1()满足多态调用的条件是多态调用,而p->Func3()是普通调用,多态调用和普通调用的区别如下:

多态调用:运行时决议,运行时查虚函数表确定调用函数的地址。调用时调用指针(引用)指向对象的函数(函数在对象的虚表指针vfptr指向的虚函数表中)

普通调用:编译时决议,编译时确定调用函数的地址。调用时调用指针(引用)类型对应的类的函数。(函数在符号表中)

从汇编代码中观察多态调用和普通调用的区别,如下图所示,编译器检查调用符不符合多态调用,符合多态调用,调用代码就转成对应的多态调用汇编代码,不符合多态调用,调用代码就转成对应的普通调用汇编代码,多态调用汇编代码的行为就是前面讲的运行时决议函数地址并且调用的是指针(引用)指向对象类型的函数,普通调用汇编代码的行为就是前面讲的编译时决议函数地址(如下图所示地址已经有了)并且调用的是调用变量类型所对应的函数。

满足多态的两个条件的原因:

(1)子类虚函数重写的父类虚函数,只有子类重写了父类的虚函数,那么父类里面虚函数表中是父类的虚函数,子类里面虚函数表中是子类重写的新的虚函数,这一才能做到指向谁调用谁。
(2)父类指针或者引用去调用虚函数,只有父类的指针(引用)才能做到既可以指向父类对象也可以指向子类对象。

子类赋值给父类的对象,也可以切片赋值,为什么实现不了多态?

从编译器实现的角度讲(原理层面),编译器就是去检查符不符合多态条件,符合多态的条件就是运行时确定函数地址,不符合多态的条件就是编译时确定函数地址。符合多态的条件函数调用时,使用什么类型的变量去调用,就去调用对应类型指向对象的函数,不符合多态的条件函数调用时,使用什么类型的变量去调用,就去调用对应类型的函数。

从理论上来讲,子类赋值给父类的对象也实现不了多态,因为子类赋值给父类的对象进行切片,对象切片的时候,子类只会拷贝成员给父类对象,不会拷贝虚表指针(如果拷贝虚表指针那就混乱了,面对一个父类对象,对象中就分不清到底是父类的虚表指针还是子类的虚表指针,那么用对象去调用,无论普通调用还是多态调用虚函数,调用的是父类的还是子类的是分不清的,造成了自身的混乱),子类地址赋值给父类的指针进行切片或子类引用赋值给父类引用对象进行切片不同,父类的指针和引用对象都是指向子类中属于父类的部分,不存在拷贝的问题,多态调用指针(引用)指向父类对象调用的是父类虚函数,指向子类对象调用子类虚函数,普通调用,指针不管指向父类对象还是子类对象调用的都是父类虚函数。

如下图所示,代码Base r1=b,将父类Base的b对象拷贝构造给r1,会将虚表指针也拷贝过去,那么此时r1调用Func1和Func3函数都是去r1的虚表中找函数地址进行调用,调用的都是父类的虚函数。代码Base r2=d,将子类Derive的d对象拷贝构造给r2,只会将d的成员拷贝给r2,不会将虚表指针拷贝过去,此时r2对象中除了虚函数指针是父类的虚函数指针(相当于所有虚函数是父类的虚函数),其余成员都是子类对象d相同,那么此时r2调用Func1和Func3函数都是去r2的虚表中找函数地址进行调用,而r2的虚函数表是父类的虚函数表。

4.3.动态绑定与静态绑定

什么是多态?

(1)静态的多态:函数重载

如下图的swap函数,函数重载调用时两次swap调用感觉好像是调用的同一个函数 ,但实际不是,实际中编译链接根据函数名修饰规则找到不同的函数。这里调用哪个函数是在编译阶段决定的,所以叫静态多态。

(2)动态的多态:本节内容讲的

如下图的Func函数,两次Func调用感觉好像是调用的同一个函数,但其实两次多态调用调用的是不同类里面的Func虚函数。这里调用哪个类里面的虚函数是在运行时决定的,所以叫动态多态。

1. 静态绑定又称为前期绑定(早绑定)(编译时决议),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定)(运行时决议),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
3.c/c++中的动态就是运行时,静态就是编译时。
注:静态库是编译链接阶段所链接的库,动态库是程序运行时所链接的库


5.单继承和多继承关系的虚函数表

5.1.单继承中的虚函数表

如下图所示,子类Derive继承了父类Base,那么子类继承了Base类里面所有的函数,其中Base的虚函数指针指向的虚函数表会拷贝给Derive的虚函数指针指向的虚函数表,然后在子类Derive中对Func1进行了重写,定义了Func3将继承的Base的Func3隐藏。定义了Func4虚函数,那么此时子类Derive的对象里面,虚函数指针指向的虚函数表中应该有重写的Func1、继承的Func2和自己定义的Func4三个虚函数地址,可是在调试窗口Derive对象d的虚函数表中只显示了Func1和Func2的虚函数地址,我们通过内存窗口找到虚函数指针指向的地址,我们发现虚函数表中有三个地址,那么第三个地址是否就是Func4的虚函数地址呢?

注:如果之前写过类似的代码,运行后修改了小部分代码再运行,在内存中vs编译器有时候可能没有把之前的代码清除干净,打印结果出问题甚至程序崩溃,这是vs编译器的BUG,我们点击生成选项,清理解决方案再重新生成解决方案即可,如下图所示

我们取内存,打印并调用,确认是否是Func4。

首先我们写一个打印虚表的函数,虚表是一个函数指针数组,里面的虚函数类型是我们定义的Func系列的函数类型为void(*)(),我们将Func系列的函数类型进行类型重定义为V_FUNC,代码typedef void(*)() V_FUNC是不行的,对函数指针重定义要将重定义的内容放在中间,所以代码为typedef void(*V_FUNC)()。函数的形参为V_FUNC* a用来接收指向虚表的指针。在vs编译器中,虚表的末尾会放一个空指针,所以我们循环的截止条件可以写成a[i]!=nullptr,等于空指针时打印结束。指向虚表的指针内容是对象内存的前四个地址的数据,我们取对象d的地址强转成int*类型然后再解引用就取到了前四个字节内容,也就是虚表的指针内容,但是此时解引用后类型是int类型,应该强值转换成V_FUNC*类型的指针,然后将其传给打印函数即可,如下图所示。

注:如果是在64位编译器下,那么指向虚表的指针内容是对象内存的前八个地址的数据,我们就不能再强转成int*类型然后再解引用就取前四个字节内容了,应该强转成double*等八字节类型的指针,然后再解引用取前八个字节内容。

这里我们打印一下指向虚表的指针内容,并且有了函数的地址,我们就可以去调用该函数了,如下图所示,这里就证明了Derive子类对象的虚函数表中第三个地址就是Func4的虚函数地址。

这里监视窗口里面的Derive子类对象的虚函数表没有显示Func4,编译器的监视窗口是故意隐藏了这个函数,可以认为是一个BUG。

vs监视窗口看到的虚函数表不一定是真实的,可能被处理过。

问题:虚表存在哪个区域?

答:区域分为栈、堆、静态区(数据段)、常量区(代码段)。同一类型的对象共用一个虚表,这个类型所有对象的虚函数指针都指向该虚表,因此虚表需要存在一个可以长期存储的区域。

栈区是用来建立栈帧的,栈帧运行结束就销毁了,虚表不会不停的销毁,因此不可能是栈区。堆区需要自己申请释放空间,因此不可能是堆区。放在静态区和和常量区都合理,但是静态区放的是全局数据和静态数据,虚表是一个函数指针数组,应该不会放在静态区,所以推测放在常量区。

我们用下面的代码验证一下,发现虚表的地址和常量区(代码段)变量的地址离得近,所以虚表应该是存在常量区(代码段)的,并且可以看出不管是虚函数还是普通函数都是存在一起的,都存在常量区(代码段)中,只不过虚函数要把其地址放在虚表中。

5.2.多继承中的虚函数表

子类Derive既继承了父类Base1又继承了父类Base2,如下图所示,从调试窗口中,可以看出子类Derive中有两个虚表,一个是父类Base1的虚表另一个是父类Base2的虚表,子类Derive自己也有虚函数func3,func3虚函数在调试窗口中没有显示(前面提到的编译器监视窗口故意隐藏了这个函数),那么func3是放在哪个虚表中呢?

和之前一样我们打印一下子类Derive中Base1部分的虚表和Base2部分的虚表来观察,Base1部分的虚表指针就在Derive的头部,Base2部分的虚表指针在Base1虚表指针(Derive头部)跳过Base1部分的位置,使用代码(VFPTR*)(*(int*)((&d + sizeof(Base1)))跳过是不行的,&d的类型是Derive*,只要加上1就跳过了整个Derive空间,我们应该强转成char*类型的,代码为(VFPTR*)(*(int*)((char*)&d + sizeof(Base1))),如下图一所示,可以看见func3是存在第一个继承的父类Base1的虚表中,Derive子类的模型图如下图二所示。

如下图一所示是上面代码打印的内容,在子类Derive中我们重写了func1虚函数,那么子类Derive中应该只有一个我们重写的func1函数,但是为什么无论调试窗口还是我们打印出来的内存中两个Base部分的func1函数地址不同呢?(在g++编译器下面直接存储的就是func1的地址,打印出来的内存中两个Base部分的func1函数地址相同)我们直接打印子类Derive中的func1虚函数地址,如下图二所示,我们发现真实的Derive中的func1虚函数地址和base1、base2部分的func1虚函数地址都不一样。

我们使用局部变量f分别获取Base1部分和Base2部分中的func1虚表地址(f是存在栈帧里面的),然后f()进行调用,Base1部分和Base2部分中的func1函数调用情况如下图所示,从图中可以看出Base1的虚表中func1函数地址并不是真正的子类Derive的func1函数地址,编译器去Base1的虚表中func1函数地址去找到调用真正的func1函数地址并调用,Base2的虚表中func1函数地址也不是真正的子类Derive的func1函数地址,编译器去Base2的虚表中func1函数地址去找到调用真正的func1函数地址并调用,在寻找的途中将ecx也就是this指针进行了修正(如果调用的是Base2的虚表中func1函数,那说明此时this指针指向的是base2部分,this指针应该指向Derive的地址也就是Base1部分的地址,所以要进行修正)。

经过上面深层次的剖析来加深理解,那么我们就可以理解,下图所示ptr1->func1()和ptr2->func1()虽然调用的都是子类Derive的func1函数,但是调用的过程是不一样的。

这里在调用Derive对象Base2虚表中func1时,是Base2指针ptr2去调用,但是这时ptr2发生切片指针偏移,指向Derive对象Base2部分的地址,而ptr2->func1()调用的应该是Derive对象的func1函数,因此中途就需要修正存储this指针的ecx的值,使得this指针指向Derive对象的地址。

我们将子类Derive对象d的地址分别赋值给Base1父类的指针、Base2父类的指针、子类Derive的指针,将三个指针数据进行打印,如下图一所示,我们发现结果不一样。打印结果不一样的原因是发生了切片,三个指针的指向和解引用得到部分的模型图如下图二所示。

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

如下图所示,父类A有虚函数func,B和C继承A并重写了A的虚函数func,D继承了B和C,如果是普通菱形继承没有问题,但是菱形虚拟继承运行报错。对于菱形虚拟继承,D类的对应模型如下图二所示,B和C中都存的是各自对于A的偏移量,那么B和C都对虚函数func进行了重写,A中应该存储B的func重写还是C的func重写呢,这里是不明确的,因此报错。

这里只要在D类中对func函数进行重写,那么A里面就有明确的虚函数func了,运行就没有问题了,如下图所示。

如下图所示,父类A有虚函数func,B和C继承A并重写了A的虚函数func,B有自己的虚函数func1,D继承了B和C并重写了A的虚函数func,此时D类的对应模型如下图二所示,值得注意的是,因为B中有自己的虚函数,所以B部分有虚表指针指向自己的虚表,B的虚基表中第一个值是距离B自己虚表指针的偏移量,第二个值是距离A的偏移量,C没有自己的虚函数,所以C部分没有虚表指针,D对A的虚函数进行了重写,A部分有重写的虚函数,所以A部分有虚表指针。

 


6. 继承和多态常见的面试问题

6.1.概念查考

1.下面程序输出结果是什么? ()
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
#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

解析:初始化的顺序和初始化列表的顺序没有关系,初始化的顺序和声明的顺序有关系,声明的顺序是ABCD依次声明的,那么初始化的顺序也是 class A class B class C class D
2. 以下程序输出结果是什么()
A: A->0         B: B->1         C: A->1         D: B->0         E: 编译出错         F: 以上都不正确
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
解析:上面代码中派生类B类继承了父类A类的test函数,父类A类的test函数中形参是有默认的A*类型的this指针,B类继承下来也继承了形参的默认的A*类型的this指针,代码p->test()调用B类里面继承的test函数,将B*类型的指针对象p传给test形参的A*类型的this指针,此时test函数里面A*类型的this指针指向new出来的B对象,test函数里面func()编译器会翻译成this->func(),因为this指向的对象B是B类型的,所以调用的是派生类B里面的func函数。因为虚函数的重写本质是派生类先将父类的所有接口函数声明部分继承下来,然后在派生类中重写的是函数定义实现部分,所以B中的func函数的声明继承了父类func函数的声明,在B类的func中没有写virtual但也是虚函数(相当于在函数定义中没有写virtual,但是在函数声明中有virtual,其也是虚函数),这样就符合多态调用的两个条件,那么B类的func函数有函数声明有函数定义,而缺省值使用的是函数声明的缺省值,也就是继承的A类func函数声明的缺省值,所以func函数中打印出来是B->1。
注:这里test函数子类没有重写,子类是继承父类的test函数,是一个继承调用而不构成多态调用,因此父类的test函数前面加不加virtual都可以没有影响。
补充:如果题目改成p->func(),那么打印的结果就是B->0,因为代码p->func()中变量p的类型是子类B的指针类型,不构成多态的条件,不会触发多态的接口(函数声明)继承,只有构成多态的条件才会触发接口(函数声明)继承。这里子类B中会继承A的func和test函数,而func函数因为同名所以会对继承父类的func函数进行隐藏,代码p->func()就是普通的调用,打印B->0

6.2.问答题

1. inline函数可以是虚函数吗?
答:可以,但是一个函数既保持内联又保持虚函数是不可能的,因为内联的函数没有地址,而虚函数要求把函数地址放在虚表中。如果是普通调用,inline的虚函数,编译器忽略虚的属性,将函数当作普通内联函数;如果是多态调用,inline的虚函数,编译器忽略inline的属性,这个函数就不再是inline,因为内联函数是没有地址的,而虚函数地址要放到虚表中去。

注:前面讲过想要观察内联的情况,在debug版本下需要进行设置,具体设置见c++入门章节,这里不再赘述。

2. 静态成员可以是虚函数吗?
答:不能,编译器强制检查static和virtual不能同时使用。原因是静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
3. 构造函数可以是虚函数吗?
答:不能,因为对象的虚表和对象中的虚表指针是在构造函数初始化列表阶段才初始化的,虚函数的意义是多态,多态调用时到虚函数表中去找,构造函数之前还没初始化,无法去找。
4. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数,原因讲过,见本节前面的内容。
5. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
6. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的(虚函数表在对应类的构造函数初始化),一般情况下存在代码段(常量区)的。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值