C++多态的实现原理剖析

1、虚函数表

首先放上结论:C++的多态是通过一张虚函数表(Virtual Table) 来实现的, 简称为 V-Table。 在这个表中, 主要是一个类的虚函数的地址表, 这张表解决了继承、 覆写的问题, 保证其真实反应实际的函数。 这样, 在有虚函数的类的实例中这个表被分配在了这个实例的内存中, 所以, 当我们用父类的指针来操作一个子类的时候, 这张虚函数表就显得由为重要了, 它就像一个地图一样, 指明了实际所应该调用的函数。这里我们着重看一下这张虚函数表C++的编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下) 。 这意味着我们通过对象实例的地址得到这张虚函数表, 然后就可以遍历其中函数指针, 并调用相应的函数。

所以在有虚函数的类的对象,用sizeof查看的时候会多出来一个4,就是因为这个虚函数表的指针在这里

画个图解释一下。 如下所示

注意: 在上面这个图中, 在虚函数表的最后多加了一个结点, 这是虚函数表的结束结点, 就像字符串的结束符'0'一样, 其标志了虚函数表的结束。 这个结束标志的值在不同的编译器下是不同的。

通过一段代码了解一下:注意下方用了很多C语言中的指针问题,我在代码中进行注释说明了。

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

typedef void (*FUNC)();

int main()
{
    cout<<"Base size:"<<sizeof(Base)<<endl;
    Base b;
    cout<<"对象的起始地址: "<<&b<<endl;
    //这是在取对象的地址,取出来的就是对象的首地址
    //对象的起始地址,就类似一个函数指针数组的指针(就是虚函数表的指针)
    
    cout<<"虚函数表的地址: "<<(int**)*(int *)&b<<endl;
    //在取出起始地址&b之后,我们只想取得前四个字节,所以将上面的地址强制类型转为int*型,再解引用,就是虚函数表的地址,虚函数表的起始地址,再进行强制类型转化成int**
    
    cout<<"虚函数表第一个函数的地址:"<<*((int**)*(int *)&b)<<endl;
    //这个就比上面的多了一个*,就是解引用,对虚函数表首地址解引用,得到虚函数表的第一个函数的地址
    
    cout<<"虚函数表第二个函数的地址:"<<*((int**)*(int *)&b+1)<<endl;
    //这个比上边的多了一个+1,可以看出是指针加1,所以地址实际+4,指向下一个函数
    
    //注意不要转为 FUNC 来打印, cout 没有重载
    FUNC pf = (FUNC)(*((char**)*(int *)&b));
    pf();
    pf = (FUNC)(*((void**)*(int *)&b+1));
    pf();
    pf = (FUNC)(*((void**)*(int *)&b+2));
    pf();
    return 0;
}

2、一般继承(无虚函数覆写)

当Derive 类继承了Base类的时候,且没有覆写Base类中的虚函数,用图表示就是这样的:

此时对于实例 Derive d; 的虚函数表如下:

3、一般继承(有虚函数覆写)

覆盖父类的虚函数是很显然的事情, 不然, 虚函数就变得毫无意义。 下面, 我们来看一下, 如果子类中有虚函数重载了父类的虚函数, 会是一个什么样子?

例如下方的继承关系:

此时对于实例 Derive d; 的虚函数表如下:

覆写的 f()函数被放到了虚表中原来父类虚函数的位置。 没有被覆盖的函数依旧

这样, 我们就可以看到对于下面这样的程序:

Base *b = new Derive();
b->f();

由 b 所指的内存中的虚函数表的 f()的位置已经被 Derive::f()函数地址所取代, 于是在实际调用发生时, 是 Derive::f()被调用了。 这就实现了多态。

静态代码发生了什么
当编译器看到这段代码的时候, 并不知道 b 真实身份。 编译器能作的就是用一段代码代替这段语句。

Base *b = new Derive();
b->f();

1, 明确 b 类型。
2, 然后通过指针虚函数表的指针 vptr 和偏移量,匹配虚函数的入口。
3, 根据入口地址调用虚函数。

Last modification:August 7th, 2019 at 09:01 pm
如果觉得我的文章对你有用,请随意赞赏

Leave a Comment