关于C++对象继承中的内存布局示例详解

前言

本文给大家介绍的是关于C++对象继承的内存布局的相关内容,分享出来供大家参考学习,在开始之前说明下,关于单继承和多继承的简单概念可参考此文章

以下编译环境均为:WIN32+VS2015

虚函数表

对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。

首先先通过一个例子来引入虚函数表,假如现在有三个类如下:

class A //包含虚函数的类
{
public:
 virtual void func1()
 {}

 virtual void func2()
 {}
};

class B//空类
{};

class C //包含成员函数不包含成员变量的类
{
 void fun()
 {}
};

void Test1()
{
 cout << sizeof(A) << endl;
 cout << sizeof(B) << endl;
 cout << sizeof(C) << endl;
}

就上述的代码,将会分别输出4,1,1

造成A的大小为4的原因就是:在A中存放了一个指向A类的虚函数表的指针。而32位下一个指针大小为4字节,所以就为4。

A类实例化后在内存中对应如下:

注:在虚函数表中用0来结尾。

通过内存中的显示我们就能知道编译器应该将虚函数表的指针存在于对象实例中最前面的位置,所以可以&a转成int*,取得虚函数表的地址,再强转成(int*)方便接下来可以每次只访问四个字节大小(虚函数表可看做是一个函数指针数组,由于32位下指针是4字节,所以转为(int*))。将取得的int*指针传给下面的打印虚函数表的函数,就能够打印出对应的地址信息。

typedef void(*FUNC) ();

//int*VTavle = (int*)(*(int*)&a)
//传参完成后就可打印出对应的信息。
void PrintVTable(int* VTable)
{
 cout << " 虚表地址>" << VTable << endl;
 for (int i = 0; VTable[i] != 0; ++i)
 {
 printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
 FUNC f = (FUNC)VTable[i];
 f();
 }
 cout << endl;
}

接下来就来分析各种继承关系中对应的内存模型以及虚函数表

单继承(无虚函数覆盖)

class A
{
public:
 virtual void func1()
 {
 cout << "A::func1" << endl;
 }

 virtual void func2()
 {
 cout << "A::func2" << endl;
 }

public:
 int _a;
};

class B : public A
{
public:
 virtual void func3()
 {
 cout << "B::func3" << endl;
 }

 virtual void func4()
 {
 cout << "B::func4" << endl;
 }

public:
 int _b;
};
void Test1()
{
 B b;
 b._a = 1;
 b._b = 2;

 int* VTable = (int*)(*(int*)&b);
 PrintVTable(VTable);
}

将内存中的显示和我们写的显示虚函数表对应起来如下:

小结:

1)虚函数按照其声明顺序放于表中。

2)父类的虚函数在子类的虚函数前面。(由于子类单继承父类,直接使用父类的虚函数表)

一般继承(成员变量+虚函数覆盖)

在上面例子进行稍微修改,使得子类中有对父类虚函数的覆盖,进行和之前同样的测试:

class A
{
public:
 virtual void func1()
 {
 cout << "A::func1" << endl;
 }

 virtual void func2()
 {
 cout << "A::func2" << endl;
 }

public:
 int _a;
};

class B : public A
{
public:
 virtual void func1()
 {
 cout << "B::func1" << endl;
 }

 virtual void func3()
 {
 cout << "B::func3" << endl;
 }

public:
 int _b;
};

小结:

1)覆盖的func()函数被放到了虚表中原来父类虚函数的位置。

2)没有被覆盖的函数依旧。

多重继承(成员变量+虚函数覆盖)

class A
{
public:
 virtual void func1()
 {
 cout << "A::func1" << endl;
 }

 virtual void func2()
 {
 cout << "A::func2" << endl;
 }

public:
 int _a;
};

class B
{
public:
 virtual void func3()
 {
 cout << "B::func1" << endl;
 }

public:
 int _b;
};

class C : public A , public B
{
 //覆盖A::func1()
 virtual void func1()
 {
 cout << "C::func1()"<<endl;
 }

