NDK学习(六)C++的虚函数实现原理

本文探讨了C++中虚函数实现多态的原理,包括虚函数表的作用和内存布局。通过实例解释了如何通过虚函数表指针找到函数地址,以及在多继承情况下内存布局的特点。强调了尽量避免使用多继承的建议。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

谈到C和C++最大的不同之处,恐怕就是C++的多态了,通过一个基类指针调用函数的时候,能够知道根据具体的对象去调用合适的函数,如果是Java的话,因为有jvm的存在,我们可以脑补出这样一个画面,基类指针p指向一个对象,首先我们可以判断这个对象的类型,在Java中判断类型是很好实现的(类的信息储存在对象头中),获取到类信息之后,就沿着这个类的继承层次往上找,直到找到一个相同方法签名的方法。那么这个过程在C++中如何实现呢?这一篇就来了解虚函数的实现过程。

讲虚函数先插一个知识点,因为这个知识点和Java中的表现形式有点不一样,看看下面这段程序:

class A
{
public:
    void f1() {}
};
class B : public A
{
    void f1(int a) {}
};

int main()
{
    B *pb = new B();
    pb->f1();
    return 0;
}

这段程序根本编译不过,为什么?不是在A中有f1()这个方法吗?因为通过pb去调用f1()方法,pb指向B类,所以先从B开始找f1(),找到了一个同名的f1(int a)函数,然后就不会往上找了,因为在C++会有同名覆盖的概念,只要名称相同就不会继续查找,因此编译不通过。

好了,步入正题,C++对象为多态,引入了virtual关键字,一旦这个函数用virtual修饰了,那么在通过指针调用这个方法的时候就会根据特定对象去调用方法了, 没有virtual关键字修饰的方法是没有多态性质的。为了具体的实现这个性质又引入了虚函数表的概念,每一个拥有virtual方法的类都有一个虚函数表,一个类最多有且只有一个函数表,函数表的位置在数据区里面,函数表的每一项的值,对应每一个虚函数的地址,函数表每一项的值在编译的时候就已经确定了。相应C++对象的头部会有一个虚函数表指针,这个指针指向的就是虚函数表的位置,比如下面这个类的虚函数表:

class A
{
public:
    virtual void f1();
    virtual void f2();
};
void A::f1()
{
    cout << "f1 from A" << endl;
}
void A::f2()
{
    cout << "f2 from A" << endl;
}
class B : public A
{
public:
    virtual void f2();
    virtual void f3();
};
void B::f2() { cout << "f2 from B" << endl; }
void B::f3() { cout << "f3 from B" << endl; }

类对应的内存布局:
在这里插入图片描述
可以看到虚函数表有三项,这三项的组成过程是这样的,首先从A继承两个虚函数,于是分别把A::f1()和A::f2()两个函数的地址放进B类的虚函数表中,然后开始放B的虚函数,因为也定义了一个相同的函数f2(),因此就会覆盖掉A中的f2(),所以把B::f2()函数的地址放进了虚函数表的第二项,剩下B中的虚函数就依次根据定义的顺序放进虚函数表中。当通过基类A的指针来调用B::f2()时,首先通过该对象的虚函数表指针找到虚函数表的位置,然后根据f2在A中的偏移,在虚函数表中也偏移相应的位置,找到里面的函数地址,然后调用。
以上就是虚函数实现多态的原理,其实讲起来还是比较容易的,为了加深印象和验证上面内存布局的真实性,用一个例子来说明,类仍然是上面的形式:

int main()
{
    typedef void (*ptrFun)(void);
    B *p = new B();
    ptrFun fun = (ptrFun) * (long *)*(long *)p;
    fun();
    fun = (ptrFun) * ((long *)*(long *)p + 1);
    fun();

    fun = (ptrFun) * ((long *)*(long *)p + 2);
    fun();

    return 0;
}

输出结果是:

f1 from A
f2 from B
f3 from B

上面的测试程序要使用GCC才能运行,visual studio运行会出错,上面的指针操作稍微有一点复杂,以

ptrFun fun = (ptrFun) * (long *)*(long *)p

为例,首先要明确的是虚函数表里面的每一项的数值就是函数的地址,我们的任务是获取每一项里面的数值。
1、p是对象的首地址,首地址里面就是放了虚函数表的地址,把下面的值暂且记为p1

*(long *)p

2、获取到了虚函数表地址p1之后,需要获取到第一项里面的值,那么就是*p1,但是不能直接这样转,因为p1编译器不知道是什么类型,因此获取p1指向的数据就不知道该获取几个字节,因此需要把p1转化成(long*),再解引用,也就是下面的样子:

*(long*)p1

把上面组合起来就得到最终结果了。

多继承中虚函数表的形式

虽然我极力不建议使用多继承,因为多继承有妇孺皆知的二义性问题,而且多继承在很多方面表现的很怪异,不过针对多继承还是可以讲一下它的内存布局,比如下面的例子:

class A
{
public:
    virtual void f1() { cout << "f1 from A" << endl; }
    int a;
};
class B
{
public:
    virtual void f2() { cout << "f2 from B" << endl; }
    virtual void f3() { cout << "f3 from B" << endl; }
};

class C : public A, public B
{
    virtual void f2() { cout << "f2 from C" << endl; }
    virtual void f4() { cout << "f4 from C" << endl; }
};

int main()
{
    typedef void (*ptrFun)(void);
    C *p = new C();
    ptrFun fun = (ptrFun) * (long *)*(long *)p;
    fun();
    fun = (ptrFun) * ((long *)*(long *)p + 1);
    fun();
    fun = (ptrFun) * ((long *)*(long *)p + 2);
    fun();
    fun = (ptrFun) * (long *)*(long *)((long)p + 16);
    fun();
    fun = (ptrFun) * ((long *)*(long *)((long)p + 16) + 1);
    fun();
    // fun = (ptrFun) * ((long *)*(long *)((long)p + 16) + 2);
    // fun();
    return 0;
}

输出结果:

f1 from A
f2 from C
f4 from C
f2 from C
f3 from B

上面的代码也要用GCC来编译,并在ubuntu上面运行。
根据输出结果,可以得到C的内存布局:
在这里插入图片描述
可以看出,C的内存布局是一个线性的,和继承的先后顺序有关系。B的虚函数表比较好理解,A的虚函数表里面还有f2这个函数,这就让我们有点不理解,C::f2()不是已经在B的虚函数表中吗?其实可以根据内存布局和函数调用的原理去猜,依据就是虚函数的偏移量,试想通过C的指针去调用f2方法,因为C的指针指向的就是对象的头部,因此获取A的虚函数表比较简单,然后根据偏移量去计算出f2的地址,这样比较方便。因此f2也在A的函数表中了,另外C中多出来f4函数是添加在第一个继承类的虚函数表中。
最后还要说一句,尽量不要使用多继承!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值