要想知道c++类对象的内存布局, 可以有多种方式,比如:

1)输出成员变量的偏移, 通过offsetof宏来得到

2)通过调试器查看, 比如常用的VS

1.没有数据成员的对象

class A{

};

int main()
{
A arr[] = { A(),A(),A() };
cout << sizeof(A) << endl; //输出1
int n = sizeof(arr) / sizeof(A); //为了防止分母为0,发生错误
cout << n << endl; return ;
}

1)最开始的c++编译器对没有成员变量的类对象执行sizeof是返回0的,但是为了防止上述错误发生,编译器会强行(隐式)地插入一个字节,至于这个字节到底存着什么信息,不知道

2)同时这也可以让这类的对象有一个明确的地址

2.只有数据成员的对象

class Base1
{
public:
int base1_1;
int base1_2;
};

对象大小及偏移:

可知对象布局:

  可以看到, 成员变量是按照定义的顺序来保存的, 最先声明的在最上边, 然后依次保存,类对象的大小就是所有成员变量大小之和.

3.没有虚函数有普通函数的对象

class Base1
{
public:
int base1_1;
int base1_2; void foo(){}
};

结果如下:

结论:

  1. 此处和前面的结果是一样的
  2. 非虚成员函数可以被看作是类作用域下的全局函数,不存在实例分配的空间里,由该类所有实例所共享,它们的地址编译期就已确定,实例调用成员函数时,编译器向成员函数传入this指针完成和实例的匹配
  3. 类的静态成员函数也是类作用域下的全局函数,不存在实例分配的空间里,但它属于整个类,不属于类的实例,它不需要实例化也能调用,而非虚成员函数必须实例化才能调用
class A {
public:
static void fun1()
{
cout << << endl;
} void fun2()
{
cout << << endl;
}
}; int main()
{
A::fun1(); //类的静态成员函数不需要实例化也能调用
A::fun2(); //编译错误:类的成员函数必须实例化才能调用
A a;
a.fun2(); //输出:888 return ;
}

4.拥有仅一个虚函数的类对象

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
}; Base1 b1;

结果如下:

  咦? 多了4个字节? 且 base1_1 和 base1_2 的偏移都各自向后多了4个字节! 说明类对象的最前面被多加了4个字节的"东东". 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:(由于没有写构造函数, 所以变量的数据没有初始化, Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460)

  base1_1前面多了一个变量 __vfptr(这就是虚函数表指针vfptr), 其类型为void**, 这说明它是一个指向void*的指针,再看高亮部分,说明它是一个指向指针数组的指针. 再看看[0]元素, 其类型为void*, 其值为 ConsoleApplication2.exe!Base1::base1_fun1(void), 这是什么意思呢? 如果对WinDbg比较熟悉, 那么应该知道这是一种惯用表示手法, 它就是指 Base1::base1_fun1() 函数的地址.

所以,该类的对象大小为12个字节, 大小及偏移信息如下:

现在的类对象布局如下:

结论:

  1. 含有虚函数的类对象里会有一个指针,叫虚函数表指针vfptr,指向虚函数表,虚函数表是一个指针数组,其中的每一个元素都是一个函数指针,指向对应的虚函数
  2. 注意到__vfptr前面的const修饰.,它修饰的是那个虚函数表,而不是__vfptr

5.拥有多个虚函数的类对象

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
virtual void base1_fun2() {}
}; Base1 b1;

大小以及偏移信息如下:

  多了一个虚函数, 类对象大小却依然是12个字节! 再来看看VS形象的表现:

现在__vfptr所指向的指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址. 通过上面两张图表, 我们可以得到如下结论:

  1. 更加肯定前面我们所描述的: __vfptr只是一个指针, 它指向一个函数指针数组(即: 虚函数表)
  2. 增加一个虚函数, 只是简单地向该类对应的虚函数表中增加一项而已, 并不会影响到类对象的大小以及布局情况

