zoukankan      html  css  js  c++  java
  • 用汇编的角度剖析c++的virtual

    多态是c++的关键技术,背后的机制就是有一个虚函数表,那么这个虚函数表是如何存在的,又是如何工作的呢?
    当然不用的编译器会有不同的实现机制,本文只剖析vs2015的实现。

    单串继承

    首先看一段简单的代码:

    class A {
    private:
        int a_value;
    public:
    	A() {};
    	virtual ~A() {};
    	virtual void my_echo() { std::cout << "A::my_echo" << std::endl; };
    	virtual void echo() { std::cout << "A::echo" << std::endl; };
    	virtual void print() { std::cout << "A::print" << std::endl; };
    };
    
    class B :public A {
    public:
    	B() {};
    	~B() {};
    	int b_value;
    	virtual void echo()override { std::cout << "B::echo" << std::endl; };
    	virtual void print()override { std::cout << "B::print" << std::endl; };
    
    	void B_Fun() { std::cout << "B::B_Fun" << std::endl; };
    };
    
    class C :public B {
    private:
    	int c_value;
    public:
    	C() {};
    	~C() {};
    	virtual void echo()override { std::cout << "C::echo" << std::endl; };
    	virtual void my_print() { std::cout << "C::print" << std::endl; };
    };
    
    

    C继承B,B继承A。

    先回顾下类的内存布局,看类C的内存布局。

    class C size(16):
            +---
     0      | +--- (base class B)
     0      | | +--- (base class A)
     0      | | | {vfptr}
     4      | | | a_value
            | | +---
     8      | | b_value
            | +---
    12      | c_value
            +---
    
    C::$vftable@:
            | &C_meta
            |  0
     0      | &C::{dtor}
     1      | &A::my_echo
     2      | &C::echo
     3      | &B::print
     4      | &C::my_print
     
    

    如上所示,虚函数表指针在首地址,占用一个指针大小,值得注意的是虚函数表首位指针是D的析构函数,那是因为基类有虚析构函数。

    来个简单调用。

    A* b = new C();
    b->print();//输出为 B::print
    

    print这个虚函数到底是怎么执行的?又是如何达到多态效果的呢?
    再看print调用的汇编代码。

    32位系统
        b->print();
    00EF6306  mov         eax,dword ptr [b]  
    00EF6309  mov         edx,dword ptr [eax]  
    00EF630B  mov         esi,esp  
    00EF630D  mov         ecx,dword ptr [b]  
    00EF6310  mov         eax,dword ptr [edx+0Ch]  
    00EF6313  call        eax  
    
    64位系统
        b->print();
    00007FF6336D2CBD  mov         rax,qword ptr [b]  
    00007FF6336D2CC1  mov         rax,qword ptr [rax]  
    00007FF6336D2CC4  mov         rcx,qword ptr [b]  
    00007FF6336D2CC8  call        qword ptr [rax+18h]  
    
    32位解析
    00EF6306  mov         eax, dword ptr[b]
        #将指针b变量指向内存地址的dword大小赋值给eax,实际就是获得b指向的地址,之后eax为b对象的实际地址,
    00EF6309  mov         edx, dword ptr[eax]
    	#将地址eax指向的内存地址,取出一个dword赋值给edx,之后edx就是虚函数表的首地址,
    00EF630D  mov         ecx, dword ptr[b]
    	#同上,将b实际指向的地址赋值给ecx,这句话作用是为了成员函数活得所需的this指针,为call做参数
    00EF6310  mov         eax, dword ptr[edx+0Ch]
    	#这里是将edx +12,因为是32位,所以这里是在虚函数表头地址向后偏移了4个函数指针,此时的地址就是虚函数print的指针地址了
    	#然后将print指针赋值给eax
    00EF6313  call        eax
    	#调用print函数
    

    64的汇编与32位相差不大,值得注意的是64位系统指针是8字节,所以是18h大小。可以看出在简单继承情况下,虚函数指针都是在首位的,而且虚函数表事一个共用表
    比如上面的单继承C=>B=>A,在new出C后,转换为基类B或者A时,是共用的一个虚函数表,接下来用一段代码来证明。

    
    #ifndef _WIN64  
    typedef unsigned int  pointer;//32位指针
    #else  
    typedef unsigned long long pointer;//64位指针
    #endif  
    
    //------------------------------
    B b;
    B* b1 = new B();
    
    A* a = new A();
    A *a1 = dynamic_cast <A*>(b1);
    B* b2 = dynamic_cast<B*>(a1);
    
    pointer vfptr_b = *(pointer*)&b;
    pointer vfptr_b1 = *(pointer*)b1;
    pointer vfptr_a = *(pointer*)a;
    pointer vfptr_a1 = *(pointer*)a1;
    pointer vfptr_b2 = *(pointer*)b2;
    
    std::cout
        << vfptr_b << "
    "
    	<< vfptr_b1 << "
    "
    	<< vfptr_a << "
    "
    	<< vfptr_a1 << "
    "
    	<< vfptr_b2 << std::endl;
    

    上面的代码意思就是将各种类型强行转换为指针,得到虚函数表的地址,从结果我们看到,只有vfptr_a不一样,也就是new A的虚函数表不一样,其余的,特比是dynamic_cast <A*>转换类型的虚函数表地址,还是为B的虚函数表地址。
    我们就可以得到一个简单结论,当是单继承时,子类指针转换为父类指针,指针地址不变,虚函数表不变,父类指针只用虚函数表的前半段。接下来从汇编结合虚函数表来分析单继承多态实现的原理。

    B* b = new C();
    b->print();
    
    A *a = dynamic_cast <A*>(b);
    a->echo();
    
    B* b2 = new B();
    b2->echo();
    
        B* b = new C();
    008B660D  push        10h  
    008B660F  call        operator new (08B131Bh)  //new 开辟内存
    ...........
    008B6633  call        C::C (08B14D3h)  //调用构造
    ...........
    00C06663  mov         dword ptr [b],ecx  //赋值给b
    
    	b->print();
    008B6666  mov         eax,dword ptr [b]  //得到对象地址
    008B6669  mov         edx,dword ptr [eax] //得到虚函数表首地址 
    008B666D  mov         ecx,dword ptr [b]  //this 指针
    008B6670  mov         eax,dword ptr [edx+0Ch]  //虚函数表中的print函数实际地址
    008B6673  call        eax  //调用print
    
    	A *a = dynamic_cast <A*>(b);
    008B667C  mov         eax,dword ptr [b]  
    008B667F  mov         dword ptr [a],eax  //编译器知道B是A的子类,所以,b指针转换为a指针,直接copy指针地址。
    
    	a->echo();
    008B6682  mov         eax,dword ptr [a]  
    008B6685  mov         edx,dword ptr [eax]  
    008B6689  mov         ecx,dword ptr [a]  //this指针
    008B668C  mov         eax,dword ptr [edx+8]  //echo在表里的偏移
    008B668F  call        eax  //执行call
    
        B* b2 = new B();
    ...........
    00E5669A  call        operator new (0E5131Bh)  
    ........... 
    00E566EE  mov         dword ptr [b2],ecx  
    
    	b2->echo();
    00E566F1  mov         eax,dword ptr [b2]  
    00E566F4  mov         edx,dword ptr [eax]  
    00E566F8  mov         ecx,dword ptr [b2]  
    00E566FB  mov         eax,dword ptr [edx+8]  //echo函数在表里的偏移,
    00E566FE  call        eax  
    
    

    new的对象C转换基类B,再转换为基类A,a->echo如何输出C::echo?下面画个图,描述了单一继承时候的虚函数表运行机制。

    多继承

    多继承和单继承在虚函数表处理上有很大不同,先看代码。

    class Base {
    private:
        int base_value;
    public:
    	Base() {};
    	virtual ~Base() {};
    	virtual void base_echo() { std::cout << "Base::base_echo" << std::endl; };
    
    };
    class A {
    private:
    	int a_value;
    public:
    	A() {};
    	virtual ~A() {};
    	virtual void a_echo() { std::cout << "A::a_echo" << std::endl; };
    	virtual void a_print() { std::cout << "A::a_print" << std::endl; };
    };
    class B {
    public:
    	B() {};
    	virtual ~B() {};
    	int b_value;
    	virtual void b_echo() { std::cout << "B::b_echo" << std::endl; };
    	virtual void b_print() { std::cout << "B::b_print" << std::endl; };
    };
    
    class C :public A,public B, public Base {
    private:
    	int c_value;
    public:
    	C() { };
    	~C() {};
    	virtual void a_echo()override { std::cout << "C::a_echo" << std::endl; };
    	virtual void b_print()override { std::cout << "C::b_print" << std::endl; };
    	virtual void c_echo() { std::cout << "C::c_echo" << std::endl; };
    };
    

    C类同时继承了A,B,Base三个类,此时内存和虚函数是怎样的呢?

    A,B,Base原有固定各自虚函数表

    Base::$vftable@:
            | &Base_meta
            |  0
     0      | &Base::{dtor}
     1      | &Base::base_echo
     
    A::$vftable@:
            | &A_meta
            |  0
     0      | &A::{dtor}
     1      | &A::a_echo
     2      | &A::a_print
     
    B::$vftable@:
            | &B_meta
            |  0
     0      | &B::{dtor}
     1      | &B::b_echo
     2      | &B::b_print
    

    然后再看C类的内存布局和虚函数表:

    class C size(28):
            +---
     0      | +--- (base class A)
     0      | | {vfptr}
     4      | | a_value
            | +---
     8      | +--- (base class B)
     8      | | {vfptr}
    12      | | b_value
            | +---
    16      | +--- (base class Base)
    16      | | {vfptr}
    20      | | base_value
            | +---
    24      | c_value
            +---
    
    C::$vftable@A@:
            | &C_meta
            |  0
     0      | &C::{dtor}
     1      | &C::a_echo
     2      | &A::a_print
     3      | &C::c_echo
    
    C::$vftable@B@:
            | -8
     0      | &thunk: this-=8; goto C::{dtor}
     1      | &B::b_echo
     2      | &C::b_print
    
    C::$vftable@Base@:
            | -16
     0      | &thunk: this-=16; goto C::{dtor}
     1      | &Base::base_echo
    

    非常惊讶的发现C类竟然有三个虚函数表,C::$vftable@A@:, C::$vftable@B@:, C::$vftable@Base@:,在同时继承的了A,B,Base三个基类里,各自都有一个虚函数表。
    而三个各自的虚函数表和源了A,B,Base三个基类虚函数表是不同的!!是单独属于C类的虚函数。更值得注意的是,C::c_echo是C的虚函数,但是却在C::$vftable@A@:表里面。

    从内存布局和虚函数布局可以得出简单结论,多继承时候,一般来说,对象会有同时继承类个数的虚函数表和表指针,子类的新虚函数会存在于第一个继承类的新虚函数

    未完待续!!!!!!!

  • 相关阅读:
    Leetcode 191.位1的个数 By Python
    反向传播的推导
    Leetcode 268.缺失数字 By Python
    Leetcode 326.3的幂 By Python
    Leetcode 28.实现strStr() By Python
    Leetcode 7.反转整数 By Python
    Leetcode 125.验证回文串 By Python
    Leetcode 1.两数之和 By Python
    Hdoj 1008.Elevator 题解
    TZOJ 车辆拥挤相互往里走
  • 原文地址:https://www.cnblogs.com/luconsole/p/6124241.html
Copyright © 2011-2022 走看看