• 【C++】多态


    目录

    一、多态的概念

    二、多态的定义及实现

    2.1 多态的构成条件

    2.2 虚函数

    2.3 虚函数的重写

    2.4 多态的使用

    2.5 析构函数重写

    2.6 C++11 override 和 final

    三、抽象类

    3.1 抽象类的概念

    3.2 接口继承和实现继承

    四、多态的原理

    4.1 虚函数表

    4.2 多态的原理

    4.3 动态绑定和静态绑定

    五、单继承和多继承关系中的虚函数表

    5.1 单继承中的虚函数表

    5.2 多继承中的虚函数表

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

    六、多态知识点总结

    6.1 语法

    6.2 原理

    七、继承和多态常见的面试问题


    一、多态的概念

    多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)

    多态的作用:消除类型之间的耦合关系。

    通俗来说,就是多种形态,具体点就是去完成某个行为当不同的对象去完成时会产生出不同的状态

    现实中,关于多态的例子不胜枚举。比方说按下 F1键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

    二、多态的定义及实现

    2.1 多态的构成条件

    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如 Student 继承了 Person。Person 对象全价买票,Student 对象买票半价。

    那么在继承中要构成多态还有两个条件

    1. 必须通过基类的指针或者引用调用虚函数。
    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

     2.2 虚函数

    虚函数即被virtual 修饰的 类成员函数称之为虚函数。

    1. class Person
    2. {
    3. public:
    4. virtual void BuyTicket()
    5. {
    6. cout << "买票-全价" << endl;
    7. }
    8. };

    2.3 虚函数的重写

    虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

    注意:

    在重写基类虚函数时,派生类的虚函数在不加 virtual 关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性),但是这种写法不规范,不建议这样使用,如下:

     2.4 多态的使用

    关于多态的两个条件:

    1. 虚函数重写
    2. 父类指针或引用去调用虚函数

     刚刚我们完成了重写,接下来就是让父类指针去调用虚函数。

    这里我们写一个 func 函数。

     我们将程序运行起来观察结果。

    但是如果出现以下情况,多态的效果就无法实现。

    1.  如果我们传入的不是指针或引用,直接使用父类接收。

    2. 如果我们将父类的 virtual 删除 —— 即不符合重写中虚函数的要求

     

    3. 将父、子类函数中的参数进行改变——即不符合重写规定

     为解决这个问题,有两个解决方法。

    1. 删除子类的中函数的 int,构成重写(即恢复原来的形式)

    2. 将父类中虚函数也添加 int 参数,构成重写,传参要传入 int,如下图:

     

    接下来就是三个特例,同样也可以构成多态。

    1. 子类虚函数可以不加 virtual,依旧构成重写(最好加上)。

    2. 重写的协变。返回值不同,要求必须是父子关系的指针或引用

    3.析构函数的重写(基类和派生类析构函数名不相同)。

    特例一:子类不加 virtual 

    特例二:重写的返回值协变

     一个返回 Person* ,一个返回 Student* ,也可以构成多态。

    特例三:析构函数的重写,虽然函数名不同

    2.5 析构函数重写

    如果基类的析构函数为虚函数,与此时派生类析构函数只要定义,无论是否加 virtual 关键字,都基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。

    虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理称 destructor ,这就符合了函数名称相同构成函数重写的条件

    如果,我们将不将父类中的析构函数定义为虚函数。就会出现很大的问题

     我们来分析一下原因:

    此时不符合多态,编译期间就确定了函数的地址。所以父类指针没有去虚表中寻找虚析构函数的地址(也就是子类的析构函数),而是直接调用了自身的析构函数(父类的析构函数)。

    如果符合了多态,那么在调用时,该指针指向父类部分,就会去调用虚表中重写的子类虚函数,然后调用子类的虚析构函数,调用完成了编译器自动调用父类的析构函数(这一步操作是自动完成)。

    所以,如果没有实现多态,子类就无法调用自身的析构函数。

     为了验证以上分析,这里演示一下编译器执行的步骤。

    即。建议析构函数定义为虚函数。

    2.6 C++11 override 和 final

    从上面可以看出,C++对函数重写的要求比较严格,但有些情况下由于疏忽,可能会导致函数名字字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来 debug 会得不偿失,因此:C++11提供了 override 和 final 两个关键字,可以帮助用户检测是否重写。

    1. final:修饰虚函数,表示该虚函数不能被重写。

    2. final:修饰类,表示一个类不能被继承。

    3.override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

    三、抽象类

    3.1 抽象类的概念

    在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

    抽象类强制了我们重写虚函数。是接口的体现。

    3.2 接口继承和实现继承

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

    四、多态的原理

    4.1 虚函数表

    这里有一道常见的笔试题,大家想到答案是 8 了吗

     原因是,Base 类中不仅仅存放了一个 _b 成员变量,还有一个指向 Func1 的指针。

     

    通过观察我们发现除了 _b 成员,还多一个 __vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个与平台有关),对象中的这个指针我们呢加偶走虚函数表指针(virtual function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称为虚表。

    为了深层次的研究,针对上面的代码做出以下改造:

    1.增加一个派生类Derive 去继承 Base

    2. Derive 中重写 Func1

    3.Base 再增加一个虚函数 Func2 和一个普通函数 Func3

    1. class Base
    2. {
    3. public:
    4. virtual void Func1()
    5. {
    6. cout << "Base::Func1()" << endl;
    7. }
    8. virtual void Func2()
    9. {
    10. cout << "Base::Func2()" << endl;
    11. }
    12. void Func3()
    13. {
    14. cout << "Base::Func3()" << endl;
    15. }
    16. private:
    17. int _b = 1;
    18. };
    19. class Derive : public Base
    20. {
    21. public:
    22. virtual void Func1()
    23. {
    24. cout << "Derive::Func1()" << endl;
    25. }
    26. private:
    27. int _d = 2;
    28. };
    29. int main()
    30. {
    31. Base b;
    32. Derive d;
    33. return 0;
    34. }

    观察上图,得出结论

    1. 派生类对象 d 中也有一个虚表指针,d 对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
    2. 基类 b 对象和派生类 d 对象虚表是不一样的,这里我们发现 Func1 完成了重写,所以 d 的虚表中存的是 重写的 Derive::Func1,所以虚函数的重写也叫做覆盖。覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。 
    3. 另外 Func2 继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
    4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放另一个 nullptr。

    总结:派生类的虚表生成

    1. ①先将基类中的虚表内容拷贝一份到派生类虚表中。
    2. ②如果派生类重写了基类中的某个虚函数,用派生类自己的虚函数覆盖表中基类的虚函数
    3. ③派生类自己新增的虚函数按其再派生类中的声明次序增加到派生类虚表的最后。

    这里还有两个容易混淆的问题:

    虚函数存在哪?虚表存在哪?

    答:虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样,都是存在代码段的,只是它们的指针又存到了虚表中。

    另外对象中存放的不是虚表,而是虚表指针,虚表也是存在代码段的。

    4.2 多态的原理

    研究完了虚函数表,接下来就来剖析多态的原理。

    代码如下:

    我们打开调试,观察父类、子类中虚表指针以及虚函数表中的存储的地址。发现,重写了的函数(BuyTicket)在虚函数表中地址不同,而没重写的函数(Func)的地址相同。

     其调用的逻辑图:

    下图是其在监视窗口中函数调用的走向,跟随着箭头观察即可。

    当使用父类的引用或指针指向子类对象时,该父类的指针只会指向被继承的那块空间。则只会调用子类继承下来的父类部分。而此时如果子类去调用虚函数,而子类中对虚函数进行了重写,那父类中的虚函数表存放的就是被子类重写了的函数。此时就会去调用子类重写了的函数。

    我们再来观察,存在多态和不存在多态时,底层的汇编代码:

    其实编译器也是会检查当前符不符合多态,如果符合多态,就去调用子类中重写了的函数,如果没有重写,就直接去调用父类的函数。

    构成多态的调用,运行时到指向对象虚表中找调用虚函数地址,所以 p 指向谁就调用谁的虚函数。

    不构成多态则是普通调用,编译时确定函数地址,直接进行调用。

    4.3 动态绑定和静态绑定

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

    五、单继承和多继承关系中的虚函数表

    需要注意的是在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型。

    5.1 单继承中的虚函数表

     同一个类的对象会共用一个虚表

    那子类的虚表是不是同一个呢?如下图: 

    所以,不管是否完成重写,子类虚表和父类虚表都不是同一个。


    那接下来还有一个问题,现在有一个只存在子类中的虚函数,那这个函数的地址会被存放到虚函数表中吗?那放在那个表中呢?

    通过我们观察在内存中的观察,发现,这 0x010115aa 可能就是 func()的地址,只不过VS的调试窗口显示 vfptr 中没有func(),可能这就是 VS的一个BUG了。

    接下来为了验证其到底是不是 BUG,我们可以通过打印地址来看看,虚函数表中到底有没有 func 函数的地址。代码如下,然后我们来解释代码:

    我们写的这个函数能通过虚表指针正常调用,也就是说,子类增加的虚函数是会被添加到自身的虚函数表中的。然而 VS 调试中,会显示子类重写的虚函数和父类中没重写的虚函数,不显示是子类增加的虚函数,这算是 VS 调试的一个小 BUG 。

     现在我们来剖析以上的关键代码。

     函数指针只能这样 typedef ,这是语法要求,我们这样定义是为了后面调用函数时增强代码的可读性。

    重点如下!!!(深度考验C语言指针)

    因为对象前4个字节存放的是虚表指针(_vfptr),所以我们要取到 p1 的前4个字节。我们取出 p1 的整体地址,然后访问到前四个字节,即,将其转化为(int*),通过解引用取出虚表指针的地址(整形的形式),再将该整形转化为一个函数指针数组。

    这样我们就可以直接通过一个类似遍历数组元素的函数来访问到虚函数表中存放的函数。

    我们传入的实参是一个函数指针数组(虚表指针_vfptr),所以我们要使用函数指针数组接受,通过下标即可访问函数表中的函数。

     这里注意,在虚表结束处放入 nullptr 只是 VS 编译器的做法,在 g++、clang等编译器下可能不适用,这时就要我们显示传入函数的个数了。

    5.2 多继承中的虚函数表

    上面我们研究的情况都是单继承,那多继承下,虚表指针的情况是什么样的呢?

     关于为什么大小是20,其实非常简单

     好的,这里我们再研究下一个问题:

    上面说了 VS 调试不显示子类新增加的虚函数,那如上我们子类有一个新增的虚函数,此函数会被放在Base1的虚函数表中呢?还是Base2的虚函数表中呢?

    接下来我们通过函数指针强行调用虚函数来看看是到底是存放在哪的。

     发现,Base1中的虚函数表中存放着子类新增加的虚函数 func 。


     再让我们看看 Base2 中的虚函数表中存放着的函数吧。

    首先第一个问题就是我们如果取到 Base2中的虚函数表呢?

     这样必定的不行的。我们先来观察 对象d 其结构。

     

     所以说,我们要让 ptr 指向 Base2 才行,即跳过 Base1 的空间。

    这样行吗?我们看看结果。

     程序调用一个函数就结束了,说明我们的代码必定是有问题的。

    问题在于:此时 &d 是一个 int* 型指针,加上 sizeof(Base1)后,直接就跳了 4 * sizeof(Base1) 的空间。所以,在加上sizeof之前,我们要先将其转化为 char* 型指针,才能让其正确的指向 Base2 的起始位置。

    上面这个方法就解决了这个问题,但是我们还有另外一个方式——切片。

     现在我们来观察其中的现象,发现 Base2 的指针中并没有存放子类增加的 func3()。

    所以,得出结论,子类新增加的虚函数会被存放到第一个父类的虚函数表中。

    此时我们便可以画出其对应的关系图:

    发现,Base1 和 Base2 中 存放 func1 的地址不同!但是明明是同一个函数,调用的地址不同,为什么最后结果调用的是相同的?

    这就是编译器的一个精妙设计,如果想研究清楚需要去研究 VS 是如果调用函数的(汇编层次)。

    首先我们重代码的层次来看一下,虚函数表中存放的地址和实际函数的地址是怎样的。

    我们来打印一下子类中重写的虚函数地址。普通函数的地址我们直接输出函数名即可,但是类中的函数我们要指定类域。

    并且,关于成员函数我们打印其地址要加上 & 地址操作符才能正常的输出(语法规定)。

    这样就可以打印成功了。

    然后我们将Base1、Base2 以及子类重写的虚函数地址都打印一下:

    发现,其实不止两个虚函数表中同一个函数的地址不同,其实两个虚函数表中函数的地址与直接打印函数的地址也都不相同。

     接下来我们通过调用来检查这三个地址最后调用的是否是同一个函数。希望大家耐心观察,其实并不复杂

    即使多继承中两个父类虚函数表中存放重写的函数地址不同,但是仍可以调用到同一个函数。

    都是重写的函数,但是两份虚函数表中存放的地址不同 。因为其存放的是jmp的地址。

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

    实际中我们不建议设计出菱形继承以及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模型,访问基类成员有一定的性能损耗。这里有两篇不错的文章:

    1.C++虚函数表解析

    2.C++对象的内存布局

    现在我们粗略的看一下虚拟继承。因为其层次太深,过于复杂,实际中也很少使用。

    如果我们在 A 中添加一个虚函数,并在 B、C 类中进行重写,Dmp中不重写。就会出现以下情况。

    问题肯定在于我们没有将 Dmp 中的 func1 函数进行重写,现在我们对其进行重写,看还是否报错。

    那此时我们使用 VS 开发者工具来看看类 Dmp 的结构。

     观察 d 对象中的虚函数和虚基指针。

    如果 B 、C 类对 func1 进行了重写,而 Dmp 中不进行重写,那 A 的虚函数表中就不知道应该存放B 的 func1 还是 C 的 func1,所以我们需要菱形继承的类主动重写 func1 函数,使继承下的 A 类中存放 Dmp 的 func1 ,这样才能通过编译。

    六、多态知识点总结

    6.1 语法

    1.多态的概念及条件

    1. 必须通过基类的指针或者引用调用虚函数。
    2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

    2.重写的概念及条件

            虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名称、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

    6.2 原理

    1.虚表是什么?

            每个有虚函数的类或者虚继承的子类,编译器都会为它生成一个虚拟函数表(简称:虚表),表中的每一个元素都指向一个虚函数的地址。

    2.多态的原理是什么

            不同的对象调用相同的方法时,因其重写了基类中虚函数,所以调用时会根据虚表找到自身重写的虚函数并调用,实现了不同对象调用同一个函数,实现效果不同。

    3.普通调用的编译时决议和多态调用的运行时决议的区别是什么?

           普通调用是编译时就将地址确定,运行时直接去调用,属于编译时决议。

            而多态调用是编译完后通过指令,去对象中虚表里去找虚函数运行,是运行时去找,找到了才调用,运行时决议,即运行时确定调用函数的地址。

    七、继承和多态常见的面试问题

    1.什么是多态?

    1. 多态的定义:指允许不同类的对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。(发送消息就是函数调用)
    2. 实现多态的技术称为:动态绑定(dynamic binding),是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。
    3. 多态的作用:消除类型之间的耦合关系。
    4. 现实中,关于多态的例子不胜枚举。比方说按下 F1键这个动作,如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;如果当前在 Word 下弹出的就是 Word 帮助;在Windows 下弹出的就是 Windows 帮助和支持。同一个事件发生在不同的对象上会产生不同的结果。

    2.什么是重载、重写(覆盖)、重定义(隐藏)?

    (1)重载要求:函数名相同,参数不同(个数、类型)、常函数也可以作为重载判断。注意返回值不是重载的判断标准。

    (2)重写(即覆盖)要求:要求基类函数为虚函数,且基类函数和派生类函数名、参数等相同。

    (3)隐藏(即重定义)要求:子类重新定义父类中有相同名称的非虚函数(参数列表可以不同)。

    (4)重写和隐藏都是发生在基类和子类中。

    3.多态的实现原理?

    多态的实现分为静态多态和动态多态的实现。
    1、静态多态主要是同名函数的重载,在编译的时候就已经确定。编译器会根据函数实参的类型(可能会进行隐式类型转换),来确定具体调用哪个函数,如果有对应的函数就调用该函数,否则会出现编译错误。
    2、动态多态主要是父子类同名函数的覆盖,运行时的多态,是用虚函数机制实现的,在运行期间动态绑定。编译器在编译的时候,会为每个包含虚函数的类创建一个虚表和虚表指针。该表是一个一维数组,在虚表中存放了每个虚函数的地址。程序运行时,会根据对象的实际类型来初始化虚表指针,让虚表指针指向所属类的虚表。在调用虚函数的时候,能够根据函数地址找到正确的函数。

    举个例子:一个父类的指针指向一个子类对象,使用父类的指针去调用子类中重写了的父类中的虚函数的时候,会调用子类重写过后的函数,在父类中声明为加了virtual关键字的函数,在子类中重写的时候不需要加virtual也是虚函数。

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

    内联函数的特征:直接在调用处进行展开,避免了指令的来回跳转,加快程序执行速度。所以说内联函数是没有地址的。

    但是知道虚函数是有地址的,其地址存放在虚函数表中。

    所以,虚函数和内联函数的特性是相斥的,一个函数是不能同时满足这两种特性的。

    但是,我们知道,inline 关键字只是建议,如果编译器绝认为此函数不能成为内联函数,会忽视该关键字。

    所以说,inline 的函数是可以为虚函数的,其并没有成为内联函数。

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

    static 函数不能是虚函数,因为static 函数没有 this 指针, 可以直接使用类域指定调用,但虚函数是为了实现多态,多态是根据指向的对象在运行时去虚表找对应函数进行决议。

    static 成员函数是在编译时决议,没有进行虚表查找该动作,所以编译器会直接禁用此行为。

     

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

    构造函数不能是虚函数,virtual 函数是为了实现多态,运行时去虚表找对应虚函数进行调用,对象中虚表指针都是构造函数初始化列表阶段才能初始化的

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

    析构函数可以是虚函数,且常常是虚函数。因为多态的作用,其会因调用的对象调用其对应的析构函数,非常符合多态的使用场景。

    8.拷贝构造函数可以是虚函数吗?

    拷贝构造不可以,因为拷贝构造其实就是构造函数的一种重载,构造函数不行,拷贝构造自然也不行。

    因为此时 a2 调用虚拷贝构造函数,会去虚函数表中进行查找,而 a2 是被构造的对象,没有经过初始化列表阶段进行虚指针的初始化,自然不能调用虚指针,一个矛盾的调用,编译器自然会进行报错。

    9.赋值运算符重载 operator= 可以是虚函数吗?

    语法上可以,但是实际无意义,因为只能做到父类赋值给子类。看下图即可。

     更改如下:

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

    虚函数不构成多态调用,一样快,

    虚函数构成多态的调用,普通函数快,因为多态调用是运行时去虚表中寻找虚函数地址。

    11.虚函数表是什么阶段生成的,存在哪里?

    构造函数初始化的是虚函数表指针(虚指针),对象存的也是虚函数表指针。

    虚函数表存放常量区(正文代码区),我们接下来做一个实验,直接通过地址来查找虚函数表指针在哪块区域。

     我们将得到的数据根据其类型进行划分:

    12.C++菱形继承的问题?虚继承的原理?

    菱形继承的问题:菱形继承主要有数据冗余和二义性的问题。由于最底层的派生类继承了两个基类,同时这两个基类有继承的是一个基类,故而会造成最顶部基类的两次调用,会造成数据冗余及二义性问题。如下图所示,在Assistant的对象中Person成员会有两份。

    菱形继承的原理:

    13.什么是抽象类?抽象类的作用?

    抽象类:含有纯虚拟函数的类称为抽象类,它不能实例化生成对象;

    作用:抽象类强制重写了虚函数,另外抽象类体现处了接口继承关系。

    选择题一:

     答案: B

    解析:

    1. P 调用 A类中的 test(),此时 test() 调用 func() ,即 this -> func(),此时条件构成多态。因为传入的是子类的指针,即 p 是指向 B 的,所以去调用子类中的 func() 函数。

    2. 因为普通函数继承是实现继承,而虚函数重写是接口继承,重写实现。所以 this -> func() 是仅仅是调用接口,val 仍然是 1。

    为了验证第二点,即使子类中 val 不给缺省值,打印出的仍然为 1 。

    变式:

     

    题二:

    求打印出的内容

    答案:

     解析:

    1. 按照声明的顺序初始化,因为 A 是最先被继承下来的。
    2. 虚拟继承下来的类,自会构造自身一次。

    题目三:

    求打印出的内容

    答案:

    GIF 动画 

    将 this 标出,便以理解。

    这里的难点是当 new B时,B 构造函数中的调用 test() 语句时,test()函数回转调用子类的 func ()函数了。

    原因:

    因为B构造函数中语句为 this->test(), 调用时将 子类的 test 进行赋值给父类的 this ,并且父类指针 this 指向子类对象,形成多态的条件。

    父类的 this 指针调用 func() 函数会去虚函数表中寻找 func 函数,发现子类进行了重写,于是就去调用了子类重写的 func() 函数。 

    题目四:

    原因:

    调用 f 指针调用 f1 函数正常运行,调用 f2 函数会崩溃.

    因为类成员函数是存储在公共代码区,所以f1直接去代码区调用F类中的f1函数;而f2函数是虚函数,所以调用f2要去指向对象的虚函数表中找,但是f指向的nullptr,所以程序崩溃。

  • 相关阅读:
    Vue+SpringBoot项目分离部署踩坑记录
    休闲娱乐 - PS4游戏 Journey 风之旅人
    算法通过村第七关-树(递归/二叉树遍历)白银笔记|递归实战
    docker部署ElasticSearch过程记录
    【微机原理笔记】第 7 章 - 常用数字接口电路
    (2)数据库mongodb 终端 和 vscode创建数据库 数据导入导出
    curl 和 wget 的使用和区别
    使用Java实现一个简单的贪吃蛇小游戏
    基础篇010.2 STM32驱动RC522 RFID模块之二:STM32硬件SPI驱动RC522
    单点登录和分布式登入用户状态储存
  • 原文地址:https://blog.csdn.net/Brant_zero/article/details/127547303