不妨, 我们再定义一个类的变量b2, 现在再来看看__vfptr的指向:

通过Watch 1窗口我们看到:

  1. b1和b2是类的两个变量, 理所当然, 它们的地址是不同的(见 &b1 和 &b2)
  2. 虽然b1和b2是类的两个变量, 但是: 它们的__vfptr的指向却是同一个虚函数表

由此我们可以总结出:

  同一个类的不同对象共用同一份虚函数表, 它们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.

是时候该展示一下类对象的内存布局情况了:

结论:

  1. 虚函数表是编译器在编译时期创建好的, 只存在一份,同一个类的不同对象共用同这份虚函数表,虚函数表保存在.rodata只读数据段
  2. 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表

6.单继承且本身不存在虚函数的派生类的内存布局

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
virtual void base1_fun2() {}
}; class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2;
}; Derive1 d1;

来看看现在的内存布局:

基类在上边, 继承类的成员在下边依次定义! 展开来看看:

现在类的布局情况应该是下面这样:

7.覆盖的基类虚函数的单继承派生类的内存布局

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
virtual void base1_fun2() {}
}; class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2; // 覆盖基类虚函数
virtual void base1_fun1() {}
}; Derivel d1;

现在的布局:

  注意高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1(), 这里发生静态绑定

8.定义了基类没有的虚函数的单继承的派生类对象布局

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
virtual void base1_fun2() {}
}; class Derive1 : public Base1
{
public:
int derive1_1;
int derive1_2; virtual void derive1_fun1() {}
}; Derive1 d1;

现在的布局:

  表面上看来几乎和第5种情况完全一样, 现在继承类明明定义了自身的虚函数, 但不见了, 那么, 来看看类对象的大小, 以及成员偏移情况吧:

  也没有变化, 现在我们只能从汇编入手了, 来看看调用derive1_fun1()时的代码:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();
 ; pd1->derive1_fun1();
mov eax,dword ptr [pd1]
mov edx,dword ptr [eax]
0082546B mov esi,esp
0082546D mov ecx,dword ptr [pd1]
mov eax,dword ptr [edx+]
call eax

汇编代码解释:

第2行: 由于pd1是指向d1的指针, 所以执行此句后 eax 就是d1的地址
第3行: 又因为Base1::__vfptr是Base1的第1个成员, 同时也是Derive1的第1个成员, 那么: &__vfptr == &d1, clear? 所以当执行完 mov edx, dword ptr[eax] 后, edx就得到了__vfptr的值, 也就是虚函数表的地址.
第5行: 由于是__thiscall调用, 所以把this保存到ecx中.
第6行: 一定要注意到那个 edx+8, 由于edx是虚函数表的地址, 那么 edx+8将是虚函数表的第3个元素, 也就是__vftable[2]!!!
第7行: 调用虚函数.

结论:

  1. 继承类Derive1的虚函数表被加在基类的虚函数后面
  2. 由于Base1只知道自己的两个虚函数索引[0][1], 所以就算在后面加上了[2],Base1根本不知情, 不会对它造成任何影响.

最新的类对象布局表示:

9.多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局

class Base1
{
public:
int base1_1;
int base1_2; virtual void base1_fun1() {}
virtual void base1_fun2() {}
}; class Base2
{
public:
int base2_1;
int base2_2; virtual void base2_fun1() {}
virtual void base2_fun2() {}
}; // 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2; // 基类虚函数覆盖
virtual void base1_fun1() {}
virtual void base2_fun2() {} // 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};
Derive1 d1;

对象大小及偏移信息:

VS的布局:

结论:

  1. 按照基类的声明顺序, 基类的成员依次分布在继承中.
  2. 注意被高亮的那两行, 已经发生了虚函数覆盖!

继承反汇编:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
 ; pd1->derive1_fun2();
