滴水三期:day45.1-动态绑定(多态)

文章详细阐述了C++中继承与虚函数表的关系,包括单继承、多继承、重写等场景下的虚函数表结构。同时,讨论了动态绑定的概念,通过实例展示了普通成员函数与虚函数在调用时的区别,强调了虚函数在多态中的关键作用。最后提到了虚析构函数的重要性,确保正确清理对象资源。

一、继承与虚函数表

1.单继承无重写

  • day44.2-虚函数表作业中详细说过,这里只说结论

    image-20230425213545341
    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.单继承有重写

  • 如图所示:

    image-20230425213847525
    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字节

    image-20230425215312197
    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字节

    image-20230425220353571
    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字节

    image-20230425220823948
    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字节

    image-20230425221905628
    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;	
}
  • 通过反汇编发现:只要该程序一编译完,调用普通成员变量的地方的成员变量地址就写死了

    65647
  • 只要该程序一编译完,调用普通成员方法的地方的函数地址也写死了,即编译时就把函数地址确定下来了,该调哪个类的对象的哪个函数:(这里就是把Base中的Function_1函数地址确定下来了,硬编码写死了)

    165927
  • 但是对于类中的虚函数,编译时,在调用的地方是没办法把要真正要调用的函数地址确定下来的,而是采用虚函数表的方式:(这里call的是base对象的虚函数表中的第一个值!但是虚函数表中的第一个值是什么,只有运行时才知道)

    170323
  • 所以先理解一下什么是动态绑定:假如调用的是虚函数表中的第一个值,但是通过一、继承与虚函数表可以得知,虚函数表中的值是可以被重写的!比如多重继承有重写中的例子:正常来说,虚函数表的第一个值应该为Base1:Fn_1函数地址值,但是现在由于Base2继承了Base1,且Base1中也有同名Fn_1虚函数,所以会把Base2:Fn_1函数地址值写到虚函数表的第一个位置上替换了原来的值。故这些虚函数的调用,有时候真正要调用哪个函数在编译时是不确定的!编译器确定的是调用虚函数表中第一个位置的函数,但是具体是什么函数,就要在运行时才能确定

    image-20230428170749722

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普通成员函数,跟父类指针指向的对象是谁无关

    image-20230428173514429
  • 对于虚函数成员,地址依然是动态绑定的:由于此时调用的是sub对象的虚函数表中的第一个值:而根据举例一中的说明可知,编译器只能确定下来调用的是sub虚函数表中的第一个值,但是由于子类sub重写了其父类base中的虚函数Function_2,所以sub虚函数表中的第一个值就不是Base:Function_2了,而改成了Sub:Function_2的地址值!

    174215

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 2

    arr[1]->print()就是用父类指针Base*指向子类对象s1,所以调用虚函数print时,使用的就是Sub1对象的虚函数表,结果为Sub1:3 4 5

    arr[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类中的虚析构函数

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值