• C++虚函数浅探


    C++中和虚函数(Virtual Function)密切相关的概念是“动态绑定”(Dynamic Binding),与之相对的概念是“静态绑定”(Static Binding)。所谓“静态绑定”,是指在编译时就能确定函数调用语句和实际执行的函数;而“动态绑定”则是——对于同一个函数调用,编译时并不能确定具体调用的函数,直到执行时才能决定。

    静态绑定

    继承而没有体现多态的例子:
    #include <iostream>
    
    using namespace std;
    
    class Base
    {
    public:
    	void show() { cout << "I am a Base object!
    "; }
    };
    
    class Derived : public Base
    {
    public:
    	void show() { cout << "I am a Derived object!
    "; }
    };
    
    int main(int argc, char *argv[])
    {
    	Base *pBase = new Base();
    	pBase->show();
    	
    	pBase = new Derived();
    	pBase->show();
    	
    	return 0;
    }
    这个例子将会输出:
    I am a Base object!
    I am a Base object!
    这个例子说明,通过基类指针(或引用)调用一般的成员函数(Member Function)时(编译器)采取的都是静态绑定。

    静态绑定的实现

    静态绑定的实现,即一般的成员函数的实现。例如,有这样的一个类的定义:
    class Simple
    {
    	int data;
    public:
    	void setData(int d) { data = d; }
    	int getData() { return data; }
    };
    编译器会将上面的两个成员函数处理为类似下面的C代码(暂不考虑名称修饰)[1]:
    // 伪代码, 说明编译器对一个成员函数定义的展开形式
    void setData(Simple* this, int d)
    {
    	this->data = d;
    }
    
    int getData(Simple* this)
    {
    	return this->data;
    }
    (C代码?现在的编译器根本不会这么做(间接编译)!哦,只有当年的cfront才会先把C++转成C代码再编译☺)
    静态绑定的实现大体如以上代码所示,即在成员函数的参数列表最前面插入一个指针(this指针),成员函数内部所有对成员变量的访问都将由此this指针寻址;这样即可实现语言层面上不同对象调用相同成员函数,访问各自的数据拷贝。

    动态绑定

    只需将上面静态绑定的例子上的show函数加上virtual,此时虽然Derived::show没有声明为virtual,但它也是virtual(由继承而来的virtual属性,标准就是这样规定的)。
    #include <iostream>
    
    using namespace std;
    
    class Base
    {
    public:
    	virtual void show() { cout << "I am a Base object!
    "; }
    };
    
    class Derived : public Base
    {
    public:
    	void show() { cout << "I am a Derived object!
    "; }
    };
    
    int main(int argc, char *argv[])
    {
    	Base *pBase = new Base();
    	pBase->show();
    	
    	pBase = new Derived();
    	pBase->show();
    	
    	return 0;
    }
    这样,程序将输出:
    I am a Base object!
    I am a Derived object!

    这里的例子体现了多态性,这里pBase->show()前后两次分别执行了Base::show()和Derived::show(),而且这种选择是在执行时期决定的,而非前面例子的编译时期。
    一个更激进的例子是——为了证明是在执行时期,可以让pBase所指向的对象由用户输入决定:
    void testRTTI()
    {
    	int n = 0;
    	while(cin >> n) { // 遇到EOF字符结束,Windows控制台上Ctrl+Z可输入EOF,Linux Ctrl+D
    		if( n % 2 ) 
    			pBase = new Base();
    		else
    			pBase = new Derived();
    		pBase->show();
    		delete pBase;
    	}	
    }
    例如,一组输入输出(黑体是输入):
    1
    I am a Base object!
    2
    I am a Derived object!
    3
    I am a Base object!

    动态绑定的实现

    动态绑定的实现,即virtual function的实现,《深度探索C++对象模型》(以下简称<模型>)第四章对此有详细探讨[3]。这里仅简要描述,编译器在编译时将一个类的所有Virtual Function的地址存入一个表中(Virtual Table, vtbl),并在类的数据成员中安插一个指向该表的指针(vptr)。在一个继承体系中(上例中的Base和Derived),子类继承自父类的virtual function将会被安插在与父类vtbl相同的位置。编译器在该类的构造函数(若用户没有定义,编译器将生成)中插入初始化vptr的代码(将vptr指向vtbl);析构函数中也会有类似的行为。
    根据<模型>的论述,上例Base和Derived的内存布局和其virtual table如下:

    这些信息使得编译器可以将pBase->show();转化为:
    pBase->_vptr[1](pBase);
    在执行时就能够实现不同的函数调用了。

    VC2008跟踪

    以下将上述动态绑定的代码在VC2008下以Win32 Debug版本编译,并用反汇编(调试->窗口->反汇编)调试。

    1.构造函数初始化vptr

    先看看Base创建的代码:

    	Base *pBase = new Base();
    00B6152E  push        4    
    00B61530  call        operator new (0B6120Dh) ; 申请内存
    00B61535  add         esp,4 ; 清除压入的4
    00B61538  mov         dword ptr [ebp-0E0h],eax ;  保存到栈上临时变量(暂计为ret)
    00B6153E  cmp         dword ptr [ebp-0E0h],0      ; ret和0比较
    00B61545  je          main+4Ah (0B6155Ah)         ; 如果ret==0 ,不执行构造函数
    00B61547  mov         ecx,dword ptr [ebp-0E0h] ; ret存入 ecx(this指针)
    00B6154D  call        Base::Base (0B61136h)    ; 调用Base::Base
    

    看到了call Base::Base,继续:

    00B61136  Base::Base (0B61630h)
    

    Base::Base:
    00B61630  push        ebp  
    00B61631  mov         ebp,esp 
    00B61633  sub         esp,0CCh       ; 栈上开辟空间(栈向下生长)
    00B61639  push        ebx  
    00B6163A  push        esi  
    00B6163B  push        edi  
    00B6163C  push        ecx  ; 最后一个push
    00B6163D  lea         edi,[ebp-0CCh]       ; 
    00B61643  mov         ecx,33h              ; 初始化刚开辟的空间
    00B61648  mov         eax,0CCCCCCCCh       ; (Debug版 特有代码)
    00B6164D  rep stos    dword ptr es:[edi]   ; /
    00B6164F  pop         ecx  ; 最近一次 push的是 ecx,而这期间esp没有被修改;而上次push之前ecx也没有被修改,所以ecx还是原来的值(main写入的this指针)
    00B61650  mov         dword ptr [ebp-8],ecx 
    00B61653  mov         eax,dword ptr [this]    ; 取出this指针
    00B61656  mov         dword ptr [eax],offset Base::`vftable' (0B67804h) ;  初始化__vptr,让它指向vftable
    00B6165C  mov         eax,dword ptr [this] ; 取出this指针,写入eax
    00B6165F  pop         edi  
    00B61660  pop         esi  
    00B61661  pop         ebx  
    00B61662  mov         esp,ebp 
    00B61664  pop         ebp  
    00B61665  ret

    可以看到 dword ptr [eax],offset Base::`vftable' (0B67804h) 就是用来设置__vptr的,因为代码中的Base,Derived没有定义其他数据成员,所以this指针所指的dword(4B)就是__vptr。

    2.virtual table里保存什么?

    通过VC2008的内存窗口(调试->窗口->内存)可以查看vftable的内容:

    可以看到vftable的第一个成员是:0x00B61118,第二个是0(表示结束,没有后续的),它很可能是一个函数的地址,在反汇编窗口输入改地址能看到:

    由此可以看到,VC2008的vftable和<模型>一书描述的并不相同,vftable第一个slot并没有存放type_info,而是直接存了Base::show;因此这里vftable只有一个slot。

    3.调用virtual function的实际代码

    再来看看从调用Base::Base()到pBase->show();的代码:
    00B6154D  call        Base::Base (0B61136h) 
    00B61552  mov         dword ptr [ebp-0E8h],eax ; eax 里存的是this指针,这相当于保存函数返回值到临时变量
    00B61558  jmp         main+54h (0B61564h) 
    00B6155A  mov         dword ptr [ebp-0E8h],0   ; 这行代码被忽略
    00B61564  mov         eax,dword ptr [ebp-0E8h] ;
    00B6156A  mov         dword ptr [pBase],eax    ; 将this保存到pBase里; 相当于 pBase = eax
    	pBase->show();
    00B6156D  mov         eax,dword ptr [pBase]    ; 再取出; 相当于 eax = pBase
    00B61570  mov         edx,dword ptr [eax]      ; 这里很关键,Base::show()在vftable的slot 0中,所以直接取eax所指向的dword(4字节)
    00B61572  mov         esi,esp 
    00B61574  mov         ecx,dword ptr [pBase]    ; ecx 传入 this 指针
    00B61577  mov         eax,dword ptr [edx]      ; 取出virtual function实际地址
    00B61579  call        eax                      ; 调用
    至此,一个完整的virtual function的执行已经梳理清楚了。

    第二次,pBase = new Derived(); 后 pBase->show(); 的流程与此完全类似,这里不再罗列;唯一不同的是,Derived::Derived()里初始化__vptr的值会是offset Derived::`vftable'。

    参考

    [1] 潘爱民, 张丽 译, Stanley B.Lippman, Josee Lajoie 著. C++ Primer 3e 中文版[M]. 北京:中国电力出版社, 2002. 521-523.
    [2]王挺,周会平,贾丽丽,徐锡山 著. C++ 程序设计[M]. 北京:清华大学出版社,2005.374-375.
    [3]侯捷 译, Stanley B.Lippman 著. 深度探索C++对象模型[M]. 北京:电子工业出版社, 2012. 152-169.


  • 相关阅读:
    Java内部类与异常类
    Java 继承和接口
    134. 加油站
    P1567 统计天数
    P2141 珠心算测验
    P1428 小鱼比可爱
    P1427 小鱼的数字游戏
    python中使用xlrd、xlwt操作excel表格详解
    同步机制
    CSS学习
  • 原文地址:https://www.cnblogs.com/xusw/p/5205861.html
Copyright © 2020-2023  润新知