C++继承多态之多态构成原理(对象内存结构、虚函数指针、虚函数表详解以及编译和运行时绑定)

1. 虚函数指针vfptr与虚函数表vftable

class Base {
public:
    Base(int data) :ma(data) { cout << "Base(int data)" << endl; }
    ~Base() { cout << "~Base()" << endl; }

    virtual void show() { cout << "Base::show()" << endl; }
    virtual void show(int val) { cout << "Base::show(int)" << endl; }
protected:
    int ma;
};

class Deriver :public Base {
public:
    Deriver(int data) :mb(data), Base(data) { cout << "Deriver(int data)" << endl; }
    ~Deriver() { cout << "~Deriver()" << endl; }
    void show(){ cout << "Deriver::show()" << endl; } 
private:
    int mb;
};

1.1 引出虚函数指针(virtual function point)以及虚函数表(virtual function table)

int main() {
    Base b(99);
    Deriver d(88);

    cout << sizeof(Base) << endl;
    cout << sizeof(Deriver) << endl;
    cout << sizeof(b) << endl;
    cout << sizeof(d) << endl;
    return 0;
}

打印执行结果:

8
12
8
12

我们不难发现,本应该有4字节大小的Base类,大小却是8,多出来4字节,对象大小也多出4字节。
原因是因为:

  • 一个类含有虚函数时,每个类都会有一张虚函数表,在编译时,由编译器生成,存放在.rodata段的内存里,这样整个类就比没有虚函数的类大了4字节;
  • 当一个类有虚函数表时,构造出来的对象的前4字节都会有一根虚函数指针(vfptr具有高优先级,所以在前4字节),用来保存虚函数表的地址,所以构造出来的对象会比没有虚函数时多出4个字节大小
  • 注意,虚函数表是属于类的,而不是属于对象的,是该类构造出所有对象所共享的。

1.2 虚函数表中存放的内容

  1. RTTI 运行时的类型信息,当前属于哪个类的表,保存的就是哪个类的类型;
  2. 对象中vfptr起始地址在整个对象内存中偏移量,因为vfptr具有最高的优先级,一般放在对象的前4字节,所以偏移量一般为0;
  3. 该类中存在的虚函数的地址

在这里插入图片描述
在这里插入图片描述

注意:
子类继承父类,如果子类含有虚函数,那么也会产生虚函数表,如果子类中的方法重写了父类的虚函数,那么子类中该函数地址将覆盖掉继承来的虚函数表中的那个父类虚函数的地址,即上图中&Base::show()在子类的虚函数表中被&Deriver::show()覆盖,由于子类没有重写&Base::show(int),所以不会发生覆盖

2. 哪些方法可以被写成虚函数?

什么样的函数可以构成虚函数呢?

3. 静态(编译期确定调用哪个函数)绑定(不存在virtual函数时的绑定)

class Base{
public:
    Base(int data):ma(data){ cout << "Base(int data)" << endl; }
    ~Base(){ cout << "~Base()" << endl; }

    void show(){ cout <<"Base::show()" <<endl; }
    void show(bool val) { cout <<"Base::show(bool)" <<endl; }
private:
    int ma;
};

class Deriver:public Base{
public:
    Deriver(int data):mb(data),Base(data){ cout << "Deriver(int data)" << endl; }
    ~Deriver(){ cout << "~Deriver()" << endl; }

    void show(){ cout <<"Deriver::show()" <<endl; }
private:
    int mb;
};

int main(){

    Base b(100);
    Deriver d(1000);
    b.show(); //Base
    d.show(); //Deriver


    Base *bp = &d; 
    Base *dp = &b; 
    bp->show();// Base
    dp->show();// Base

    Deriver *bpp = static_cast<Deriver*>(&b);
    Deriver *dpp = &d;
    bpp->show(); //Deriver
    dpp->show(); //Deriver

    return 0;
    /*
    不含虚函数,不会进行运行时的类型检查,只会根据指针或者引用的类型进行判断调用基类还是派生类的方法
    */
}

执行结果:

Base(int data)
Base(int data)
Deriver(int data)
Base::show()
Deriver::show()
Base::show()
Base::show()
Deriver::show()
Deriver::show()
~Deriver()
~Base()
~Base()

show()调用点处的汇编代码:

    bp->show();// Base
001E2A58  mov         ecx,dword ptr [bp]  
001E2A5B  call        type_info::name (01E14C4h) 
    dp->show();// Base
001E2A60  mov         ecx,dword ptr [dp]  
001E2A63  call        type_info::name (01E14C4h)  

    Deriver* bpp = static_cast<Deriver*>(&b);
001E2A68  lea         eax,[b]  
001E2A6B  mov         dword ptr [bpp],eax  
    Deriver* dpp = &d;
001E2A6E  lea         eax,[d]  
001E2A71  mov         dword ptr [dpp],eax  
    bpp->show(); //Deriver
001E2A74  mov         ecx,dword ptr [bpp]  
001E2A77  call        Base::show (01E14C9h)  
    dpp->show(); //Deriver
001E2A7C  mov         ecx,dword ptr [dpp]  
001E2A7F  call        Base::show (01E14C9h)  //编译就确定了调用哪个函数

