一、继承与虚函数表
1.单继承无重写
-
day44.2-虚函数表作业中详细说过,这里只说结论
class Base{ public: virtual void Function_1(){ printf("Base:Function_1...\n"); } virtual void Function_2(){ printf("Base:Function_2...\n"); } virtual void Function_3(){ printf("Base:Function_3...\n"); } }; class Sub:public Base{ public: virtual void Function_4(){ printf("Sub:Function_4...\n"); } virtual void Function_5(){ printf("Sub:Function_5...\n"); } virtual void Function_6(){ printf("Sub:Function_6...\n"); } };
2.单继承有重写
-
如图所示:
class Base{ public: virtual void Function_1(){ printf("Base:Function_1...\n"); } virtual void Function_2(){ printf("Base:Function_2...\n"); } virtual void Function_3(){ printf("Base:Function_3...\n"); } }; class Sub:public Base{ public: virtual void Function_1(){ printf("Sub:Function_1...\n"); } virtual void Function_2(){ printf("Sub:Function_2...\n"); } virtual void Function_6(){ printf("Sub:Function_6...\n"); } };
3.多继承无重写
-
如图所示:如果Sub有两个父类Base1和Base2,那么就有两个虚函数表:第一个虚表中有Base1和Sub中的虚函数地址,第二个虚表中有Base2中的虚函数地址。所以Sub对象的大小就会多出来8字节
class Base1{ public: virtual void Fn_1(){ printf("Base1:Fn_1...\n"); } virtual void Fn_2(){ printf("Base1:Fn_2...\n"); } }; class Base2{ public: virtual void Fn_3(){ printf("Base2:Fn_3...\n"); } virtual void Fn_4(){ printf("Base2:Fn_4...\n"); } }; class Sub: public Base1,public Base2{ public: virtual void Fn_5(){ printf("Sub:Fn_5...\n"); } virtual void Fn_6(){ printf("Sub:Fn_6...\n"); } };
4.多继承有重写
-
如图所示:同样有两个虚函数表:第一个虚表中有Base1中的没有函数覆盖的虚函数地址和Sub中重写Base1中虚函数地址以及Sub自己的虚函数地址,第二个虚表中有Base2中没有函数覆盖的虚函数地址和Sub中重写Base2中虚函数地址。所以Sub对象大小就会多出来8字节
class Base1{ public: virtual void Fn_1(){ printf("Base1:Fn_1...\n"); } virtual void Fn_2(){ printf("Base1:Fn_2...\n"); } }; class Base2{ public: virtual void Fn_3(){ printf("Base2:Fn_3...\n"); } virtual void Fn_4(){ printf("Base2:Fn_4...\n"); } }; class Sub:public Base1,public Base2{ public: virtual void Fn_1(){ printf("Sub:Fn_1...\n"); } virtual void Fn_3(){ printf("Sub:Fn_3...\n"); } virtual void Fn_5(){ printf("Sub:Fn_5...\n"); } };
5.多重继承无重写
-
如图所示:Sub继承Base2,Base2又继承Base1,那么Sub对象就只有一张虚函数表:依次存储爷爷、父亲、自己的虚函数地址。所以Sub对象只会多出来4字节
class Base1{ public: virtual void Fn_1(){ printf("Base1:Fn_1...\n"); } virtual void Fn_2(){ printf("Base1:Fn_2...\n"); } }; class Base2:public Base1{ public: virtual void Fn_3(){ printf("Base2:Fn_3...\n"); } virtual void Fn_4(){ printf("Base2:Fn_4...\n"); } }; class Sub:public Base2{ public: virtual void Fn_5(){ printf("Sub:Fn_5...\n"); } virtual void Fn_6(){ printf("Sub:Fn_6...\n"); } };
6.多重继承有重写
-
如图所示:和前面都差不多,只要重写了,就把子类重写的虚函数地址改到父类对应虚函数地址的位置即可。同样Sub对象只多了4字节
class Base1{ public: virtual void Fn_1(){ printf("Base1:Fn_1...\n"); } virtual void Fn_2(){ printf("Base1:Fn_2...\n"); } }; class Base2:public Base1{ public: virtual void Fn_1(){ printf("Base2:Fn_1...\n"); } virtual void Fn_3(){ printf("Base2:Fn_3...\n"); } }; class Sub:public Base2{ public: virtual void Fn_3(){ printf("Sub:Fn_3...\n"); } virtual void Fn_5(){ printf("Sub:Fn_5...\n"); } };
二、动态绑定
- 绑定就是将函数调用与地址关联起来,即在函数调用的地方将要调用的真正函数地址确定下来的过程
1.举例一
#include "stdafx.h"
class Base{
public:
int x;
Base(){
x = 100;
}
void Function_1(){ //Func1没有加virtual
printf("Base:Function_1...\n");
}
virtual void Function_2(){ //Func2加Virtual
printf("Base:Function_2...\n");
}
};
class Sub:public Base{
public:
int x;
Sub(){
x = 200;
}
void Function_1(){
printf("Sub:Function_1...\n");
}
virtual void Function_2(){
printf("Sub:Function_2...\n");
}
};
void Test(Base* pb){
int n = pb->x;
printf("%d\n",n); //100
pb->Function_1(); //Base:Function_1...
pb->Function_2(); //Base:Function_2...
}
int main(int argc, char* argv[]){
Base base; //创建父类对象
Test(&base);
return 0;
}
-
通过反汇编发现:只要该程序一编译完,调用普通成员变量的地方的成员变量地址就写死了
-
只要该程序一编译完,调用普通成员方法的地方的函数地址也写死了,即编译时就把函数地址确定下来了,该调哪个类的对象的哪个函数:(这里就是把Base中的Function_1函数地址确定下来了,硬编码写死了)
-
但是对于类中的虚函数,编译时,在调用的地方是没办法把要真正要调用的函数地址确定下来的,而是采用虚函数表的方式:(这里call的是base对象的虚函数表中的第一个值!但是虚函数表中的第一个值是什么,只有运行时才知道)
-
所以先理解一下什么是动态绑定:假如调用的是虚函数表中的第一个值,但是通过一、继承与虚函数表可以得知,虚函数表中的值是可以被重写的!比如多重继承有重写中的例子:正常来说,虚函数表的第一个值应该为Base1:Fn_1函数地址值,但是现在由于Base2继承了Base1,且Base1中也有同名Fn_1虚函数,所以会把Base2:Fn_1函数地址值写到虚函数表的第一个位置上替换了原来的值。故这些虚函数的调用,有时候真正要调用哪个函数在编译时是不确定的!编译器确定的是调用虚函数表中第一个位置的函数,但是具体是什么函数,就要在运行时才能确定
2.举例二
#include "stdafx.h"
class Base{
public:
int x;
Base(){
x = 100;
}
void Function_1(){ //Func1没有加virtual
printf("Base:Function_1...\n");
}
virtual void Function_2(){ //Func2加Virtual
printf("Base:Function_2...\n");
}
};
class Sub:public Base{
public:
int x;
Sub(){
x = 200;
}
void Function_1(){
printf("Sub:Function_1...\n");
}
virtual void Function_2(){
printf("Sub:Function_2...\n");
}
};
void Test(Base* pb){ //父类的指针指向子类的对象
int n = pb->x;
printf("%d\n",n); //100
pb->Function_1(); //Base:Function_1...
pb->Function_2(); //Sub:Function_2...
}
int main(int argc, char* argv[]){
Sub sub; //创建子类对象
Test(&sub); //父类的指针指向子类的对象
return 0;
}
-
对于普通成员函数,地址还是写死的:(所以父类指针->普通成员函数Function_1,就调用父类中的Function_1普通成员函数,跟父类指针指向的对象是谁无关)
-
对于虚函数成员,地址依然是动态绑定的:由于此时调用的是sub对象的虚函数表中的第一个值:而根据举例一中的说明可知,编译器只能确定下来调用的是sub虚函数表中的第一个值,但是由于子类sub重写了其父类base中的虚函数Function_2,所以sub虚函数表中的第一个值就不是Base:Function_2了,而改成了Sub:Function_2的地址值!
3.总结
- 普通成员函数:编译完成后,在调用的地方,地址就写死了。称为==前期绑定或编译期绑定==
- 只有virtual的函数是动态绑定,又称晚绑定,或运行时绑定
- 动态绑定又称为==多态==:即虽然Test方法的参数是
Base*
指针pb,但是Base*
指针指向的是Base类对象,还是Base的子类对象sub是不确定的,就可能导致pb->Function_2()
这个虚函数体现出不同的行为 - 本质原因还是在于:
Base*
指针指向base对象,即调用虚函数时使用的是base对象的虚函数表;但Base*
指针指向其子类对象sub,调用虚函数时使用的就是sub对象的虚函数表。(就要考虑继承、重写带给虚函数表的变化) - 所以动态绑定是通过虚函数表实现的
- 如果没有多态,一个父类的指针永远只能访问自己类中的方法,访问不了其子类中重写的方法!
三、作业
1.体会多态
-
定义一个父类Base:有两个成员X,Y;有一个函数Print(非virtul)能够打印X,Y的值
-
定义3个子类:Sub1,有一个成员A;Sub2,有一个成员B。每个子类有一个函数Print(非virtul),打印所有成员----Sub1:打印X Y A;Sub2:打印X Y B
-
定义一个数组,存储Base Sub1 Sub2对象;再使用一个循环语句调用所有的Print函数
#include "stdafx.h" class Base{ public: int X; int Y; public: Base(){ X = 1; Y = 2; } void print(){ printf("Base:%x %x\n",X,Y); } }; class Sub1:public Base{ public: int A; public: Sub1(){ X = 3; Y = 4; A = 5; } void print(){ printf("Sub1:%x %x %x\n",X,Y,A); } }; class Sub2:public Base{ public: int B; public: Sub2(){ X = 6; Y = 7; B = 8; } void print(){ printf("Sub2:%x %x %x\n",X,Y,B); } }; void Test(){ Base b; Sub1 s1; Sub2 s2; //定义一个Base*指针类型的数组!所以arr[0]就是父类指针指向自己的对象;arr[1]和arr[2]就是父类的指针指向其子类的对象 Base* arr[] = {&b,&s1,&s2}; for(int i = 0;i < 3;i++){ arr[i]->print(); //Base:1 2 Base:3 4 Base:6 7 } } int main(int argc, char* argv[]){ Test(); return 0; }
因为都是用**
Base*
指针的方式调用的普通成员函数**:所以无论Base*
指针指向的是Base对象、还是其子类的对象,都调用的是Base类中的print函数!但是由于创建子类对象s1时先调用父类构造器、再调用自己的构造器,导致X,Y的值变成了3,4。所以调用Base类中的print函数打印X,Y,A的值时结果为3,4,5。s2以此类推 -
将上面所有的Print函数改成virtul,继续观察效果:
#include "stdafx.h" class Base{ public: int X; int Y; public: Base(){ X = 1; Y = 2; } virtual void print(){ printf("Base:%x %x\n",X,Y); } }; class Sub1:public Base{ public: int A; public: Sub1(){ X = 3; Y = 4; A = 5; } virtual void print(){ printf("Sub1:%x %x %x\n",X,Y,A); } }; class Sub2:public Base{ public: int B; public: Sub2(){ X = 6; Y = 7; B = 8; } virtual void print(){ printf("Sub2:%x %x %x\n",X,Y,B); } }; void Test(){ Base b; Sub1 s1; Sub2 s2; //定义一个Base*指针类型的数组!所以arr[0]就是父类指针指向自己的对象;arr[1]和arr[2]就是父类的指针指向其子类的对象 Base* arr[] = {&b,&s1,&s2}; for(int i = 0;i < 3;i++){ arr[i]->print(); } } int main(int argc, char* argv[]){ Test(); return 0; }
arr[0]->print()
就是用Base对象调用虚函数print,所以使用的就是Base对象的虚函数表,结果为Base:1 2arr[1]->print()
就是用父类指针Base*
指向子类对象s1,所以调用虚函数print时,使用的就是Sub1对象的虚函数表,结果为Sub1:3 4 5arr[2]->print()
也是用父类指针指向子类的对象s2,所以调用虚函数print时,使用的是Sub2对象的虚函数表,结果为Sub2: 6 7 8这就是多态:同一个类型的指针调用一个函数,却表现出不同的行为
2.为什么析构函数建议写成virtul
-
如果有一个父类Base,它有一个子类Sub,现在做如下操作
Base b; Sub s; Base* pb = &b; //Base类指针指向自己的对象 Base* ps = &s; //父类指针指向子类的对象
-
如果Base类中和Sub类中的析构函数都不是Virtual,当上述中s对象销毁时,由于是用
Base*
指针指向的s对象,所以会调用Base类中的析构函数!这里本应该销毁什么对象,就调用这个对象的类中的析构函数,清理该对象自己的资源 -
如果Base类中和Sub类中的析构函数是Virtual,当上述中s对象销毁时,即使是用
Base*
指针指向的s对象,但是还是会调用Sub类中的虚析构函数