前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家
点击跳转到网站
一、面试题:
分析如下代码,选择正确答案:
答案选:【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*指针,运用截断机制拿到虚表指针的值。int main(){Student S;Person P;Person* PP = &P;int* ptr = (int*)PP;printf("取到虚函数指针的值:%p", *ptr);return 0;}
(6)、满足多态以后的函数调用是在运行起来以后到对象中去找的 。 不满足多态的函数调用时编译时确认好的(即普通函数调用,去符号表里面找)。 (7)、同一个类的不同对象,有不同的虚表指针,但指向的都是同一个虚表。 (8)、虚表是编译时生成的,对象里面的虚表指针是在构造函数的初始化列表最开始就赋值的。
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是放在继承列表第一个类的虚表中。