C++ 各种继承关系中的虚函数

C++ 各种继承关系中的虚函数探幽

最近在学习C++,卡在了实现多态的虚函数这里,之后通过阅读博文:C++虚函数表解析和自己的一些实践,有了点初步的理解,如果错误,欢迎指正。
首先建立如图所示的继承关系:
C++ 各种继承关系中的虚函数_第1张图片
代码如下:

class A
{
     
public:
    virtual void speak()
    {
     
        cout << "this is A"<<endl;

    }
};
class B
{
     
public:
    virtual void speak()
    {
     
        cout << "this is B" << endl;
    }


};

class C:public A
{
     
public:
    virtual void speak()
    {
     
        cout << "this is C" << endl;
    }
};

class D:public A,public B
{
     
public:
    virtual void speak()
    {
     
        cout << "this is D" << endl;
    }
};

class E:public C
{
     
public:
    virtual void speak()
    {
     
        cout << "this is E" << endl;
    }
};

虚函数实现多态的方式,从内存上去看

首先我们测试最基础的使用,通过基类A的引用调用C子类的函数:

void doPrint(A &typeClass)
{
     
    typeClass.speak();
}


void test()
{
     
    C realClass;
    doPrint(realClass);
}

int main()
{
     
    
    test();


    return 0;
}

结果:
C++ 各种继承关系中的虚函数_第2张图片
原理:当基类用virtual关键字定义了函数 speak()时,建立了虚函数表,且定义了一个指向此虚函数表的指针。子类C也继承了这个虚函数表和指向虚函数表的指针,并重写了此虚函数。

那么A类中的非虚函数会加入虚函数表吗,答案是不会的。

class A
{
     
public:
    virtual void speak()
    {
     
        cout << "this is A"<<endl;

    }
    void speak2()
    {
     
        cout << "this is A2" << endl;
    }
};
class C:public A
{
     
public:
    virtual void speak()
    {
     
        cout << "this is C" << endl;
    }

    void speak2()
    {
     
        cout << "this is C2" << endl;
    }
};

C++ 各种继承关系中的虚函数_第3张图片 C++ 各种继承关系中的虚函数_第4张图片

从内存上去理解是这样的:
编译器在内存中新建了一个C类的对象,这个对象中实际上只有一个指针变量,即指向虚函数表的指针,虚函数表保存的是函数的地址。如果A和C的speak方法的返回值,传参,函数名甚至是参数的顺序有所不同,那么虚函数表的第一个地址将指向的是虚函数A的speak方法,第二个地址指向C的speak方法。在我们的例子中,子类和父类函数重名,发生了动态多态,即重写了speak。那么问题来了,编译器是将虚函数表中原本指向A的speak方法的地址中的内容修改了,还是新开辟了一块空间用来存放C的speak方法,并将虚函数表中原本指向A的speak方法的地址修改成了新的空间的地址。
我们创建两个A和C的对象,看一下他们vfptr的值:

void test()
{
     
    C realClass;
    A typeClass;

    cout << "A类 虚函数地址值:" << *((int* )(*((int*)(&typeClass)))) << endl;

    cout << "C类 虚函数地址值:" << *((int *)(*((int*)(&realClass)))) << endl;

    //doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第5张图片
经过我的尝试,无论C类是否重写speak方法,虚函数表的地址始终是不同的,这也很容易理解,因为我们创建一个新的对象都新增了四个字节的内存空间。
那么虚函数表中存放的虚函数地址相同吗?
当C类重写speak时,我们打印一下指向speak的指针的值
C++ 各种继承关系中的虚函数_第6张图片
显然,两者的值不同,那么我们可以得到,当C类重写speak方法时,编译器新申请了一块内存用来存放C::speak方法,并将修改其虚函数表中存放的值。
那么当C类中没有A的重名函数,虚函数表的内存情况又是什么样子呢?

代码修改:

class A
{
     
public:
    virtual void speak()
    {
     
        cout << "this is A"<<endl;

    }
    
};

class C:public A
{
     
public:
    virtual void speak1()
    {
     
        cout << "this is C" << endl;
    }

    
};

C的虚函数表:
C++ 各种继承关系中的虚函数_第7张图片
此时C的虚函数表中存放的0号元素是从A继承的speak方法的指针,1号元素是成员方法speak1的指针。

打印地址:

void test()
{
     
    C realClass;
    A typeClass;

    cout << "A类 虚函数表元素0值:" << *((int *)(*((int*)(&typeClass)))) << endl;

    cout << "C类 虚函数表元素0值:" << *((int*)(*((int*)(&realClass)))) << endl;

    //doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第8张图片
两个值相等,也就是说明,C和A调用的是同一块地址。

各种继承关系下的虚函数

C++ 各种继承关系中的虚函数_第9张图片

如果C中的speak方法不加virtual关键字,E是否还能重写speak函数实现多态呢,答案是可以的

class C:public A
{
     
public:
    void speak()
    {
     
        cout << "this is C" << endl;
    }

    
};

当C的内容修改后,打印其内存布局:
C++ 各种继承关系中的虚函数_第10张图片

即使我们不加virtual关键字,由于其继承了A类,编译器会自动将speak加上 virtual , 无论继承了多少代,只要第N代的类中定义了虚函数,那么从第N代往后所有的类中都会存在一个虚函数表指针。

如果A类中定义了虚函数,B类中没有定义虚函数,那么D中的speak方法又是怎么样的呢?

执行如下代码:

void doPrint(A &typeClass)
{
     
    typeClass.speak();
}


void test()
{
     
    D realClass;
   

    doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第11张图片
执行如下代码:

void doPrint(B &typeClass)
{
     
    typeClass.speak();
}


void test()
{
     
    D realClass;
   

    doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第12张图片
执行如下代码:

void test()
{
     
    D realClass;
   
    realClass.speak();
    //doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第13张图片
执行如下代码:

void test()
{
     
    D realClass;
   
    realClass.B::speak();
    //doPrint(realClass);
}

C++ 各种继承关系中的虚函数_第14张图片
这种情况下,D中重载了两个speak方法,一个是从B继承的,一个是从A继承并重写的,D默认会执行D::speak()。

你可能感兴趣的:(c++)