C++多重继承下,函数指针的多态实现

最近在看《深度探索C++对象模型》在看到函数部分,遇到一个关于多重继承和函数指针如何实现多态的问题。如果你想看懂本篇内容,需要先理解c++中变量的内存模型,一点点的汇编基础。

本篇博客是基于微软编译器

0X00在单继承情况下函数指针多态实现

类成员虚函数指针可以实现多态嘛?

我们有一段单继承的代码,并且定义了一个函数指针,我们问题是:类成员虚函数指针在不同的对象调用情况下可不可以实现多态?

class Base{
     
public:
    virtual void func() {
     
        cout << "Base::func" << endl;
    }
};

class Imp : public Base{
     
public:
    virtual void  func() {
     
        cout << "Imp::func" << endl;
    }
};



int main() {
     
    void (Base::*test)() = &Base::func;//定义了Base中的func的函数
    Base *b = new Base;
    (b->*test)();

    b = new Imp;
    (b->*test)();
    return 1;
}

用子类Imp和父类Base的变量,我们都用Base的指针指向这些成员,这个时候我们用test虚函数指针调用,结果是如下:
单继承下函数指针是否支持多态
答案是可以的,也就是说。虚函数指针在不同的对象调用下也是可以支持多态。那么它是怎么做的呢?

虚函数指针如何支持多态

首先我们可以排除虚函数指针肯定不是直接指向函数地址的(非虚函数指针是直接指向函数地址)。
我们跟踪下刚刚我们那段代码在调用函数的时候做了什么。
接下来我们会展示一波汇编代码。

    void (Base::*test)() = &Base::func;
    Base *b = new Base;
    (b->*test)();//执行这段代码时候发生了什么

我们看当执行(b->*test)()时候发生了什么
C++多重继承下,函数指针的多态实现_第1张图片
首先会把调用的对象放入一个ecx的寄存器里。然后调用虚函数指针test指向的函数,我们接下来看下这个函数就一条语句直接跳到叫Base::`vcall{0}的函数地址。
调用Base::vacall

我们看Base::`vcall{0}中做了什么
直接调用
第一步,将ecx寄存器中的地址指向的内容取出到eax。如果了解c++内存模型的小伙伴都知道(不知道的可以看下文章)一个对象中第一项是虚函数表指针。然后将eax(虚函数表)中(其实是第一个虚函数)地址直接取出直接并且跳转到该地址,然我们看下到跳到哪了。
跳转到Base::func
到最后调用跳转到Base::func了,现在成功找到了需要调用的函数。
看不懂的,我用简单图总结下具体做了什么
C++多重继承下,函数指针的多态实现_第2张图片

第1步:调用虚函数指针指向的函数,实际上调用的是Base::`vcall{0}这个函数
第2步:在vacll{0}函数中将传入的对象的虚函数表中的第一项取出,然后调用
可以看出我们的函数指针并不是指向实际的函数地址,而是先调用编译器为我们设置的一个函数,这个函数中我们会取出我们对象的虚函数表然后调用响应位置的虚函数。也就是说是根据传入的虚函数表才会确定调用的是哪一个函数。

0X01在多继承情况下函数指针多态实现

看了单继承的实现,那么在多继承的情况下有什么特点呢?

为什么在多继承下情况比较复杂,因为在多继承下我们会有两个虚函数表,程序怎么确定是哪一个虚函数表呢?

以下面代码为实例:

//基类P1
class P1{
     
public:
	//拥有虚函数funcP1
    virtual void funcP1() {
     
    	//输出一段信息
        cout << "P1 invoke " << endl;
    }
};
//基类p2
class P2{
     
public:
	//拥有虚函数funcP2
    virtual void funcP2() {
     
    	//输出一段信息
        cout << "P2 invoke " << endl;
    }
};


//多重继承,先继承p2,后继承p1,P3内存结构会很复杂
class P3 : public P2, public P1{
     
public:
	//重写P1中的虚函数funcP1()
    virtual void funcP1() {
     
        cout << "P3 invoke " << endl;
    }
    //拥有自己的虚函数
    virtual void funcP3() {
     
        cout << "funcP3" << endl;
    }
};

int main() {
     
	//定义一个P1类中funcP1虚函数指针
    float (P1::*func)() = &P1::funcP1;
    //new了一个P3的对象
    P3 *p3 = new P3;

	//P3对象通过P1类中funcP1虚函数指针调用funcP1函数
    (p3->*func)();

    return 1;
}

运行结果如下图,还是准确定位到了相应的函数
运行结果
给大家看下P3内存结构的图
C++多重继承下,函数指针的多态实现_第3张图片
其中有两个虚函数表,分别是从P2和P1中继承下来的,但是对于我们要调用的funcP1这个函数它是在p1的虚函数表中记录的。在我们定义P1的函数指针时候它并不知道自己在P3的内存结构中会是个什么情况,所以它是怎么确定哪一个虚函数表呢?

实际上,程序并不会去挑哪个虚函数表,它只会去调用第一个虚函数表。如果我们是函数指针不是在第一个虚函数表,而是在第二个虚函数表,这个时候我们编译器会自己计算需要偏移,把变量指针加上偏移(变相的确定了哪个虚函数表)。

大家注意在编译期,P3是知道这个函数指针是P1类型的函数指针,P1的在P3中的内存也是可以确定的,也就是说这些完全可以确定,比那一起只要将P3成员指针偏移4(sizeof(P2))位就可以了。之后做的事就和单继承一样了。

我们同样看下汇编后的代码
C++多重继承下,函数指针的多态实现_第4张图片

编译根据函数指针和对象指针的类型,直接计算出P3到P1的偏移量,把内存地址指向P1的偏移,然后和单继承方式调用一个叫P1::`vcall{0}的函数就成功了。

你可能感兴趣的:(C++拾遗,c++,函数指针,多继承,虚函数,多态)