 virtual void func4()
 {
 cout << "C::func4()" << endl;
 }
public:
 int _c;
};

再次调试观察:

小结:

多重继承后的子类将与自己第一个继承的父类公用一份虚函数表。(上述例子中A为C的第一个继承类)

菱形继承(成员变量 + 虚函数覆盖)

class A
{
public:
 virtual void func1()
 {
 cout << "A::func1" << endl;
 }

public:
 int _a;
};

class B : public A
{
public:
 virtual void func2()
 {
 cout << "B::func2" << endl;
 }

public:
 int _b;
};

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

public:
 int _c;
};

class D : public B , public C
{
 virtual void func2()
 {
 cout << "D::fun2()" << endl;
 }

 virtual void func4()
 {
 cout << "D::fun4()" << endl;
 }
public:
 int _d;
};

void Test1()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._b = 3;
 d._c = 4;
 d._d = 5;

 int* VTable = (int*)(*(int*)&d);
 PrintVTable(VTable);
}

掌握了单继承和多继承的规律,按照总结的一步步分析,就可以最终得到D的虚函数表。

由于子类B继承父类A,所以B与A公用一个虚函数表,又因为B是D多继承中的第一个继承的类,所以B,D共用一个虚函数表。

菱形的虚拟继承(成员变量 + 虚函数覆盖)

参考下面这个例子:

class A
{
public:
 virtual void func1()
 {
 cout << "A::func1()" << endl;
 }
 virtual void func2()
 {
 cout << "A::func2()" << endl;
 }

public:
 int _a;
};

class B : virtual public A//虚继承A,覆盖func1()
{
public:
 virtual void func1()
 {
 cout << "B::func1()" << endl;
 }
 virtual void func3()
 {
 cout << "B::func3()" << endl;
 }

public:
 int _b;
};

class C : virtual public A //虚继承A,覆盖func1()
{
 virtual void func1()
 {
 cout << "C::func1()" << endl;
 }

 virtual void func3()
 {
 cout << "C::func3()" << endl;
 }

public:
 int _c;
};

class D : public B , public C//虚继承B,C,覆盖func1()
{
 virtual void func1()
 {
 cout << "D::func1()" << endl;
 }

 virtual void func4()
 {
 cout << "D::func4()" << endl;
 }

public:
 int _d;
};

typedef void(*FUNC) ();

void PrintVTable(int* VTable)
{
 cout << " 虚表地址>" << VTable << endl;
 for (int i = 0; VTable[i] != 0; ++i)
 {
 printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
 FUNC f = (FUNC)VTable[i];
 f();
 }
 cout << endl;
}

void Test1()
{
 D d;
 d.B::_a = 1;
 d.C::_a = 2;
 d._b = 3;
 d._c = 4;
 d._d = 5;

 cout <<"sizeof(A) = "<< sizeof(A) << endl;
 cout << "sizeof(B) = " << sizeof(B) << endl;
 cout << "sizeof(C) = " << sizeof(C) << endl;

 //打印d的虚函数表
 int* VTable = (int*)(*(int*)&d);
 PrintVTable(VTable);

 //打印C的虚函数表
 VTable = (int*)*(int*)((char*)&d + sizeof(B)-sizeof(A));
 PrintVTable(VTable);

 //打印A的虚函数表
 VTable = (int*)*(int*)((char*)&d + sizeof(B)+sizeof(C)-2*sizeof(A)+4);
 PrintVTable(VTable);

}

接下来就慢慢分析:

1)先通过调试查看内存中是如何分配的,并和我们打印出的虚函数表对应起来:

注:由于B,C是虚继承A,所以编译器为了解决菱形继承所带来的“二义性”以及“数据冗余”,便将A放在最末端,并在子类中存放一个虚基表,方便找到父类;而虚基表的前四个字节存放的是对于自己虚函数表的偏移量,再往下四个字节才是对于父类的偏移量。

2)接下来就抽象出来分析模型

总结

1)虚函数按照其声明顺序放于表中;

