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

一、继承与虚函数表

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

第1讲:2015-01-12(进制01) 第2讲:2015-01-13(进制02) 第3讲:2015-01-14(数据宽度-逻辑运算03) 第4讲:2015-01-15(通用寄存器-内存读写04) 第5讲:2015-01-16(内存寻址-堆栈05) 第6讲:2015-01-19(EFLAGS寄存器06) 第7讲:2015-01-20(JCC) 第8讲:2015-01-21(堆栈图) 第8讲:2015-01-21(宝马问题) 第9讲:2015-01-22(堆栈图2) 第10讲:2015-01-23(C语言01_后半段) 第10讲:2015-01-23(C语言完整版) 第11讲:2015-01-26(C语言02_数据类型) 第12讲:2015-01-27(C语言03_数据类型_IF语句) 第13讲:2015-01-28(C语言04_IF语句逆向分析上) 第14讲:2015-01-28(C语言04_IF语句逆向分析下) 第15讲:2015-01-29(C语言04_正向基础) 第16讲:2015-01-30(C语言05_循环语句) 第17讲:2015-02-02(C语言06_参数_返回值_局部变量_数组反汇编) 第18讲:2015-02-02(2015-01-30课后练习) 第19讲:2015-02-03(C语言07_多维数组) 第20讲:2015-02-03(2015-02-02课后练习) 第21讲:2015-02-04(C语言08_结构体) 第22讲:2015-02-05(C语言09_字节对齐_结构体数组) 第23讲:2015-02-06(C语言10_Switch语句反汇编) 第24讲:2015-02-26(C语言11_指针1) 第25讲:2015-02-27(C语言11_指针2) 第26讲:2015-02-28(C语言11_指针3) 第27讲:2015-02-28(C语言11_指针4) 第28讲:2015-03-02(C语言11_指针5) 第29讲:2015-03-03(C语言11_指针6) 第30讲:2015-03-04(C语言11_指针7) 第31讲:2015-03-06(C语言11_指针8) 第32讲:2015-03-09(位运算) 第33讲:2015-03-10(内存分配_文件读写) 第34讲:2015-03-11(PE头解析_手动) 第35讲:2015-03-12(PE头字段说明) 第36讲:2015-03-13(PE节表) 第37讲:2015-03-16(FileBuffer转ImageBuffer) 第38讲:2015-03-17(代码节空白区添加代码) 第39讲:2015-03-18(任意节空白区添加代码) 第40讲:2015-03-19(新增节添加代码) 第41讲:2015-03-20(扩大节-合并节-数据目录) 第42讲:2015-03-23(静态连接库-动态链接库) 第43讲:2015-03-24(导出表) 第44讲:2015-03-25(重定位表) 第45讲:2015-03-26(移动导出表-重定位表) 第46讲:2015-03-27(IAT表) 第47讲:2015-03-27(导入表) 第48讲:2015-03-30(绑定导入表) 第49讲:2015-03-31(导入表注入) 第50讲:2015-04-01(C++ this指针 类 上) 第51讲:2015-04-01(C++ this指针 类 下) 第52讲:2015-04-02(C++ 构造-析构函数 继承) 第53讲:2015-04-03(C++ 权限控制) 第54讲:2015-04-07(C++ 虚函数表) 第55讲:2015-04-08(C++ 动态绑定-多态-上) 第56讲:2015-04-08(C++ 动态绑定-多态-下) 第57讲:2015-04-09(C++ 模版) 第58讲:2015-04-10(C++ 引用-友元-运算符重载) 第59讲:2015-04-13(C++ new-delete-Vector) 第60讲:2015-04-14(C++Vector实现) 第61讲:2015-04-15(C++链表) 第62讲:2015-04-16(C++链表实现) 第63讲:2015-04-16(C++二叉树) 第64讲:2015-04-17(C++二叉树实现) 第65讲:2015-04-20(Win32 宽字符) 第66讲:2015-04-21(Win32 事件-消息-消息处理函数) 第67讲:2015-04-22(Win32 ESP寻址-定位回调函数-条件断点) 第68讲:2015-04-23(Win32 子窗口-消息处理函数定位) 第69讲:2015-04-24(Win32 资源文件-消息断点) 第70讲:2015-04-27(Win32 提取图标-修改标题) 第71讲:2015-04-28(Win32 通用控件-VM_NOTIFY) 第72讲:2015-04-29(Win32 PE查看器-项目要求) 项目一:PE查看器 开发周期(5天) 需求文档 第73讲:2015-05-07(Win32 创建线程) 第74讲:2015-05-08(Win32 线程控制_CONTEXT) 第75讲:2015-05-11(Win32 临界区) 第76讲:2015-05-12(Win32 互斥体) 第77讲:2015-05-13(Win32 事件) 第78讲:2015-05-14(Win32 信号量) 第79讲:2015-05-15(Win32 线程同步与线程互斥) 第80讲:2015-05-18(Win32 进程创建_句柄表) 第81讲:2015-05-20(Win32 以挂起形式创建进程) 第82讲:2015-05-21(Win32 加密壳_项目说明) 项目二:加密壳 开发周期(5天) 需求文档 第83讲:2015-05-28(Win32 枚举窗口_鼠标键盘事件) 第84讲:2015-05-29(Win32 CE练习) 第85讲:2015-06-01(Win32 OD练习) 第86讲:2015-06-03(Win32 ShellCode_远程线程注入) 第87讲:2015-06-04(Win32 加载EXE_模块隐藏) 第88讲:2015-06-09(Win32 IAT_HOOK) 第89讲:2015-06-10(Win32 InlineHook) 第90讲:2015-06-11(Win32 进程通信) 第91讲:2015-06-11(Win32 进程监控_项目说明) 项目三:进程监控 开发周期(5天) 需求文档 第92讲:2015-06-15(硬编码_01) 第93讲:2015-06-16(硬编码_02) 第94讲:2015-06-17(硬编码_03) 第95讲:2015-06-18(硬编码_04) 第96讲:2015-06-19(硬编码_05)
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值