c++多态及虚函数表内部原理实战详解

1.多态实现方式

c++的多态机制主要是靠虚函数来实现。具体来说,就是用父类的指针指向子类的实例,然后通过父类指针调用子类对象中的成员函数。这样,就实现了父类指针的“多态"。

想了解虚函数实现机制,就必须先了解对象的存储方式。

2.类的存储方式

我们以为的存储方式是这样:c++多态及虚函数表内部原理实战详解_第1张图片
上面的图,表示对象的数据和函数代码都要分配内存空间,这样内存的利用效率显然较低,因此实际上存储方式是这样:
c++多态及虚函数表内部原理实战详解_第2张图片
每个对象占用存储空间的只是该对象的数据部分(虚函数指针和虚基类指针也属于数据部分),函数代码属于公用部分。我们常说的“A对象的成员函数,是从逻辑的角度而言的,而成员函数的物理存储方式其实不是如此。

3.c++内存分区

C++的内存分区大概分成五个部分:

1.栈(stack):是由编译器在需要时自动分配,不需要时自动清除的变量存储区,通常存放局部变量、函数参数等。
2.堆(heap):是由malloc等分配的内存块,和堆十分相似,用free来释放
3.自由存储区:是由new分配的内存块,由程序员释放(编译器不管),一般一个new与一个delete对应,一个new[]与一个delete[]对应,如果程序员没有释放掉,资源将由操作系统在程序结束后自动回收
4.全局/静态存储区:全局变量和静态变量被分配到同一块内存中
5.常量存储区:这是一块特殊存储区,里边存放常量,不允许修改

你可能会问:静态成员函数和非静态成员函数都是在类的定义时放在内存的代码区的,因而可以说它们都是属于类的,但是类为什么只能直接调用静态类成员函数,而非静态类成员函数(即使函数没有参数)只有类对象才能调用呢

原因是:类的非静态类成员函数其实都内含了一个指向类对象的指针型参数(即this指针),因此只有类对象才能调用(此时this指针有实值)

4.虚函数表

每个包含虚函数的类都含一个虚函数表 Virtual Table,简称为VTable。在c++中,编译器会保证虚函数表的指针会存在对象实例最前面的位置。于是我们可以通过对象实例地址得到这张虚函数表,然后遍历其中函数指针,就可以调用相应的函数。

总结起来就是以下几点:
1.每个包含虚函数的类都有一个虚函数列表。
2.虚函数表的指针存在对象实例最前面的位置。
3.派生类虚表的虚函数地址排列顺序与基类中虚函数地址排列顺序完全一致。如果子类中包含有自身虚函数,会排列在后面。
4.虚表可以继承。如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。

5.通过代码来验证虚表原理

首先我们再次明确一下:虚函数表的指针存在对象实例最前面的位置(目前各个编译器基本都是是这样设置)

假设我们有Base类

class Base {
    public:
        virtual void f() {cout<<"base::f"<<endl;}
        virtual void g() {cout<<"base::g"<<endl;}
        virtual void h() {cout<<"base::h"<<endl;}
};

我们试图通过Base类的实例来得到虚函数表。

void virtual_func() {
    typedef void(*Fun)(void);
    Base b;
    Fun pFun = NULL;
    cout << "虚函数表地址:" << (long*)(&b) << endl;
    cout << "虚函数表 — 第一个函数地址:" << (long*)*(long*)(&b) << endl;
    pFun = (Fun)*((long*)*(long*)(&b));
    pFun();
    pFun=(Fun)*((long*)*(long*)(&b)+1);
    pFun();
    pFun=(Fun)*((long*)*(long*)(&b)+2);
    pFun();
    pFun=(Fun)*((long*)*(long*)(&b)+3);
    // pFun(); 会报错
    cout<<pFun<<endl; // 此内存区域现在不知道是啥,会输出一随机值。

    
}

int main(int argc, char const *argv[])
{
    virtual_func();
    return 0;
}

上面的代码我们逐句解读一下

typedef void(*Fun)(void);

定义了一个函数指针,该函数指针的参数为void (没有),返回类型为void。

重点看看这句(long*)*(long*)(&b):
&b,取b的首地址,根据我们前面所说,即为虚函数表的地址。然后我们将其(long*)(&b),强制转换为long型指针,也就是指向虚函数表这个数组中首元素地址的指针。
注意(long*)(&b)本身是一个指针,里面存的数据也是指针,即虚表数组首元素地址。因此,我们对(long*)(&b)这个数组取值,再转成long型指针,得到的就是虚表首元素,即第一个虚函数Base::f()的地址了。

pFun = (Fun)*((long*)*(long*)(&b));
又因为pFun是由Fun这个函数声明的函数指针,所以相当于是Fun的实体,必须再将这个地址转换成pFun认识的类型,即加上(Fun)*进行强制转换。

整个过程简单来说,就是从bObj地址开始读取四个字节的内容(&bObj),然后将这个内容解释成一个内存地址((long*)(&bObj)),再访问这个地址((long*) * (long*)(&bObj)),最后将这个地址中存放的值再解释成一个函数的地址((Fun) * ((long*) * (long*)(&bObj)))

代码的最终输出如下:

虚函数表地址:0x7ffeeb8b5328
虚函数表 — 第一个函数地址:0x10435c070
base::f
base::g
base::h
1

套用一张图,我们就能比较清晰地看明白上面的过程
c++多态及虚函数表内部原理实战详解_第3张图片

参考文献

https://www.cnblogs.com/zhxmdefj/p/11594459.html

你可能感兴趣的:(c/c++,c++,多态,虚函数,虚函数表,内存模型)