C++ 对象的内存布局(上)
本文转载自haoel博主的博客:陈皓专栏 【空谷幽兰,心如皓月】
原文地址:C++ 对象的内存布局(上)
C++
对象的内存布局(上)
陈皓
前言
07年12月,我写了一篇《C++虚函数表解析》的文章,引起了大家的兴趣。有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的。我在这里一并对大家的留言表示感谢。这也是我为什么再写一篇续言的原因。因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单。不想,这篇文章成为了打开C++对象模型内存布局的一个引子,引发了大家对C++对象的更深层次的讨论。当然,我之前的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数表里,至少有以下这些内容没有涉及:
1)有成员变量的情况。
2)有重复继承的情况。
3)有虚拟继承的情况。
4)有钻石型虚拟继承的情况。
这些都是我本篇文章需要向大家说明的东西。所以,这篇文章将会是《C++虚函数表解析》的一个续篇,也是一篇高级进阶的文章。我希望大家在读这篇文章之前对C++有一定的基础和了解,并能先读我的上一篇文章。因为这篇文章的深度可能会比较深,而且会比较杂乱,我希望你在读本篇文章时不会有大脑思维紊乱导致大脑死机的情况。;-)
对象的影响因素
简而言之,我们一个类可能会有如下的影响因素:
1)成员变量
2)虚函数(产生虚函数表)
3)单一继承(只继承于一个类)
4)多重继承(继承多个类)
5)重复继承(继承的多个父类中其父类有相同的超类)
6)虚拟继承(使用virtual方式继承,为了保证继承后父类的内存布局只会存在一份)
上述的东西通常是C++这门语言在语义方面对对象内部的影响因素,当然,还会有编译器的影响(比如优化),还有字节对齐的影响。在这里我们都不讨论,我们只讨论C++语言上的影响。
本篇文章着重讨论下述几个情况下的C++对象的内存布局情况。
1)单一的一般继承(带成员变量、虚函数、虚函数覆盖)
2)单一的虚拟继承(带成员变量、虚函数、虚函数覆盖)
3)多重继承(带成员变量、虚函数、虚函数覆盖)
4)重复多重继承(带成员变量、虚函数、虚函数覆盖)
5)钻石型的虚拟多重继承(带成员变量、虚函数、虚函数覆盖)
我们的目标就是,让事情越来越复杂。
知识复习
我们简单地复习一下,我们可以通过对象的地址来取得虚函数表的地址,如:
typedef void(*Fun)(void); Base b; Fun pFun = NULL; cout << "虚函数表地址:" << (int*)(&b) << endl; cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl; // Invoke the first virtual function pFun = (Fun)*((int*)*(int*)(&b)); pFun();
我们同样可以用这种方式来取得整个对象实例的内存布局。因为这些东西在内存中都是连续分布的,我们只需要使用适当的地址偏移量,我们就可以获得整个内存对象的布局。
本篇文章中的例程或内存布局主要使用如下编译器和系统:
1)Windows XP
和 VC++ 2003
2)Cygwin
和 G++ 3.4.4
单一的一般继承
下面,我们假设有如下所示的一个继承关系:

请注意,在这个继承关系中,父类,子类,孙子类都有自己的一个成员变量。而了类覆盖了父类的f()方法,孙子类覆盖了子类的g_child()及其超类的f()。
我们的源程序如下所示:
class Parent {
public:
    int iparent;
    Parent ():iparent (10) {}
    virtual void f() { cout << " Parent::f()" << endl; }
    virtual void g() { cout << " Parent::g()" << endl; }
    virtual void h() { cout << " Parent::h()" << endl; }
};
class Child : public Parent {
public:
    int ichild;
    Child():ichild(100) {}
    virtual void f() { cout << "Child::f()" << endl; }
    virtual void g_child() { cout << "Child::g_child()" << endl; }
    virtual void h_child() { cout << "Child::h_child()" << endl; }
};
class GrandChild : public Child{
public:
    int igrandchild;
    GrandChild():igrandchild(1000) {}
    virtual void f() { cout << "GrandChild::f()" << endl; }
    virtual void g_child() { cout << "GrandChild::g_child()" << endl; }
    virtual void h_grandchild() { cout << "GrandChild::h_grandchild()" << endl; }
};
我们使用以下程序作为测试程序:(下面程序中,我使用了一个int** pVtab
来作为遍历对象内存布局的指针,这样,我就可以方便地像使用数组一样来遍历所有的成员包括其虚函数表了,在后面的程序中,我也是用这样的方法的,请不必感到奇怪,)
typedef void(*Fun)(void);
GrandChild gc;
int** pVtab = (int**)&gc;
cout << "[0] GrandChild::_vptr->" << endl;
for (int i=0; (Fun)pVtab[0][i]!=NULL; i++){
	pFun = (Fun)pVtab[0][i];
	cout << "    ["<<i<<"] ";
	pFun();
}
cout << "[1] Parent.iparent = " << (int)pVtab[1] << endl;
cout << "[2] Child.ichild = " << (int)pVtab[2] << endl;
cout << "[3] GrandChild.igrandchild = " << (int)pVtab[3] << endl;
其运行结果如下所示:(在VC++ 2003和G++ 3.4.4下)
| 
 [0] GrandChild::_vptr->                         [1] Parent.iparent = 10 [2] Child.ichild = 100 [3] GrandChild.igrandchild = 1000  | 