总结:

  1. 正如我们预料之中的,p和*p的类型与Base相关,与后面指向实体类型无关,与指针定义的类型相关,因为指针或者引用指向的对象中不存在vfptr,没有RTTI也就不会访问去RTTI,仅根据指针的类型来判断调用哪个类中的方法,而在编译器就确定了类型
  2. 根据汇编我们不难发现,编译时,我们的point->show()代码被编译为call Base::show和Deriver::show或者根据对象类型来调用相应方法,也就是编译期就已经确定调用的是Base::show还是Deriver::show ,这又被称为静态的绑定

4. 动态绑定(存在重写时的绑定),深入理解RTTI的作用以及多态的构成原理

class Base {
public:
    Base(int data) :ma(data) { cout << "Base(int data)" << endl; }
    ~Base() { cout << "~Base()" << endl; }

    virtual void show() { cout << "Base::show()" << endl; }
    virtual void show(bool val) { cout << "Base::show(bool)" << endl; }
private:
    int ma;
};

class Deriver :public Base {
public:
    Deriver(int data) :mb(data), Base(data) { cout << "Deriver(int data)" << endl; }
    ~Deriver() { cout << "~Deriver()" << endl; }

    void show() { cout << "Deriver::show()" << endl; }
private:
    int mb;
};

int main() {

    Base b(100);
    Deriver d(1000);
    b.show(); //Base
    d.show(); //Deriver

    Base* bp = &d;
    Base* dp = &b;
    bp->show(); //Deriver
    dp->show(); //Base

    Deriver* bpp = static_cast<Deriver*>(&b);
    Deriver* dpp = &d;
    bpp->show(); //Base
    dpp->show(); //Deriver

    Base& rb = b;
    Base& rd = d;
    rb.show(); // Base
    rd.show(); //Deriver

    return 0;
    /*
    无论前面什么类型的指针或者引用,只需要关心右边实体中存放的RTTI即可,因为继承中有虚函数
    然后运行时根据RTTI的类型调用相应类中的虚函数
    */
}

执行结果:

Base(int data)
Base(int data)
Deriver(int data)
Base::show()
Deriver::show()
Deriver::show()
Base::show()
Base::show()
Deriver::show()
Base::show()
Deriver::show()
~Deriver()
~Base()
~Base()

show()调用点处的汇编代码:

   b.show(); //Base
001A2BC1  call        4DA82BC5  
    d.show(); //Deriver
001A2BC6  shr         al,1  
001A2BC8  call        8E1A2BB1  

    Base* bp = &d;
001A2BCD  inc         ebp  
001A2BCE  ror         byte ptr [ecx+458DC445h],1  
    Base* dp = &b;
001A2BD4  in          al,89h  
001A2BD6  inc         ebp  
001A2BD7  mov         eax,8BC4458Bh  
    bp->show(); //Deriver
...
001A2BE7  cmp         esi,esp  
001A2BE9  call        __RTC_CheckEsp (01A12A3h)  
    dp->show(); //Base
...
001A2BFD  cmp         esi,esp  
001A2BFF  call        __RTC_CheckEsp (01A12A3h)  

    Deriver* bpp = static_cast<Deriver*>(&b);
001A2C04  lea         eax,[b]  
001A2C07  mov         dword ptr [bpp],eax  
    Deriver* dpp = &d;
001A2C0A  lea         eax,[d]  
001A2C0D  mov         dword ptr [dpp],eax  
    bpp->show(); //Base
...
001A2C1F  cmp         esi,esp  
001A2C21  call        __RTC_CheckEsp (01A12A3h)  
    dpp->show(); //Deriver
...
001A2C35  cmp         esi,esp  
001A2C37  call        __RTC_CheckEsp (01A12A3h)  

    Base& rb = b;
001A2C3C  lea         eax,[b]  
001A2C3F  mov         dword ptr [rb],eax  
    Base& rd = d;
001A2C42  lea         eax,[d]  
001A2C45  mov         dword ptr [rd],eax  
    rb.show(); // Base
...
001A2C57  cmp         esi,esp  
001A2C59  call        __RTC_CheckEsp (01A12A3h)  
    rd.show(); //Deriver
... 
001A2C6D  cmp         esi,esp  
001A2C6F  call        __RTC_CheckEsp (01A12A3h)  

注意:__RTC_CheckEsp 为C运行时的类型检查,即就是程序在运行时会查看RTTI的类型,然后根据RTTI的类型调用类中的虚函数。
有一些比较旧的编译器在这里直接回call eax,调用一个寄存器,这就是静态绑定和动态绑定的差别,动态绑定call的内容只有在程序真正运行起来才知道里面是哪个函数,静态绑定在编译阶段就已经确定了。

5. 多态的构成条件

  1. 类中存在虚函数,不论是当前类自身的,还是从父类继承而来的,只要存在虚函数
  2. 用指针或者引用去调用该虚函数即可构成。

补充:用对象调用虚函数不可构成多态,一定是指针和引用。

6.多态发生时,如何判断调用的是谁的虚函数?

一般来说,查看指针或者引用所指向的对象的内存中存放的RTTI是哪个类就调用哪个类中的虚函数。


版权声明:本文为KingOfMyHeart原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。