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. windows 7文件误删shift+delete后找回

    昨天要还电脑了,结果脑子一抽,某个目录还没拷贝,shift+delete了整个目录,删除到一半,完了...我的源码都在里面还没出来啊...这TMD要命啊... 赶紧搜了一把,windows文件误删恢复 ...

  2. jquery实现轮播

    HTML代码: <div class="ad"> <ul class="slider"> <li><img src=& ...

  3. 正确匹配URL的正则表达式

    网上流传着多种匹配URL的正则表达式版本,但我经过试验,最好用的还是从stackoverflow上查到的: (https?|ftp|file)://[-A-Za-z0-9+&@#/%?=~_| ...

  4. android 回调函数一:基本概念

    1.概念 客户程序C调用服务程序S中的某个函数A,然后S又在某个时候反过来调用C中的某个函数B,对于C来说,这个B便叫做回调函数. 一般说来,C不会自己调用B,C提供B的目的就是让S来调用它,而且是C ...

  5. IOS 简单动画 首尾式动画

    首尾式动画 首尾式动画即通过实现控件由初始状态到结束状态的过程.(主要表现在控件的Frame 透明度 ) // // ViewController.m // CX 简单动画 // // Created ...

  6. Unity下载文件一(www协程下载)

    下载功能,是大多数游戏或者软件都需具备的一个基础模块,但是很多人却没有机会去写这个完整功能. 那么我就分享下我写该功能时的随笔整理 本文只说www协程下载,http的同步和异步下载放到下篇 这个简单: ...

  7. JDK8 API文档(下载)

    DK API文档 java SE 8 API文档: http://www.oracle.com/technetwork/java/javase/documentation/jdk8-doc-downl ...

  8. 关于Assets.car素材问题

    最近在做自己的第一个App,由于全程都是自己一个人完成,所以原型设计.素材都得自己找,自己改.遇到了提取Assets.car中的素材的问题,通过网络找到了2中解决方法: themeEngine 使 用 ...

  9. Sencha Cmd是什么

    Sencha Cmd的简介 ~~~~~~~~~~~~~~~~~~~~~~~ Sencha cmd 是一个跨平台的命令行工具,它从你应用程序的新创建到部署入产品中的整个生命周期都提供了许多自动化的执行任 ...

  10. OBIEE 11g 启动与停止包含服务器重启

    ORACLE_BIEE_HOME为biee安装路径 注意:默认建立的是"instance1"但是如果你安装过多次可能实例名是不一样(例如: instance2以此类推).因此,请找 ...