通过虚函数表去理解C++多态

多态是C++的一个基本概念,简单来说就是程序运行时使用基类指针或引用去调用派生类的方法,被调用的方法必须是基类里定义过的虚函数。

本文从虚函数表去理解C++的多态。


一 类的虚指针vptr (virtual pointer)

只要类里有虚函数,那么它实例化的对象就会有虚指针,且位于对象所占内存空间的起始地方,虚指针指向虚函数表。下面来看下如何获取类的虚指针。

首先来看一个空类,

class Empty
{}

C++规定空类大小不能是0,而是1个字节。因为如果是0字节,那么就无法区分空类实例化出的多个对象,多个对象都是0字节,都没有内存地址,那么它们就是完全一样的了。

可以使用下面语句去查看空类大小,

printf("size: %d\n", sizeof(Empty));

下面来定义一个包含虚函数的类,

class Test
{
public:
    virtual void fn1(void)
    {
        printf("hello world 1\n");
    }

    virtual void fn2(void)
    {
        printf("hello world 2\n");
    }
};

使用下面的函数来获取对象的虚指针值,

int getVptr(Test * p)
{
     // 把对象指针转为int*,然后解引用就可以获取对象地址上前4个字节的值
    int data = *(int *)p;
    return data;
}

这里把虚指针值转为整形,注意,本文使用的是32位系统,所以指针值为4个字节。

我们在main函数里来测试一下,

int main(void)
{
    Test obj1, obj2;
    printf("obj1 size: %d; obj2 size: %d\n", sizeof(obj1), sizeof(obj2));
    printf("obj1 vptr: 0x%x\n", getVptr(&obj1));
    printf("obj2 vptr: 0x%x\n", getVptr(&obj2));
}

输出如下,
在这里插入图片描述
可以看出,这2个对象的虚指针值是相同的,也就是它们指向的是相同的虚函数表。

同样也可以看出,在计算类对象大小时,并不包含其拥有的函数,因为函数可以看做是一块代码单元,相同类的对象都可以使用它,在调用函数时,会传递一个隐式参数this来表示是哪个类对象在调用它。这也是为什么obj1和obj2的虚指针指向的是相同的虚函数表。


二 类的虚函数表vtbl (virtual table)

上节中,获取了类对象的虚指针值,虚指针指向虚函数表,虚函数表里存放虚函数指针,下面通过虚函数表来获取类成员函数指针,

typedef void (*fn_t)(void);

int main(void)
{
    Test obj1;

    int vptr = getVptr(&obj1); // 获取虚指针值
    int *vtbl = (int*)vptr; // 解引用得到虚函数表值
    
    fn_t func = (fn_t)vtbl[0]; // 获取第一个函数(即fn1)的地址值,并转为函数指针
    func(); // 通过函数指针去调用函数
}

输出打印如下,
在这里插入图片描述
可以看到类Test里定义的第一函数fn1被调用了。

我们可以写个函数来调用类里指定的函数,可以打印虚指针值和虚函数表里的函数地址值

void callFuncInVTable(Test * p, int index, bool printFlag=false)
{
    int vptr = getVptr(p); // 获取虚指针值
    if (printFlag)
    {
        printf("vptr: 0x%08x\n", vptr);
    }


    int *vtbl = (int*)vptr; // 解引用得到虚函数表值
    fn_t func = (fn_t)vtbl[index]; // 获取第一个函数(即fn1)的地址值,并转为函数指针
    if (printFlag)
    {
        printf("function addr: 0x%p\n", func);
    }
    
    func(); // 通过函数指针去调用函数
}

这样main函数可以写成如下这样,

int main(void)
{
    Test obj1;
    callFuncInVTable(&obj1, 0);
    callFuncInVTable(&obj1, 1);
}

打印如下,
在这里插入图片描述


三 理解多态

下面创建2个派生类,继承Test

class YYY : public Test
{
    virtual void fn1(void)
    {
        printf("goodbye 1\n");
    }

    virtual void fn2(void)
    {
        printf("goodbye 2\n");
    }
};


class ZZZ : public Test
{
    virtual void fn1(void)
    {
        printf("how are you 1\n");
    }

    virtual void fn2(void)
    {
        printf("how are you 2\n");
    }
};

同样,我们在main()函数里去查看这些派生类的对象的虚指针和成员函数调用,

int main(void)
{
    Test obj1;

    callFuncInVTable(&obj1, 0, true);
    callFuncInVTable(&obj1, 1, true);

    printf("\n\n");


    YYY yyyobj;

    callFuncInVTable(&yyyobj, 0, true);
    callFuncInVTable(&yyyobj, 1, true);

    printf("\n\n");

    ZZZ zzzobj;

    callFuncInVTable(&zzzobj, 0, true);
    callFuncInVTable(&zzzobj, 1, true);
}

注意,callFuncInVTable()的第一个参数的类型是Test *,即基类的指针类型,这也是多态的调用方法。

打印如下,
在这里插入图片描述
可以看到2个派生类和基类所创建的对象中虚指针是不同的,它们指向各自的虚函数表,并调用各自的成员函数。

这样我们就理解了基类指针或引用调用派生类方法的原理。

另外,由于2个派生类把基类里的虚函数都重写了,所以vtbl里的函数地址值也都是不一样的,如果派生类里只重写了部分虚函数,那么没被重写的虚函数它们的地址也在派生类的vtbl里吗?下面简单看下,

class TTT : public Test
{
    virtual void fn2(void)
    {
        printf("goodbye 2\n");
    }
};

main函数如下,

int main(void)
{
    Test obj1;

    callFuncInVTable(&obj1, 0, true);
    callFuncInVTable(&obj1, 1, true);

    printf("\n\n");


    TTT tttobj;

    callFuncInVTable(&tttobj, 0, true);
    callFuncInVTable(&tttobj, 1, true);
}

打印如下,
在这里插入图片描述
可以看到,TTT只重写了Test的fn2,但是TTT的类对象的vtbl里包含了基类里fn1的函数地址,这样就可以正确调用fn1了,只是fn1是基类里的定义的。


四 总结

本文从虚指针和虚函数表去理解C++多态的工作原理,本文中所写的函数并不能100%模拟真实程序运行时多态的运行过程,如传递this指针,不过大体上原理都是差不多的。

如果有写的不对的地方,希望能留言指正,谢谢阅读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值