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(, , );
    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 = ;
    p->mj = ;
    p->mk = ;

    cout << "After Change..." << endl;
    d.print();

    ;
}
/*输出结果:
sizeof(Demo) = 12
sizeof(Derived) = 16

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

(2)Derived对象的内存布局

【实例分析】单一继承

class Base
{
    public:
        Base()
        {
            mBase1 = ;
            mBase2 = ;
        }
        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 = ;
            mDerived2 = ;
        }
        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 = ;
        }
        virtual void funcA()
        {
            cout << "Base1::funcA()" << endl;
        }
        virtual void funcB()
        {
            cout << "Base1::funcB()" << endl;
        }
    private:
        int mBase1;
};

class Base2
{
    public:
        Base2()
        {
            mBase2 = ;
        }
        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 = ;
        }
        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[])(p); //假设vfunc函数在虚函数表中的索引值为1,
                  //参数p为this指针,因为成员函数默认都要传入this指针。

(3)实现多态:

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

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

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

(4)小结

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

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

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

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

【编程实验】多态本质分析——用C写面向对象

//51-2.h

#ifndef _51_2_H_
#define _51_2_H_

typedef void Demo;
typedef void Derived;

//父类
Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);

//子类
Derived* Derived_Create(int i, int j, int k);
int Derived_GetK(Derived* pThis);
int Derived_Add(Derived* pThis, int value);

#endif

//51-2.c

#include "51-2.h"
#include <malloc.h>

static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);

struct VTable   //2. 定义虚函数表数据结构
{
    int (*pAdd)(void*, int); //3. 虚函数表里面存储的内容(函数指针)
};

struct ClassDemo
{
    struct VTable* vptr; //1. 定义虚函数表指针 ==>虚函数表指针的类型???
    int mi;
    int mj;
};

struct ClassDerived
{
    struct ClassDemo d;
    int mk;
};

//父类的虚函数表
static struct VTable g_Demo_vtbl =
{
    Demo_Virtual_Add
};

//子类的虚函数表
static struct VTable g_Derived_vtbl =
{
    Derived_Virtual_Add
};

//*************************************父类***********************************
Demo* Demo_Create(int i, int j)
{
    struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo));

    )
    {
        ret ->vptr = & g_Demo_vtbl;  //4. 关联对象和虚函数表
        ret ->mi = i;
        ret ->mj = j;
    }

    return ret;
}

int Demo_GetI(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    return obj->mi;
}

int Demo_GetJ(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    return obj->mj;
}

//6. 定义虚函数表中指针所指向的具体函数
static int Demo_Virtual_Add(Demo* pThis, int value)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;

    return obj->mi + obj->mj + value;
}

//5. 分析具体的虚函数
int Demo_Add(Demo* pThis, int value)  //是个虚函数
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;

    //从虚函数表中找到真正的实现函数
    return obj->vptr->pAdd(pThis, value);
}

void Demo_Free(Demo* pThis)
{
    free(pThis);
}

//**********************************Derived类****************************
Derived* Derived_Create(int i, int j, int k)
{
    struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));

    )
    {
        ret -> d.vptr = &g_Derived_vtbl;
        ret -> d.mi = i;
        ret -> d.mj = j;
        ret -> mk = k;
    }

    return ret;
}

int Derived_GetK(Derived* pThis)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;

    return obj->mk;
}

//定义子类虚函数表中指针所指向的具体函数
static int Derived_Virtual_Add(Demo* pThis, int value)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;

    return obj->mk + value;
}

//分析虚函数
int Derived_Add(Derived* pThis, int value)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;

    return obj->d.vptr->pAdd(pThis, value);
}

//main.c

#include <stdio.h>
#include "51-2.h"

void run(Demo* p, int v)
{
    );  //多态
    printf("r = %d\n", r);
}

int main(void)
{
    Demo* pb = Demo_Create(, );
    Derived* pd = Derived_Create(, , );

    printf());
    printf()); 

    run(pb, );
    run(pd, );   

    Demo_Free(pb);
    Demo_Free(pd);

    ;
}

5. 小结

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

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

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

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

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

第51课 C++对象模型分析(下)的更多相关文章

  1. 第50课 C++对象模型分析(上)

    1. 回归本质 (1)class是一种特殊的结构体 ①在内存中class依旧可以看作变量的集合 ②class与struct遵循相同的内存对齐规则 ③class中的成员函数与成员变量是分开存放的.即每个 ...

  2. 第50 课C++对象模型分析——成员变量(上)

    C++对象模型,其实就是C++中的对象在内存中是如何排布的.C++中的对象包含了成员变量和成员函数,其实就是研究C++中的类对象它的成员变量和成员函数在内存中是如何排布的. 回归本质class 是一种 ...

  3. 第50 课C++对象模型分析——成员函数(上)

    类中的成员函数位于代码段中调用成员函数时对象地址作为参数隐式传递成员函数通过对象地址访问成员变量C++语法规则隐藏了对象地址的传递过程 #include<iostream> #includ ...

  4. 【转】证书的应用之一 —— TCP&SSL通信实例及协议分析(下)

    原文链接 前面两部分分别讲解了如何在.net程序中使用SSL实现安全通信以及SSL的通信过程,并通过抓包工具具体分析了ssl的握手过程,本文通过一个demo来模拟ssl协议,在TCP之上实现自己的安全 ...

  5. 第24课 - #pragma 使用分析

    第24课 - #pragma 使用分析 1. #pragma简介 (1)#pragma 是一条预处理器指令 (2)#pragma 指令比较依赖于具体的编译器,在不同的编译器之间不具有可移植性,表现为两 ...

  6. 用Python分析下王小波与李银河写情书最爱用哪些词

    作家王小波其实也是我国最早期的程序员,突发奇想,王小波写情书最喜欢用哪些词呢?用Python词云分析下! 直接上代码吧,有注释很好理解.输出的图片设置的比较大,所以运行的比较慢,可以适当把图片尺寸改小 ...

  7. TreeMap 还能排序?分析下源码就明白了

    Java 中的 Map 是一种键值对映射,又被称为符号表或字典的数据结构,通常使用哈希表来实现,但也可使用二叉查找树.红黑树实现. HashMap 基于哈希表,但迭代时不是插入顺序 LinkedHas ...

  8. TIOBE11月份编程语言排行榜:C非常接近Java,分析下中美的就业情况

    TIOBE公布11月份编程语言排行榜:C非常接近Java Swift挤进前10,分析下中美的就业情况. 我们先看看他们官方对数据的解读 本月TIOBE指数前20位出现了一些有趣的变动.首先,C语言现在 ...

  9. Elasticsearch7.X 入门学习第九课笔记-----聚合分析Aggregation

    原文:Elasticsearch7.X 入门学习第九课笔记-----聚合分析Aggregation 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. ...

随机推荐

  1. SQL Server SQL语句执行顺序

    执行顺序: 1.FROM:对FROM子句中前两个表执行笛卡尔积生成虚拟表vt1 2.ON:对vt1表应用ON筛选器只有满足 为真的行才被插入vt2 3.OUTER(join):如果指定了 OUTER ...

  2. SQL索引学习-聚集索引

    这篇接着我们的索引学习系列,这次主要来分享一些有关聚集索引的问题.上一篇SQL索引学习-索引结构主要是从一些基础概念上给大家分享了我的理解,没有实例,有朋友就提到了聚集索引的问题,这里列出来一下: 其 ...

  3. CentOS安装Erlang

    1.首先要安装编译源码用的编译器gcc&g++,安装方式很简单,先用yum search gcc搜索出包,然后选择适合自己的版本复制全名,用yum intall gcc_XXX来进行安装即可. ...

  4. ArcGis在Oracle中常用的sql

    ) m 查询坐标数据 更新坐标: update tableName set SHAPE = mdsys.sdo_geometry(2002,28420,null,mdsys.sdo_elem_info ...

  5. 流媒体一些server

    (1)darwin stream server (2)red5 (3)nginx rtmp

  6. block中出现此种报错: Incompatible block pointer types initializing 'float (^__strong)(float, float)' with an expression of type 'int (^)(float, float)'

    当block(代码块)的返回值是float时,应注意的地方:定义的返回值类型一定要与return的返回值类型一样 我们以两个数的四则运算来举例 在main.m文件中的四则运算中,我采用两种返回值类型( ...

  7. C语言-08-预处理器

    C预处理器,C Preprocessor简称CPP.C预处理器不是编译器的一部分,它是一个单独的文本替换工具,指示编译器在实际编译之前需要完成的工作. 常用的预处理器指令 #include 包含头文件 ...

  8. 所有Mac用户都需要知道的9个实用终端命令行

    通常情况下,只有高端用户才会经常用到终端应用.这并不意味着命令行非常难学,有的时候命令行可以轻松.快速的解决问题.相信所有Mac用户都尝试过命令行,今天为大家带来9个非常实用的命令行操作.一些命令行需 ...

  9. Java部分总结图片版(已经加上原图链接下载!!!)

    Java基础知识图片版(原图下载链接)

  10. 为Asp.net MVC中的RenderSection设置默认内容

    1. RenderSection的简单介绍 Asp.net MVC中提供了RenderSection方法,这样就能够在Layout中定义一些区块,这些区块留给使用Layout的view来实现比如我们定 ...