mov eax,dword ptr [pd1]
mov edx,dword ptr [eax]
0099530B mov esi,esp
0099530D mov ecx,dword ptr [pd1]
mov eax,dword ptr [edx+0Ch]
call eax

汇编解释:

第2行: 取d1的地址
第3行: 取Base1::__vfptr的值!!
第6行: 0x0C, 也就是第4个元素(下标为[3])

结论:

  1. 派生类的虚函数表依然是保存到第1个拥有虚函数表的那个基类的虚函数表里

看看现在的类对象布局图:

10.如果第1个直接基类没有虚函数

class Base1
{
public:
int base1_1;
int base1_2;
}; class Base2
{
public:
int base2_1;
int base2_2; virtual void base2_fun1() {}
virtual void base2_fun2() {}
}; // 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2; // 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};

VS的布局:

现在的大小及偏移情况:

  注意: sizeof(Base1) == 8, sizeof(Base1) == 12

继承返汇编:

Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();
 ; pd1->derive1_fun2();
012E4BA6 mov eax,dword ptr [pd1]
012E4BA9 mov edx,dword ptr [eax]
012E4BAB mov esi,esp
012E4BAD mov ecx,dword ptr [pd1]
012E4BB0 mov eax,dword ptr [edx+0Ch]
012E4BB3 call eax

  这段汇编代码和前面一个完全一样, 观察基类成员变量偏移:

  可以发现,base2_1、base2_2在base1_1、base1_2前面

结论:

  1. 哪个基类有虚函数表, 在派生类对象的内存布局中,这个基类就放在前面

现在类的布局情况:

11.如果两个基类都没有虚函数表

class Base1
{
public:
int base1_1;
int base1_2;
}; class Base2
{
public:
int base2_1;
int base2_2;
}; // 多继承
class Derive1 : public Base1, public Base2
{
public:
int derive1_1;
int derive1_2; // 自身定义的虚函数
virtual void derive1_fun1() {}
virtual void derive1_fun2() {}
};

VS的基本布局:

  可以看到, 现在__vfptr已经独立出来了, 不再属于Base1和Base2!

看看偏移情况:

对象布局:

结论:

  1. 虚函数表指针始终在最前面

参考资料:

https://blog.twofei.com/496/

12.类的成员函数调用

1)非虚成员函数可以被看作是类作用域下的全局函数,不存在实例分配的空间里,由该类所有实例所共享,它们的地址编译期就已确定,实例调用成员函数时,编译器向成员函数传入this指针完成和实例的匹配

2)如果是虚函数,则类对象通过其虚表指针找到虚函数表,并进行后续的调用。 同一个类,共用同一份虚函数表.

3)基类指针(或引用)指向派生类对象(或引用)时调用虚函数:含有虚函数的基类对象和派生类对象都有vfptr指针,每个类的vfptr指针指向相应的虚函数表。基类指针(或引用)绑定到派生类对象时,可以使用派生类对象中的基类元素,这里包括在位于最前端的vfptr(这也是为什么vfptr总在最前端的原因),此时调用虚函数时,通过这个最前端的vfptr指针找到虚函数表,在虚函数表中找到相应虚函数,然后进行调用,这就是动态绑定

13.虚函数表里保存的可能并非虚函数的地址,但是肯定跟虚函数有一点关联,最后都能通过虚函数表里的表项成功的调用虚函数

参考资料:

https://www.cnblogs.com/cswuyg/archive/2010/08/20/1804716.html