使用图片表示如下:

可见以下几个方面:
1)虚函数表在最前面的位置。
2)成员变量根据其继承和声明顺序依次放在后面。
3)在单一的继承中,被overwrite的虚函数在虚函数表中得到了更新。
多重继承
下面,再让我们来看看多重继承中的情况,假设有下面这样一个类的继承关系。注意:子类只overwrite了父类的f()函数,而还有一个是自己的函数(我们这样做的目的是为了用g1()作为一个标记来标明子类的虚函数表)。而且每个类中都有一个自己的成员变量:

我们的类继承的源代码如下所示:父类的成员初始为10,20,30,子类的为100
class Base1 {
public:
    int ibase1;
    Base1():ibase1(10) {}
    virtual void f() { cout << "Base1::f()" << endl; }
    virtual void g() { cout << "Base1::g()" << endl; }
    virtual void h() { cout << "Base1::h()" << endl; }
};
class Base2 {
public:
    int ibase2;
    Base2():ibase2(20) {}
    virtual void f() { cout << "Base2::f()" << endl; }
    virtual void g() { cout << "Base2::g()" << endl; }
    virtual void h() { cout << "Base2::h()" << endl; }
};
class Base3 {
public:
    int ibase3;
    Base3():ibase3(30) {}
    virtual void f() { cout << "Base3::f()" << endl; }
    virtual void g() { cout << "Base3::g()" << endl; }
    virtual void h() { cout << "Base3::h()" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
    int iderive;
    Derive():iderive(100) {}
    virtual void f() { cout << "Derive::f()" << endl; }
    virtual void g1() { cout << "Derive::g1()" << endl; }
};
我们通过下面的程序来查看子类实例的内存布局:下面程序中,注意我使用了一个s变量,其中用到了sizof(Base)来找下一个类的偏移量。(因为我声明的是int成员,所以是4个字节,所以没有对齐问题。关于内存的对齐问题,大家可以自行试验,我在这里就不多说了)
typedef void(*Fun)(void); Derive d; int** pVtab = (int**)&d; cout << "[0] Base1::_vptr->" << endl; pFun = (Fun)pVtab[0][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[0][1]; cout << " [1] ";pFun(); pFun = (Fun)pVtab[0][2]; cout << " [2] ";pFun(); pFun = (Fun)pVtab[0][3]; cout << " [3] "; pFun(); pFun = (Fun)pVtab[0][4]; cout << " [4] "; cout<<pFun<<endl; cout << "[1] Base1.ibase1 = " << (int)pVtab[1] << endl; int s = sizeof(Base1)/4; cout << "[" << s << "] Base2::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); Fun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; out << " [3] "; cout<<pFun<<endl; cout << "["<< s+1 <<"] Base2.ibase2 = " << (int)pVtab[s+1] << endl; s = s + sizeof(Base2)/4; cout << "[" << s << "] Base3::_vptr->"<<endl; pFun = (Fun)pVtab[s][0]; cout << " [0] "; pFun(); pFun = (Fun)pVtab[s][1]; cout << " [1] "; pFun(); pFun = (Fun)pVtab[s][2]; cout << " [2] "; pFun(); pFun = (Fun)pVtab[s][3]; cout << " [3] "; cout<<pFun<<endl; s++; cout << "["<< s <<"] Base3.ibase3 = " << (int)pVtab[s] << endl; s++; cout << "["<< s <<"] Derive.iderive = " << (int)pVtab[s] << endl;
其运行结果如下所示:(在VC++ 2003和G++ 3.4.4下)
| 
 [0] Base1::_vptr->                     [4] 00000000      ç [1] Base1.ibase1 = 10 [2] Base2::_vptr->                [3] 00000000      ç [3] Base2.ibase2 = 20 [4] Base3::_vptr->                     [5] Base3.ibase3 = 30 [6] Derive.iderive = 100  | 
使用图片表示是下面这个样子:

我们可以看到:
1) 
每个父类都有自己的虚表。
2) 
子类的成员函数被放到了第一个父类的表中。
3) 
内存布局中,其父类布局依次按声明顺序排列。
4) 
每个父类的虚表中的f()函数都被overwrite成了子类的f()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
C++ 对象的内存布局(上)的更多相关文章
- C++ 对象的内存布局(上)
		
