一、面试题:
分析如下代码,选择正确答案:
答案选:【B】
首先我们看到B继承了A,B的func函数重写了A的func函数,main函数里面,B对象p调用test函数,而test函数时继承A类的,所以test函数的形参this指针是A类的this指针,所以test函数里面调用func是A类this指针调用的func,所以满足父类指针调用的条件,又因为func函数又构成重写关系,所以这里是构成多态关系的,又因为test形参的A类指针是由B类对象p调用传参进行赋值的,所以该A类this指向的是B类this指针,所以根据多态的原理,test函数里面会调用B类的fun函数,又因为func函数时重写关系,重写是实现重写(即只会重写实现部分吧),函数声明部分是直接照搬父类的,所以val的缺省值还是父类的1,所以打印“B->1”,所以选B。
二、虚函数表和虚函数表指针
1、本类虚函数表:
我们观察一个现象:
class Person { protected: int _a; public: virtual void fun1() { cout << "调用父类的fun" << endl; } virtual void fun2() { cout << "虚表的第二个值" << endl; } ~Person() { cout << "父类析构" << endl; } }; int main() { Person P; return 0; }
问题:
我们知道一个类对象里面只会存储成员变量,不会存储成员函数,成员函数是放在一个公共区。那我们看上述Person里面只有一个成员变量 _a ,为什么监视窗口会多显示一个vfptr变量呐?
解答:
(1)、这个vfptr就是虚函数表指针(简称虚表指针),当类里面有虚函数时,实例化对象后就会自动多出这个变量,名字叫vfptr是“virtual function pointer”的缩写,这是一个函数指针数组。
(2)、vfptr的值就是虚函数表(简称虚表)在内存中的存储地址,类里面有几个虚函数,该表里面就有几个值,如上述代码中只有一个虚函数,所以虚表里面只有一个值,就是该虚函数的地址。若我再增加一个虚函数,则虚表就会有两个值,如下:
(3)、并且虚表里面的存储值的顺序就是虚函数从上往下声明的顺序。
2、子类的虚函数表及其虚表的相关规则
首先我们看如下代码:
class Person { protected: int _a = 0; public: virtual void fun1() { cout << "调用父类的fun" << endl; } virtual void fun2() { cout << "虚表的第二个值" << endl; } }; class Student :public Person { protected: int _s = 1; public: virtual void fun1() { cout << "调用子类fun" << endl; } }; int main() { Person P; Student S; return 0; }
我们Student类继承了Person类,并且重写了fun1函数,继承了fun2函数
我们会观察到以下几点现象:
(1)、子类对象S中也有一个虚表指针,S对象由两部分构成,一部分是自己的成员,一部分是父类继承下来的成员。
(2)、我们可以发现,子类的虚表指针是继承父类那部分的虚表指针(但要注意:子类虚表指针不是父类的虚表指针,看值也可以知道)。如果没有重写父类的虚函数,那么虚表中对应的函数地址也是原来的地址,如果重写了父类的虚函数,那么就会把父类虚表中的对应的函数地址覆盖掉。
所以虚函数的重写也可以叫覆盖:覆盖就是指虚表中虚函数的覆盖
重写是语法的叫法,覆盖是原理层的叫法。 (3)、虚函数表本质是一个存虚函数指针的指针数组,一般情况(如vs编译器中)这个数组最后面放了一个nullptr,可以通过内存窗口查看 这样设置,我们可以用来打印虚函数表,作为循环判断条件。 (4)、注意若我们子类自己定义了虚函数,也会放进继承父类那部分的虚表里面,只是vs编译器的监视窗口看不见,但可以使用内存窗口看见。 (5)、通常虚表指针是设置在对象的前四个/八个字节,或者最后四个/八个字节,若放在前面,我们想要拿到这个虚表指针的话,就可以用int*指针,运用截断机制拿到虚表指针的值。(6)、满足多态以后的函数调用是在运行起来以后到对象中去找的 。 不满足多态的函数调用时编译时确认好的(即普通函数调用,去符号表里面找)。 (7)、同一个类的不同对象,有不同的虚表指针,但指向的都是同一个虚表。 (8)、虚表是编译时生成的,对象里面的虚表指针是在构造函数的初始化列表最开始就赋值的。int main() { Student S; Person P; Person* PP = &P; int* ptr = (int*)PP; printf("取到虚函数指针的值:%p", *ptr); return 0; }
3、虚表和虚表指针的相关问题:
(1)、虚函数存在哪的?虚表存在哪的?虚表指针存在哪?
1、虚函数和普通函数一样,因为函数都会编译成对应的指令,所以都存储在代码段区域;
2、vs2019中,虚表存储在常量区,可以通过比较地址的相似度进行判断;
3、虚表指针存储在实例对象本身的内存空间中(对象的前四个字节或最后四个字节)。
三、动态绑定与静态绑定
(1)、静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如: 函数重载 (2)、动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为 动态多态 。(指向谁调用谁的函数)。
四、抽象类
1、概念:
(1)、在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做 抽象类 (也叫接口类),抽象类不能实例化出对象。 (2)、子类继承后若没有重写纯虚函数的话,子类也叫抽象类,也不能实例化出对象,只有重写纯虚函数,子类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
2、接口继承和实现继承
(1)、普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。 (2)、虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口(函数声明与父类一样,这就是开头面试题缺省值的问题)。所以如果不实现多态,不要把函数定义成虚函数。
五、多继承的虚函数表:
首先看一段代码:
class A { public: virtual void fun1() { cout << "A的fun1" << endl; } virtual void fun2() { cout << "A的fun2" << endl; } protected: int _a; }; class B { protected: int _b; public: virtual void fun1() { cout << "B的fun1" << endl; } virtual void fun2() { cout << "B的fun2" << endl; } }; class C:public A,public B { protected: int _c; public: virtual void fun1() { cout << "C的fun1" << endl; } virtual void fun3() { cout << "C的fun3" << endl; } }; int main() { return 0; }
类A和类B都分别有两个虚函数和一个成员变量,类C多继承的类A和类B,然后重写了fun1函数,自己定义了fun3虚函数。
我们观察到如下现象:
(1)、计算类C对象的大小:
(2)、查看C类对象的虚表:
发现C类对象包含了从A继承的部分和从B继承的部分,并且每部分都有一个虚表。
(3)、切片问题:
首先我们要知道上述中:
1、ptr1和ptr2是不相等的:因为兼容赋值只会切继承父类的那部分。
2、但ptr1和&c相等,但含义不同,因为ptr1是类C继承列表的第一个类,所以起始地址相同。
(4)、子类自身定义的虚函数:
子类自身定义的虚函数,要么放在继承列表的第一个类的虚表中,要么放在继承列表所有类的虚表,如vs2019是放在继承列表第一个类的虚表中。
六、关于多态章节超经典的面试问答题:
1、什么是多态? 答:参考上述内容。 2. 什么是重载、重写(覆盖)、重定义(隐藏)? 答:参考上一篇文章。 3. 多态的实现原理? 答:参考虚表的相关内容 4. inline函数可以是虚函数吗? 答:可以,这涉及内联函数的双属性,我们要知道普通内联函数是没有地址的,只在调用位置展开,但如果是多态调用,就会忽略内联属性,此时也会有地址,此时就是作为虚函数。这个函数就不再是 inline,虚函数要放到虚表中去。 5. 静态成员可以是虚函数吗? 答:不能,因为静态成员函数没有this指针,使用类型::成员函数 的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。 6. 构造函数可以是虚函数吗? 答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。 7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数? 答:可以,并且最好把基类的析构函数定义成虚函数。使用场景参考上一篇文章。 8. 对象访问普通函数快还是虚函数更快? 答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。 9. 虚函数表是在什么阶段生成的,存在哪的? 答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。 10. C++菱形继承的问题?虚继承的原理? 答:菱形继承的问题参考底层分析,虚基表。注意这里不要把虚函数表和虚基表搞混了。 11. 什么是抽象类?抽象类的作用? 答:参考上述内容。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。标签:虚表,函数,继承,多态,C++,函数指针,重写,指针 From: https://blog.csdn.net/hffh123/article/details/143904570