第51课 C++对象模型分析(下)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第51课 C++对象模型分析(下)相关的知识,希望对你有一定的参考价值。

1. 单继承对象模型

(1)单一继承

【编程实验】继承对象模型初探

技术分享
#include <iostream>

using namespace std;
class Demo
{
protected:
    int mi;
    int mj;
public:

    //虚函数
    virtual void print()
    {
       cout << "mi = " << mi << ", "
            << "mj = " << mj << endl;       
    }    
};

class Derived : public Demo
{
    int mk;
public:
    Derived(int i, int j, int k)
    {
        mi = i;
        mj = j;
        mk = k;
    }
    
    void print()
    {
        cout << "mi = " << mi << ", "
             << "mj = " << mj << ", "
             << "mk = " << mk << endl;       
    }
};

struct Test
{
    void* p;
    int mi;
    int mj;
    int mk;
};

int main()
{
    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;      //12,不是8,因为插入了一个虚函数表指针
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl; //16,不是12,原因同上
    
    Derived d(1, 2, 3);
    Test* p = reinterpret_cast<Test*>(&d);
    
    cout << endl;
    
    //以下实验证明带有虚函数的Derived的内存模型与Test结构体是一致的
    //1、大小相同。2、第1个成员变量是vptr指针;3、往后依次为mi、mj、mk
    cout << "Before Change..." << endl;
    d.print();
    
    p->mi = 10;
    p->mj = 20;
    p->mk = 30;
    
    cout << "After Change..." << endl;
    d.print();
    
    return 0;
}
/*输出结果:
sizeof(Demo) = 12
sizeof(Derived) = 16

Before Change...
mi = 1, mj = 2, mk = 3
After Change...
mi = 10, mj = 20, mk = 30
*/
View Code

(2)Derived对象的内存布局

【实例分析】单一继承

class Base
{
    public:
        Base()
        {
            mBase1 = 101;
            mBase2 = 102;
        }
        virtual void func1()
        {
            cout << "Base::func1()" << endl;
        }
        virtual void func2()
        {
            cout << "Base::func2()" << endl;
        }
    private:
        int mBase1;
        int mBase2;
};

class Derived : public Base
{
    public:
        Derived():
            Base()
        {
            mDerived1 = 1001;
            mDerived2 = 1002;
        }
        virtual void func2()
        {
            cout << "Derived::func2()" << endl;
        }
        virtual void func3()
        {
            cout << "Derived::func3()" << endl;
        }
    private:
        int mDerived1;
        int mDerived2;
};

技术分享

