1.虚函数作业
1.多层继承无函数覆盖
结论:多继承无函数覆盖时,首先调用的是“爷类”,然后是“父类”,最后是“子类”
2.多层继承有函数覆盖(爷类与父类覆盖)
结论:首先调用的是父类虚函数,再调用爷类
3.多层继承有函数覆盖(爷类与子类覆盖)
结论:首先调用的是子类虚函数,再爷类
4.多层继承有函数覆盖(子类与父类覆盖)
结论:首先调用的是子类虚函数,再父类
5.多层继承有函数覆盖(三类都覆盖覆盖)
结论:首先调用的是子类,再调用父类 ,最后爷类
6.多重继承无函数覆盖
结论:此时有两个虚函数表,this指针前四个字节指向第一个虚函数表(子类和继承的最近的那个父类),第二个四字节指向第二个虚函数表(第二个父类)
7.多重继承有函数覆盖
结论:含有两个虚函数表,子类覆盖哪个父类就调用在哪个虚函数表中,始终记住,第一个虚函数表是子类与最近的父类的虚函数表,第二个虚函数是子类与较远的父类的虚函数表
2.前提绑定与动态绑定
根据前面的知识点,我们对于父类与子类指针之间的调用还有很多的疑惑,下面我们进行几组实验。
前提条件:首先我们定义父类子类之间的继承关系,父类子类之间有着重写函数。
我们定义父类的对象,用对象去调用函数和打印变量成员时,发现调用的都是父类的成员。相反,我们定义一个子类的对象,用子类的对象去调用时,被调用的都是子类的成员。
因此我们可以的出结论:在使用对象调用时,如果定义的是父类的对象,那么调用的是父类的成员;如果定义的是子类的对象,那么调用的是子类的成员。(在我们定义子类的对象想要调用父类成员时,我们需要更换书写:sub.Base::Fubction_1(),但是不可以定义父类的对象,来调用子类的对象,因为是子类继承父类,因此子类中函数有父类,但是父类不包含子类,只能通过指针的强转来访问)。
那么什么是动态绑定呢,本人个人认为绑定是对于代码形式的抽象定义。
void Test(Base* p)
{
printf("%d\n",p->x);
p->Function_1();
p->Function_2();
}
int main()
{
Sub fp;
Test(&fp);
return 0;
}
我们通过定义一个函数来调用的形式,在函数参数接受参数时为Base*(父类对象的指针)为什么要定义为父类指针接收,因为父类指针可以兼容父类和子类,我们可以认为用Base*指针来接收时,无论传参的是父类还是子类的地址,我们都可以进行访问,但是定义为Sub*指针来接收时,子类宽度大于父类,如果此时传参过来的是父类的地址,进行访问时就会出现越界的情况,因此此时编译器会出现无法转型的报错,查看反汇编
此时我们发现,调用普通函数时call的地址已经固定为Base父类的地址,但是虚函数调用使用的是虚函数表(此时虚函数表存在多态性,根据子类父类之间的重写关系而具有着不同的结果)因此绑定的效果就是通过定义函数接收参数的类型而造成的固定形态的抽象定义。
当我们传递父类的指针作为参数时:
当我们传递子类的指针作为参数时:
总结:因为调用函数接受的是父类指针的宽度,所有调用的都是父类中的成员,但是因为虚函数的不确定性(虚函数子类出现虚函数重写时,虚函数首个四字节的地址会变成子类的)因此出现了动态绑定和前期绑定
1.前期绑定:又叫编译器绑定,即非虚函数类的成员在传参给函数前就已经固定了调用的地址。
2.动态绑定:又叫运行期绑定,晚绑定,多态,即调用函数没有接收到参数时,还不能确定虚函数表的首个地址是谁的,所以出现了不确定性,即多态性。
3.作业
每个子类有一个函数Print(非virtul),打印所有成员.
Sub1:打印X Y A
Sub2:打印X Y B
Sub3:打印X Y C
2、定义一个数组,存储Base Sub1 Sub2 Sub3 使用一个循环语句调用所有的Print函数。
3、将上面所有的Print函数改成virtul 继续观察效果.
#include<stdio.h>
struct Base
{
public:
int x;
int y;
Base()
{
this->x=1;
this->y=2;
}
virtual void Print()
{
printf("Base:x=%d y=%d\n",x,y);
}
};
struct Sub1:Base
{
public:
int x;
int y;
Sub1()
{
this->x=3;
this->y=4;
}
virtual void Print()
{
printf("Base:x=%d y=%d\n",x,y);
}
};
struct Sub2:Base
{
public:
int x;
int y;
Sub2()
{
this->x=5;
this->y=6;
}
virtual void Print()
{
printf("Base:x=%d y=%d\n",x,y);
}
};
void Test()
{
Base base;
Sub1 s1;
Sub2 s2;
Base* arr[]={&base,&s1,&s2};//用Base*类型可以兼容两个字类
for(int i=0;i<3;i++)
{
arr[i]->Print();
}
}
int main()
{
Test();
return 0;
}
4、思考题:为什么析构函数建议写成virtul的?
当出现继承关系时,如果我们的析构函数不是虚函数的形式,那么在每次调用完毕后就会执行父类的析构函数。
但是如果将析构函数变成虚函数的形式,就可以达到动态绑定的目的,可以根据传参的不同而进行不同的析构函数,而不会影响到父类。