2)父类的虚函数在子类的虚函数前面(由于子类单继承父类,直接使用父类的虚函数表);

3)覆盖的func()函数被放到了虚表中原来父类虚函数的位置;

4)没有被覆盖的函数依旧;

5)如果B,C虚继承A,并且B,C内部没有再声明或定义虚函数,则B,C没有对应的虚函数表;

6)在菱形的虚拟继承中,要注意A为B,C所共有的,在打印对应虚函数表时要注意偏移量。

好了,以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • C++类继承之子类调用父类的构造函数的实例详解

    C++类继承之子类调用父类的构造函数的实例详解 父类HttpUtil: #pragma once #include <windows.h> #include <string> using namespace std; class HttpUtil { private: LPVOID hInternet; LPVOID hConnect; LPVOID hRequest; protected: wchar_t * mHostName; short mPort; string send

  • C++中的菱形继承深入分析

    菱形继承 class Person { int _AA; }; class Student:public Person { int _BB; }; class Teacher :public Person { int _CC; }; class Assistant :public Student, public Teacher { int _DD; }; PS: Assistant的对象中存在两份Person成员 菱形继承存在二义性和数据冗余 解决: 使用虚继承 首先不使用虚继承时: #incl

  • C语言模式实现C++继承和多态的实例代码

    这个问题主要考察的是C和C++的区别,以及C++中继承和多态的概念. C和C++的区别 C语言是面向过程的语言,而C++是面向对象的过程. 什么是面向对象和面向过程? 面向过程就是分析解决问题的步骤,然后用函数把这些步骤一步一步的进行实现,在使用的时候进行一一调用就行了,注重的是对于过程的分析.面向对象则是把构成问题的事进行分成各个对象,建立对象的目的也不仅仅是完成这一个个步骤,而是描述各个问题在解决的过程中所发生的行为. 面向对象和面向过程的区别? 面向过程的设计方法采用函数来描述数据的操作,

  • C++ 中继承与动态内存分配的详解

    C++ 中继承与动态内存分配的详解 继承是怎样与动态内存分配进行互动的呢?例如,如果基类使用动态内存分配,并重新定义赋值和复制构造函数,这将怎样影响派生类的实现呢?这个问题的答案取决于派生类的属性.如果派生类也使用动态内存分配,那么就需要学习几个新的小技巧.下面来看看这两种情况: 一.派生类不使用new 派生类是否需要为显示定义析构函数,复制构造函数和赋值操作符呢? 不需要! 首先,来看是否需要析构函数,如果没有定义析构函数,编译器将定义一个不执行任何操作的默认构造函数.实际上,派生类的默认构造

  • 关于C++中菱形继承和虚继承的问题总结

    前言 菱形继承是多重继承中跑不掉的,Java拿掉了多重继承,辅之以接口.C++中虽然没有明确说明接口这种东西,但是只有纯虚函数的类可以看作Java中的接口.在多重继承中建议使用"接口",来避免多重继承中可能出现的各种问题.本文将给大家详细介绍关于C++菱形继承和虚继承的相关内容,分享出来供大家参考学习,话不多说了,来一起看看详细的介绍吧. 继承: 1. 单继承–一个子类只有一个直接父类时称这个继承关系为单继承 2. 多继承–一个子类有两个或以上直接父类时称这个继承关系为多继承 例如下面

  • 关于C++对象继承中的内存布局示例详解

    前言 本文给大家介绍的是关于C++对象继承的内存布局的相关内容,分享出来供大家参考学习,在开始之前说明下,关于单继承和多继承的简单概念可参考此文章 以下编译环境均为:WIN32+VS2015 虚函数表 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的.简称为V-Table.在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承.覆盖的问题,保证其容真实反应实际的函数. 首先先通过一个例子来引入虚函数表,假如现在有三

  • c++对象内存布局示例详解

    目录 前言 继承对象的内存布局 具有多重继承和虚拟功能的对象的内存布局 总结 前言 了解你所使用的编程语言究竟是如何实现的,对于C++程序员可能特别有意义.首先,它可以去除我们对于所使用语言的神秘感,使我们不至于对于编译器干的活感到完全不可思议:尤其重要的是,它使我们在Debug和使用语言高级特性的时候,有更多的把握.当需要提高代码效率的时候,这些知识也能够很好地帮助我们. 简单非多态的内存布局 class X { int x; float xx; public: X() {} ~X() {}

  • Java语言中的内存泄露代码详解

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

  • flutter中使用流式布局示例详解

    目录 简介 Flow和FlowDelegate Flow的应用 总结 简介 我们在开发web应用的时候,有时候为了适应浏览器大小的调整,需要动态对页面的组件进行位置的调整.这时候就会用到flow layout,也就是流式布局. 同样的,在flutter中也有流式布局,这个流式布局的名字叫做Flow.事实上,在flutter中,Flow通常是和FlowDelegate一起使用的,FlowDelegate用来设置Flow子组件的大小和位置,通过使用FlowDelegate.paintChildre可

  • C++中的内存对齐实例详解

    C++中的内存对齐实例详解 内存对齐 在我们的程序中,数据结构还有变量等等都需要占有内存,在很多系统中,它都要求内存分配的时候要对齐,这样做的好处就是可以提高访问内存的速度. 我们还是先来看一段简单的程序: 程序一 #include <iostream> using namespace std; struct X1 { int i;//4个字节 char c1;//1个字节 char c2;//1个字节 }; struct X2 { char c1;//1个字节 int i;//4个字节 ch

  • Python深度学习实战PyQt5窗口切换的堆叠布局示例详解

    目录 1. 堆叠布局简介 1. 1什么是堆叠布局(Stacked Layout) 1.2 堆叠布局的实现方法 2. 创建多窗口切换的堆叠布局 3. 堆叠布局的主程序设计 3.1 QStackedWidget 类 3.2 建立信号/槽连接 3.3 页面控制程序 3.4 堆叠布局中的控件操作 软件项目中经常需要多种不同的图形界面,以适应不同的任务场景.选项卡控件(QTackedWidget)通过标签选择打开对应的对话框页面,不需要另外编程.堆叠窗口控件(QStackedWidget)在主程序中通过编

  • Java中的反射机制示例详解

    目录 反射 什么是Class类 获取Class实例的三种方式 通过反射创建类对象 通过反射获取类属性.方法.构造器 更改访问权限和实例赋值 运用场景 反射 反射就是把Java类中的各个成分映射成一个个的Java对象.即在运行状态中,对于任意一个类,都能够知道这个类的所以属性和方法:对于任意一个对象,都能调用它的任意一个方法和属性.这种动态获取信息及动态调用对象方法的功能叫Java的反射机制 每一个Java程序执行必须通过编译.加载.链接和初始化四个阶段 1.编译:将.java.文件编译成字节码.

  • JavaScript中事件委托的示例详解

    目录 事件流 事件委托 结尾 大家好,我是前端西瓜哥.今天我们来认识一下事件委托. 所谓事件委托,就是将原本应该在当前元素绑定的事件,放到它的祖先元素上,让祖先元素来委托处理. 事件流 事件流指从页面中接收事件的顺序,也可理解为事件在页面中传播的顺序. 事件流由两阶段组成: 捕获事件 冒泡事件 我们通常用 addEventListener 给元素添加事件: document.querySelector('#card')addEventListener( 'click', function (ev

  • Swift 中的 JSON 反序列化示例详解

    目录 业界常用的几种方案 手动解码方案,如 Unbox(DEPRECATED) 阿里开源的 HandyJSON 基于 Sourcery 的元编程方案 Swift build-in API Codable 属性装饰器,如 BetterCodable 各个方案优缺点对比 Codable 介绍 原理浅析 Decoder.Container 协议 自研方案 功能设计 Decoder.Container 具体实现 再议 PropertyWrapper 应用场景示例 单元测试 性能对比 业界常用的几种方案

随机推荐