(3)结论

  ①vptr位于对象的最前端,非static的成员量根据其继承顺序和声明顺序排在其后。

  ②子类继承基类所声明的虚函数,即基类的虚函数地址会被复制到派生类的虚函数表中相应的项中。(即子类有自己一张独立的虚函数表

  ③子类中新加入的virtual函数跟在其继承而来的virtual后面。如本例中的的func3虚函数被添加到func2后面。

  ④若子类重写父类的virtual函数,则子类的虚函数表中该virtual函数对应的项会更新为新函数的地址。如本例中,子类重写func2虚函数,则虚函数表中的func2的项更新为子类重写的函数func2的地址

2.多重继承对象模型

(1)多重继承

【实例分析】多重继承

class Base1
{
    public:
        Base1()
        {
            mBase1 = 101;
        }
        virtual void funcA()
        {
            cout << "Base1::funcA()" << endl;
        }
        virtual void funcB()
        {
            cout << "Base1::funcB()" << endl;
        }
    private:
        int mBase1;
};

class Base2
{
    public:
        Base2()
        {
            mBase2 = 102;
        }
        virtual void funcA()
        {
            cout << "Base2::funcA()" << endl;
        }
        virtual void funcC()
        {
            cout << "Base2::funcC()" << endl;
        }
    private:
        int mBase2;
};

class Derived : public Base1, public Base2
{
    public:
        Derived():
            Base1(),
            Base2()
        {
            mDerived = 1001;
        }
        virtual void funcD()
        {
            cout << "Derived::funcD()" << endl;
        }
        virtual void funcA()
        {
            cout << "Derived::funcA()" << endl;
        }
    private:
        int mDerived;
};

(2)Derived对象的内存布局

技术分享 

(3)结论

  ①n重继承下,子类会有n张虚函数表。其中1个为主表,与第1个基类(如本例中的Base1)共享,其他为次表,与其他基类(如本例中的Base2)有关

  ②子类新声明的virtual函数,放在主虚函数表中。如本例中,子类新声明的与Base共享虚函数表。

  ③每一个父类的对象在子类的对象保持原样,并依次按声明次序排列。

  ④若子类重写virtual函数,则其所有父类中的签名相同的virtual函数会被改写。如本例中,子类重写了funcA函数,则两个虚函数表中的funcA函数的项均被更新为子类重写的函数的地址。这样做的目的是为了解决不同的父类类型的指针指向同一个子类对象,而能够调用到实际的函数

3. 关于虚析构函数的说明

(1)若父类声明了一个virtual析构函数,则其子类的析构函数会更新其所有的虚函数表中的析构函数的项,把该项中的函数地址更新为子类的析构函数的地址。

(2)因为当父类的析构函数为virtual时,若用户不显式提供一个析构函数,编译器会自动合成一个,所以若父类声明了一个virtual析构函数,其子类中必然存在一个virtual的析构函数,并用这个virtual析构函数更新虚函数表。

4. 多态的原理

(1)多态:使用父类指针(或引用)时调用虚函数时,会产生多态

Base* p;
……
p->vfunc(); //vfunc是Base中声明的virtual函数

(2)多态原理:

  ①由于指针p可以指向一个Base对象也可以指向其派生类的对象,而编译器在编译时并不知道p所指向的真实对象到底是什么,那么究竟如何判断呢?

  ②从C++对象的内存分布图中可以看出,尽管虚函数表的地址可能被更新,但在父类与子类间相同签名的虚函数在虚函数表中的索引值是不会变的。所以无论p指向的是Base对象,还是其派生类的对象,其virtual函数vfunc在虚函数表中的索引值是不变的(设均为1)

  ③在编译期,编译器无法知道具体的对象,但可以根据指针p所指向的对象的Base子对象(即,子类与父类重合的那部分内存)中虚函数表来实现函数调用。于是编译器可能把virtual函数调用的代码修改为如下的伪代码:

(*p->vptr[1])(p); //假设vfunc函数在虚函数表中的索引值为1,
                  //参数p为this指针,因为成员函数默认都要传入this指针。

(3)实现多态:

  ①若p指向一个Base对象,则调用父类Base本身的虚函数表中索引值为1的函数。

  ②若p指向一个Base派生类对象,则调用子类自身中虚函数表中索引值为1的函数,这样就实现了多态。(注意父类和子类的虚函数表是相互独立的,只不过子类会父类中复制一部分过来而己)

  ③这种函数调用是根据指针p所指的对象的虚函数表来实现的,在编译时由于无法确定指针p所指的真实对象,所以无法确定真实要调用哪一个函数,只有在运行时根据指针p所指的对象来动态决定。所以说,虚函数是在运行时动态绑定的,而不是在编译时静态绑定的

(4)小结

  ①当类中声明虚函数时,编译器会在类中生成一个虚函数表,该表是一个存储成员函数(虚函数)地址的数据结构。

  ②虚函数表是由编译器自动生成与维护的

  ③virtual成员函数会被放入虚函数表中

  ④存在虚函数时,每个对象都有一个指向虚函数表的指针

【编程实验】多态本质分析

 

5. 小结

(1)继承的本质是父子间成员变量的叠加

(2)C++中的多态是通过虚函数表实现

(3)虚函数表是由编译器自动生成与维护的

(4)虚函数的调用效率低于普通成员函数。

【参考资料】:C++对象模型之详述C++对象的内存布局

以上是关于第51课 C++对象模型分析(下)的主要内容,如果未能解决你的问题,请参考以下文章

第29课 指针和数组分析(下)

第60课 自定义模型类(下)

好课分享:c++对象模型探索高清完整资源

第59课 自定义模型类(中)

第57课 模型视图设计模式(下)

第53课 被遗弃的多重继承