c++类对象的内存分布的更多相关文章

  1. C++类虚函数内存分布(这个 你必须懂)

    转自:http://www.cnblogs.com/jerry19880126/p/3616999.html C++类内存分布 书上类继承相关章节到这里就结束了,这里不妨说下C++内存分布结构,我们来 ...

  2. C++中派生类对象的内存布局

    主要从三个方面来讲: 1 单一继承 2 多重继承 3 虚拟继承 1 单一继承 (1)派生类完全拥有基类的内存布局,并保证其完整性. 派生类可以看作是完整的基类的Object再加上派生类自己的Objec ...

  3. C++对象的内存分布和虚函数表

    c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员.    2.静态数据成员被提取出来放在程序的静态数据 ...

  4. 剖析c++(三) 类对象在内存中的布局

    基本规则: 1.各data member的自然边界为其字节大小(char为1,short为2,int为4),并按照各自的自然边界对齐: 2.整个object的总自然边界为最大data memeber的 ...

  5. 【转】C++类-内存分布

    C++类内存分布 - 转载自Jerry19880126 - 博客园 的文章 在上面这篇文章的基础上做了些整理. 主要讨论了C++类对象的内存分布结构. 来看看编译器是怎么处理类成员内存分布的,特别是在 ...

  6. C++对象内存分布(3) - 菱形继承(virtual)

    1.前言 本篇文章的全部代码样例.假设是windows上编译执行.则使用的是visual studio 2013.假设是RHEL6.5平台(linux kernal: 2.6.32-431.el6.i ...

  7. C++对象内存分布详解(包括字节对齐和虚函数表)

    转自:https://www.jb51.net/article/101122.htm 1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数 ...

  8. C++语法小记---面向对象模型(实例的内存分布)

    面向对象的模型(内存分布) 对于一个对象而言,成员变量和成员函数是分开存放的 成员函数位于代码段,所有的类对象共有 成员变量为每一个对象独有,位于内存中 类对象在内存中的分布和struct完全相同 对 ...

  9. HotSpot源码分析之C++对象的内存布局

    HotSpot采用了OOP-Klass模型来描述Java类和对象.OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象的具体类型.为了更好理解这个模型, ...

随机推荐

  1. Metasploit 简单渗透应用

    1.Metasploit端口扫描: 在终端输入msfconsole或直接从应用选metasploit 进入msf>nmap -v -sV 192.168.126.128  与nmap结果一样 用 ...

  2. MRO,C3算法

    1了解python2和python3类的区别 python2在2.4之前使用的是经典类, 2.4之后, 使用的是新式类 class Foo: pass class Foo(object): pass ...

  3. django之 使用py文件操作django项目中的表

    要想在一个另外的py文件中操作django,可以使用如下的代码 import os os.environ.setdefault("DJANGO_SETTINGS_MODULE",' ...

  4. git自定义项目钩子和全局钩子

    钩子介绍 自定义钩子分为:项目钩子和全局钩子 自定义全局钩子: 全局钩子目录结构: (注意:excludes目录结构是我们自定义的目录,规则逻辑在update.d/update.py脚本里实现的,非g ...

  5. This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.

    This iPhone 6s is running iOS 11.3.1 (15E302), which may not be supported by this version of Xcode.

  6. spark-1

    先测试搭好的spark集群: 本地模式测试: 在spark的目录下: ./bin/run-example SparkPi 10 --master local[2] 验证成功: 集群模式 Spark S ...

  7. sass的基本使用

    使用sass的前提是安装Ruby,如果是Mac系统,那么免去安装,Windows系统需要自行安装https://www.sass.hk/install/.当安装好以后,直接执行安装sass命令:gem ...

  8. 【原】The Linux Command Line - Redirection

    ● cat - Concatenate files● sort - Sort lines of text● uniq - Report or omit repeated lines● grep - P ...

  9. Ubuntu16下用virtualbox 安装Windows虚拟机

    平时要用Windows系统,但是现在工作都是在Linux下面开发,所以都没装Windows,之前用vm虚拟机比较麻烦, 所以就用virtualbox搞搞: 1.sed -i '$adeb http:/ ...

  10. springboot @scheduled 并发

    本文介绍如何使用springboot的sheduled实现任务的定时调度,并将调度的任务实现为并发的方式. 1.定时调度配置scheduled 1)注册定时任务 package com.xiaoju. ...