zoukankan      html  css  js  c++  java
  • C++ | 动多态的发生时机

    探究动多态的发生时机


    有了虚函数和虚函数表为动多态提供支持,从而可以实现C++语言的动多态。那么,问题又来了。

    动多态的发生时机是什么?
    或者说,动多态发生有哪些条件与限制呢?

    下面让我们一起来探究动多态的秘密,揭示动多态的发生时机。


    详细步骤:
    1、虚函数与普通函数的调用
    2、利用汇编代码分析动多态
    3、初步探究动多态调用方式
    4、深入探究动多态发生时机
    5、总结


    1、虚函数与普通函数的调用

    我们已经知道,在调用虚函数时会通过虚表中保存的虚函数入口地址来调用。那么试想一下,如果类中既含有虚函数又含有普通函数,他们调用的方式又有何不同呢?

    现有以下代码,基类中既有虚函数又有普通的类成员函数。

    #include <iostream>
    
    class Base		//定义基类
    {
    public:
    	Base(int a) :ma(a) {}
    	virtual void Show()
    	{
    		std::cout << "Base: ma = " << ma << std::endl;
    	}
    	void Print()
    	{
    		std::cout << "Base: This is print " << std::endl;
    	}
    protected:
    	int ma;
    };
    class Deriver : public Base		//派生类
    {
    public:
    	Deriver(int b) :mb(b), Base(b) {}
    	void Show()
    	{
    		std::cout << "Deriver: mb = " << mb << std::endl;
    	}
    protected:
    	int mb;
    };
    
    int main()
    {
    	Base* pb = new Deriver(10);		
    	pb->Show();
    	pb->Print();
    	return 0;
    }
    
    

    输出结果:
    在这里插入图片描述

    2、利用汇编代码分析动多态

    输出结果毫无疑问是正确的,要想理解程序在运行时究竟做了什么,我们需要在汇编层次上进行探究

    /* 以下代码段为:
    	pb->Show();
    	pb->Print();
    	return 0;
    */
    	pb->Show();
    00766512  mov         eax,dword ptr [pb]  
    00766515  mov         edx,dword ptr [eax]  
    	pb->Show();
    00766517  mov         esi,esp  
    00766519  mov         ecx,dword ptr [pb]  
    0076651C  mov         eax,dword ptr [edx]  
    0076651E  call        eax  					// ⑴
    00766520  cmp         esi,esp  
    00766522  call        __RTC_CheckEsp (07612DFh)  
    	pb->Print();
    00766527  mov         ecx,dword ptr [pb]  
    0076652A  call        Base::Print (07614BFh)  // ⑵
    	return 0;
    0076652F  xor         eax,eax  
    

    先不看其他汇编代码具体有什么含义,在上述 ⑴ 、⑵ 标出的位置上,都执行了 call 指令(call指令是计算机转移到调用的子程序)。

    • 在 ⑴ 处,call eax ,eax寄存器是在运行阶段暂存了某个变量的值,结合 call 指令我们可以得知,eax 中应该存放的就是 Deriver::Show() 的入口地址。通过在运行阶段确定函数的入口地址,进行动态的绑定
    • 在 ⑵处,call Base::Print (07614BFh) 直接 call 了 Base::Print() 的入口地址,说明在编译阶段已经确定了函数的调用,直接写入到指令中,进行了一个静态的绑定。

    3、初步探究动多态调用方式

    上述中通过简单判断 call 指令从而判断函数是否发生了动多态,下面为了更加深入的探究动多态发生原理,我们稍加修改一下源码测试:

    3.1 指针方式调用
    /* 修改main 函数中内容如下: */
    int main()
    {
    	Base b(10);
    	Deriver d(20);
    
    	Base* pb1 = &b;	//基类指针 指向 基类
    	Base* pb2 = &d;	//基类指针 指向 派生类
    
    	Deriver* pd = &d; //派生类指针 指向 派生类
    
    	pb1->Show();
    	pb2->Show();
    	pd->Show();
    
    	b.Show();
    	d.Show();
    	return 0;
    }
    

    汇编分析:

    
    	pb1->Show();
    004365B4  call        eax   /* 动态绑定 */
      
    	pb2->Show();
    004365C9  call        eax   /* 动态绑定 */
    
    	pd->Show();
    004365DE  call        eax   /* 动态绑定 */
      
    	b.Show();
    004365EA  call        Base::Print (043144Ch)  
    
    	d.Show(); 
    004365F2  call        Base::Show (04314B5h)  
    
    

    在结果中我们发现:动多态发生在指针调用虚函数时

    3.2 引用方式调用

    我们说在C++中,引用的底层实现是依靠指针来做支持的,理论上引用与指针访问是没有多大区别的。那么我们再来测试一下以引用的方式来访问,探究动多态的发生时机。修改代码如下:

    /* 修改main 函数中内容如下: */
    int main()
    {
    	Base b(10);
    	Deriver d(20);
    
    	Base& rb1 = b;
    	Base& rb2 = d;
    
    	Deriver& rd = d;
    
    	rb1.Show();
    	rb2.Show();
    	rd.Show();
    	
    	return 0;
    }
    

    汇编分析:

    
    	rb1.Show(); 
    006965B4  call        eax   /* 动态绑定 */
     
    	rb2.Show();
    006965C9  call        eax   /* 动态绑定 */
     
    	rd.Show();
    006965DE  call        eax   /* 动态绑定 */
    
    

    通过上述实验,我们初步得出结论:动多态发生在指针调用引用调用的虚函数上

    4、深入探究动多态发生时机

    思考:那么。是否所有的动多态都可以通过指针或引用的方式调用实现呢,又或者通过引用调用或指针调用的方式就一定会发生动多态吗?

    对于第一问,答案是显而易见的,当然是。调用函数无非就是拿到函数的入口地址,而能发生动多态的只能是(成为虚函数的)类成员函数,无论是通过 this->Show().*运算符、->* 运算符(类成员函数指针) 访问,实质上都是普通的类成员方法访问,而要实现动多态就要具备有以基类指针形式存在,而又可以访问派生类的函数的的特点。因此,动多态只能通过指针或引用的方式实现。

    ps:理论上通过类成员函数指针的方式模拟实现C++的动多态,这里引用CSDN的一篇博客函数指针实现多态,有兴趣的可以看看。

    4.1 在构造和析构中是否可以发生动多态

    对于第二问,我们需要进行以下实验才可得出结论。
    在C++或者说在C/C++语言中,一个函数可以调用另一个函数,甚至有自身调用自身的递归调用存在。在C++中,类的构造函数和析构函数也支持这一特点,那么我们猜想在构造或者析构中调用函数能否发生动多态。

    探究构造函数:
    /* 在构造函数中调用 Show() */
    #include <iostream>
    
    class Base		//定义基类
    {
    public:
    	Base(int a) :ma(a) 
    	{
    		this->Show();	/* 在基类的构造函数中调用 Show()*/
    	}
    	virtual void Show()
    	{
    		std::cout << "Base: ma = " << ma << std::endl;
    	}
    	void Print()
    	{
    		std::cout << "Base: This is print " << std::endl;
    	}
    protected:
    	int ma;
    };
    class Deriver : public Base		//派生类
    {
    public:
    	Deriver(int b) :mb(b), Base(b) {}
    	void Show()
    	{
    		std::cout << "Deriver: mb = " << mb << std::endl;
    	}
    protected:
    	int mb;
    };
    
    int main()
    {
    	Base b(10);
    	Deriver d(20);
    
    	return 0;
    }
    

    汇编分析:

    	Base(int a) :ma(a) 
    00021F46  mov         eax,dword ptr [this]  
    00021F49  mov         ecx,dword ptr [a]  
    00021F4C  mov         dword ptr [eax+4],ecx  
    		this->Show();
    00021F4F  mov         ecx,dword ptr [this]  
    00021F52  call        Base::Show (0213E3h)  
    	}
    

    我们可以看到 call Base::Show (0213E3h) 在汇编代码中,发生的是静态的绑定,编译时直接把代码写死在指令段中。

    探究析构函数:
    /* 添加虚析构函数, 注:在探究析构函数时应取消构造函数中的Show()调用 */
    virtual ~Base()
    {
    	this->Show();
    }
    /* 汇编分析 */
    		this->Show();
    00812295  mov         ecx,dword ptr [this]  
    00812298  call        Base::Show (08113E3h) 
    

    可以看到,在析构函数中也无法实现函数的动多态调用。

    4.2 在普通类成员函数中实现动多态

    在普通类成员函数也可以调用函数,下面测试在类成员方法中是否可以实现动多态

    /* 在基类的 Print() 函数中调用Show() 
         在main 函数中添加 b.Print()    */
    void Print()
    {
    	this->Show();
    	std::cout << "Base: This is print " << std::endl;
    }
    /* 汇编分析 */
    		this->Show();
    007D2689  call        eax  
    
    

    可以看到在普通的类成员方法中是可以实现动多态的。

    5、总结

    分析:
    构造函数内不能发生动多态:构造函数开始工作时对象正在生成,对象不完整
    析构函数内不能发生动多态:析构函数开始工作时对象正在销毁,对象不完整

    总结: 动多态发生条件

    • 1、指针或引用调用虚函数
    • 2、对象完整

    满足以上条件可以发生动多态。

    动多态发生时机为: 在基类指针访问派生类对象中的虚函数时,并且该访问满足动多态的发生条件,即可发生动多态。


    附:
    虚函数产生条件:https://blog.csdn.net/weixin_43919932/article/details/104388194

  • 相关阅读:
    gojs入门
    chartjs:改变图表的外观
    chart.js入门
    verilog与C语言的6点重大区别
    PCB布线原则【转】_神经火光_百度空间
    verilog中对同一个变量有判断条件的赋值
    同步复位与异步复位——异步复位同步释放
    如何利用TCL文件给FPGA分配引脚
    0欧姆电阻的作用
    独热码
  • 原文地址:https://www.cnblogs.com/TaoR320/p/12680127.html
Copyright © 2011-2022 走看看