C++ 对象的内存布局(上) 陈皓 http://blog.csdn.net/haoel 点击这里查看下篇>>> 前言 07年12月,我写了一篇<C++虚函数表解析>的文 ...
 - VS中C++对象的内存布局
		
本文主要简述一下在Visual Studio中C++对象的内存布局,这里没有什么测试代码,只是以图文的形式来描述一下内存分布,关于测试的代码以及C++对象模型的其他内容大家可以参考一下陈皓先生的几篇博 ...
 - JVM——深入分析对象的内存布局
		
概述 一个对象本身的内在结构需要一种描述方式,这个描述信息是以字节码的方法存储在方法区中的.Class本身就是一个对象,都以KB为单位,如果new Integer()为了表示一个数据就占用KB级别的内 ...
 - Java对象的内存布局
		
对象的内存布局 平时用java编写程序,你了解java对象的内存布局么? 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域: 对象头 实例数据 对齐填充 对象头 对象头包括两部分信息: ...
 - jvm学习记录-对象的创建、对象的内存布局、对象的访问定位
		
简述 今天继续写<深入理解java虚拟机>的对象创建的理解.这次和上次隔的时间有些长,是因为有些东西确实不好理解,就查阅各种资料,然后弄明白了才来做记录. (此文中所阐述的内容都是以Hot ...
 - Java对象的内存布局以及对象所需内存大小计算详解
		
1. 内存布局 在HotSpot虚拟机中,对象的内存布局可以分为三部分:对象头(Header). 实例数据(Instance Data)和对齐填充(Padding). 1) 对象头(Header): ...
 - JVM总结-java对象的内存布局
		
在 Java 程序中,我们拥有多种新建对象的方式.除了最为常见的 new 语句之外,我们还可以通过反射机制.Object.clone 方法.反序列化以及 Unsafe.allocateInstance ...
 - Java对象创建的过程及对象的内存布局与访问定位
		
这里以HotSpot为例,且所说的对象指普通的Java对象,不包括数组和Class对象等. 1.对象创建的过程 1.类加载.解析.初始化:虚拟机遇到new时先检查此指令的参数是否能在常量池中找到类的符 ...
 - JVM中对象的内存布局与访问定位
		
一.对象的内存布局 已主流的HotSpot虚拟机来说, 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header).实例数据(Instance Data)和对齐填 ...
 
随机推荐
- python中如何将生成等差数列和等比数列
			
在python库numpy 中提供了函数linspace和logspace函数用于生产等差数列和等比数列. 1.linspace函数生成等差数列 def linspace(start, sto ...
 - ubuntu上的附件-终端和用快捷键ctrl+alt+f1 有啥区别
			
ctrl +alt +Fn 打开的是模拟终端,简单说来,linux系统一开机会自动打开6个模拟终端,然后自动切换到其中一个(一般来说是切换到图形界面的那个也就是说窗口管理器是在这6个模拟终端中运行的) ...
 - 谷歌发布 TensorFlow Lite [官方网站,文档]
			
机器学习社区:http://tensorflow123.com/ 简介 TensorFlow Lite TensorFlow Lite 是 TensorFlow 针对移动和嵌入式设备的轻量级解决方案. ...
 - jQuery  extend 方法使用 (转)
			
方法介绍 jQuery 的 API 手册中,extend 方法挂载在 jQuery 和 jQuery.fn 两个不同的对象上,但在 jQuery 内部代码实现的是相同的,只是功能各不相同. 先看看官方 ...
 - My Stuck in C++
			
My Stuck in C++ Zhong-Liang Xiang Oct. 1st, 2017 这个专题记录了对于我而言, c++迷一样的东西.
 - vue-cli中配置sass
			
第一步, npm install node-sass --save-dev npm install sass-loader --save-dev 第二部,打开webpack.base.config.j ...
 - Go 语言环境安装
			
Go 语言支持以下系统: Linux FreeBSD Mac OS X(也称为 Darwin) Window 安装包下载地址为:https://golang.org/dl/. 各个系统对应的包名: 操 ...
 - RunLoop总结:RunLoop的应用场景(五)
			
今天要介绍的RunLoop应用场景感觉很酷炫,我们可能不常用到,但是对于做Crash 收集的 SDK可能会用得比较频繁吧.相比关于RunLoop 可以让应用起死回生,大家都听说过,可是怎么实现呢?今天 ...
 - Linux文件编辑命令详细整理
			
刚接触Linux,前几天申请了个免费体验的阿里云服务器,选择的是Ubuntu系统,配置jdk环境变量的时候需要编辑文件. vi命令编辑文件,百度了一下,很多回答不是很全面,因此编辑文件话了一些时间. ...
 - RxJava(十一)defer操作符实现代码支持链式调用
			
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52597643 本文出自:[余志强的博客] 一.前言 现在越来越多An ...