[CPP] 类的内存布局
本文讨论的是下面 3 个问题:
- 以不同方式继承之后,类的成员变量是如何分布的?
- 虚函数表及虚函数表指针,在可执行文件中的位置?
- 单一继承、多继承、虚拟继承之后,类的虚函数表的内容是如何变化的?
在这里涉及的变量有:有无继承、有无虚函数、是否多继承、是否虚继承。
准备工作
在开始探索类的内存布局之前,我们先了解虚函数表的概念,字节对齐的规则,以及如何打印一个类的内存布局。
查看类的内存布局
我们可以使用
clang++来查看类的内存布局:
# 查看对象布局, 要求 main 中有 sizeof(class_t) clang++ -Xclang -fdump-record-layouts xxx.cpp # 查看虚函数表布局, 要求 main 中实例化一个对象 clang++ -Xclang -fdump-record-layouts xxx.cpp # 或者 clang -cc1 -fdump-vtable-layouts -emit-llvm xxx.cpp
虚函数表
- 每个类都有一个属于自己虚函数表,虚函数表属于类,而不是某一个实例化对象。
- 如果一个类声明了虚函数,那么在该类的所有实例化对象中,在
[0, 7]
这 8 个字节(假设是 64 位机器),会存放一个虚函数表的指针vtable
。 - 虚函数表中的每一个元素都是一个函数地址,指向代码段的某一虚函数。
- 虚函数表指针
vtable
是在对象实例化的时候填入的(因此构造函数不能用virtual
声明为一个虚函数)。 假设 B 继承了 A ,假如我们在运行时有A *a = new B()
,那么a->vtable
实际上填入的是类 B 的虚函数表地址。 - 如何获得
vtable
的值?通过读取对象的起始 8 个字节的内容,即*(uint64_t *)&object
。
+---------+ +----------------+ | entity1 | | .text segment | +---------+ +----------------+ | vtable |-------+ +------->| Entity::vfunc1 | | member1 | | +-----------------+ | +---->| Entity::vfunc2 | | member2 | | | Entity's vtable | | | | ... | +---------+ | +-----------------+ | | +----------------+ +-------->| 0 : vfunc_ptr0 |------+ | | Entity::func1 | +---------+ | | 1 : vfunc_ptr1 |---------+ | Entity::func2 | | entity2 | | | ... | | ... | +---------+ | +-----------------+ +----------------+ | vtable |-------+ | member1 | | member2 | +---------+
那么虚函数表(即上图的
Entity's vtable)会存放在哪里呢?
一个直觉是与
static成员变量一样,存放在
.data segment,因为二者都属于是类共享的数据。
字节对齐
字节对齐的规则:按照编译器「已经扫描」的最长的数据类型的字节数 (总是为
1, 2, 4, 8) 进行对齐,并且尽量填满「空隙」。
编译器是按照声明顺序(从前往后扫描)来解析一个
struct / class的。
需要注意的是,不同的编译器,其字节对齐的规则会略有差异,但总的来说是大同小异的。本文所使用的编译器均为 clang/clang++ 。
例子一
struct Entity { char c1; int val; }; // sizeof(Entity) = 8
- 如果把
char c1
换成short val0
,那么还是 8 。 - 如果把
int val
换成double d
,那么是 16 。
例子二
struct Entity { char cval; short ival; double dval; }; /* *** Dumping AST Record Layout 0 | struct Entity 0 | char cval 2 | short ival 8 | double dval | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8] */
- 如果
short ival
换成int ival
,那么ival
的起始位置是 4 (因为编译器扫描到ival
的时候,看到的最长字节数是sizeof(int) = 4
)。
例子三
struct Entity { char cval; double dval; char cval2; int ival; }; /* *** Dumping AST Record Layout 0 | struct Entity 0 | char cval 8 | double dval 16 | char cval2 20 | int ival | [sizeof=24, dsize=24, align=8, | nvsize=24, nvalign=8] */
此处的例子,就是为了说明上述的「尽可能填满空隙」,注意到
cval2和
ival之间留出了
17, 18, 19这 3 个字节的空白。
- 在
cval2, ival
插入任意的一个字节的数据类型(最多插入 3 个),不会影响sizeof(Entity)
的大小。 - 如果我们在
cval2, ival
之间插入一个short sval
,那么sval
会位于 18 这一位置。
例子四
如果有虚函数,又会怎么样呢?
class Entity { char cval; virtual void vfunc() {} }; /* *** Dumping AST Record Layout 0 | class Entity 0 | (Entity vtable pointer) 8 | char cval | [sizeof=16, dsize=9, align=8, | nvsize=9, nvalign=8] */
在 64 位机器上,一个指针的大小是 8 字节,所以编译器会按照 8 字节对齐。
单一的类
成员变量
考虑无虚函数的条件下,成员变量的内存布局。
class A { private: short val1; public: int val2; double d; static char ch; void funcA1() {} }; int main() { __attribute__((unused)) int k = sizeof(A); } // clang++ -Xclang -fdump-record-layouts test.cpp
使用上述命令编译之后,输出为:
*** Dumping AST Record Layout 0 | class A 0 | short val1 4 | int val2 8 | double d | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8]
从上面的输出可以看出:
static
类型的成员并不占用实例化对象的内存(因为static
类型的成员存放在静态数据区.data
)。- 成员函数不占用内存(因为存放在代码段
.text
)。 - 成员变量的权限级别
private, public
不影响内存布局,内存布局只跟声明顺序有关(可能需要字节对齐)。
虚函数表
class A { private: short val1; public: int val2; double d; static char ch; void funcA1() {} virtual void vfuncA1() {} virtual void vfuncA2() {} }; int main() { __attribute__((unused)) int k = sizeof(A); // __attribute__((unused)) A a; }
内存布局如下:
clang++ -Xclang -fdump-record-layouts test.cpp *** Dumping AST Record Layout 0 | class A 0 | (A vtable pointer) 8 | short val1 12 | int val2 16 | double d | [sizeof=24, dsize=24, align=8, | nvsize=24, nvalign=8] clang++ -Xclang -fdump-vtable-layouts test.cpp Original map Vtable for 'A' (4 entries). 0 | offset_to_top (0) 1 | A RTTI -- (A, 0) vtable address -- 2 | void A::vfuncA1() 3 | void A::vfuncA2() VTable indices for 'A' (2 entries). 0 | void A::vfuncA1() 1 | void A::vfuncA2()
从这里可以看出,虚函数表的指针默认是存放在一个类的起始位置(一般占用 4 或者 8 字节,视乎机器的字长)。
offset_to_top(0)
: 表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。如果是多继承的情况,一个类可能存在多个vtable
的指针。RTTI
: 即 Run Time Type Info, 指向存储运行时类型信息 (type_info
) 的地址,用于运行时类型识别,用于typeid
和dynamic_cast
。
单一继承
成员变量
class A { public: char aval; static int sival; void funcA1(); }; class B : public A { public: double bval; void funcB1(); }; class C : public B { public: int cval; void funcC1() {} };
内存布局:
clang++ -Xclang -fdump-record-layouts test.cpp *** Dumping AST Record Layout 0 | class A 0 | char aval | [sizeof=1, dsize=1, align=1, | nvsize=1, nvalign=1] *** Dumping AST Record Layout 0 | class B 0 | class A (base) 0 | char aval 8 | double bval | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class C 0 | class B (base) 0 | class A (base) 0 | char aval 8 | double bval 16 | int cval | [sizeof=24, dsize=20, align=8, | nvsize=20, nvalign=8]
可以看出,普通的单一继承,成员变量是从上到下依次排列的,并且遵循前面提到的字节对齐规则。
虚函数表
- A 中有 2 个虚函数
vfuncA1, vfuncA2
. - B 重写 (Override) 了
vfuncA1
,自定义虚函数vfuncB
. - C 重写了
vfunc1
,自定义虚函数vfuncC
.
class A { public: char aval; static int sival; virtual void vfuncA1() {} virtual void vfuncA2() {} }; class B : public A { public: double bval; virtual void vfuncA1() {} virtual void vfuncB() {} }; class C : public B { public: int cval; virtual void vfuncA1() {} virtual void vfuncC() {} };
成员变量布局:
clang++ -Xclang -fdump-record-layouts test.cpp *** Dumping AST Record Layout 0 | class A 0 | (A vtable pointer) 8 | char aval | [sizeof=16, dsize=9, align=8, | nvsize=9, nvalign=8] *** Dumping AST Record Layout 0 | class B 0 | class A (primary base) 0 | (A vtable pointer) 8 | char aval 16 | double bval | [sizeof=24, dsize=24, align=8, | nvsize=24, nvalign=8] *** Dumping AST Record Layout 0 | class C 0 | class B (primary base) 0 | class A (primary base) 0 | (A vtable pointer) 8 | char aval 16 | double bval 24 | int cval | [sizeof=32, dsize=28, align=8, | nvsize=28, nvalign=8]
3 个类的虚函数表如下:
clang++ -Xclang -fdump-vtable-layouts test.cpp Original map void C::vfuncA1() -> void B::vfuncA1() void B::vfuncA1() -> void A::vfuncA1() Vtable for 'C' (6 entries). 0 | offset_to_top (0) 1 | C RTTI -- (A, 0) vtable address -- -- (B, 0) vtable address -- -- (C, 0) vtable address -- 2 | void C::vfuncA1() 3 | void A::vfuncA2() 4 | void B::vfuncB() 5 | void C::vfuncC() VTable indices for 'C' (2 entries). 0 | void C::vfuncA1() 3 | void C::vfuncC() Original map void C::vfuncA1() -> void B::vfuncA1() void B::vfuncA1() -> void A::vfuncA1() Vtable for 'B' (5 entries). 0 | offset_to_top (0) 1 | B RTTI -- (A, 0) vtable address -- -- (B, 0) vtable address -- 2 | void B::vfuncA1() 3 | void A::vfuncA2() 4 | void B::vfuncB() VTable indices for 'B' (2 entries). 0 | void B::vfuncA1() 2 | void B::vfuncB() Original map void C::vfuncA1() -> void B::vfuncA1() void B::vfuncA1() -> void A::vfuncA1() Vtable for 'A' (4 entries). 0 | offset_to_top (0) 1 | A RTTI -- (A, 0) vtable address -- 2 | void A::vfuncA1() 3 | void A::vfuncA2() VTable indices for 'A' (2 entries). 0 | void A::vfuncA1() 1 | void A::vfuncA2()
可以看出,在单一继承中,子类的虚函数表通过以下步骤构造出来:
- 先拷贝上一层次父类的虚函数表。
- 如果子类有自定义虚函数(例如
B::vfuncB, C::vfuncC
),那么直接在虚函数表后追加这些虚函数的地址。 - 如果子类覆盖了父类的虚函数,使用新地址(例如
B::vfuncA1, C::vfuncA1
)覆盖原有地址(即A::vfunc1
)。
多继承
现直接组合成员变量和虚函数一起来看。
class A { char aval; virtual void vfuncA1() {} virtual void vfuncA2() {} }; class B { double bval; virtual void vfuncB1() {} virtual void vfuncB2() {} }; class C : public A, public B { char cval; virtual void vfuncC() {} virtual void vfuncA1() {} virtual void vfuncB1() {} };
内存布局如下(注意类 C 的布局):
clang++ -Xclang -fdump-record-layouts test.cpp *** Dumping AST Record Layout 0 | class A 0 | (A vtable pointer) 8 | char aval | [sizeof=16, dsize=9, align=8, | nvsize=9, nvalign=8] *** Dumping AST Record Layout 0 | class B 0 | (B vtable pointer) 8 | double bval | [sizeof=16, dsize=16, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class C 0 | class A (primary base) 0 | (A vtable pointer) 8 | char aval 16 | class B (base) 16 | (B vtable pointer) 24 | double bval 32 | char cval | [sizeof=40, dsize=33, align=8, | nvsize=33, nvalign=8]
注意到类 C 的内存布局:
- 一共 40 字节,有 2 个
vtable
指针。 - 继承有
primary base
父类和普通base
父类之分。
实际上就是:
+--------+--------+---------------+ | offset | size | content | +--------+--------+---------------+ | 0 | 8 | vtable1 | | 8 | 1 | aval | | 9 | 7 | aligned bytes | | 16 | 8 | vtable2 | | 24 | 8 | bval | | 32 | 1 | cval | | 33 | 7 | aligned bytes | +--------+--------+---------------+
总的来说,在最底层子类的内存布局中,多继承的成员变量,以及
vtable指针的排列规则是:
- 第一个声明的继承是
primary base
父类。 - 按照继承的声明顺序依次排列,并需要遵循编译器的字节对齐规则。
- 最后排列最底层子类的成员变量。
虚函数表如下(省略了 A 和 B 的内容):
clang++ -Xclang -fdump-vtable-layouts test.cpp Original map void C::vfuncA1() -> void A::vfuncA1() Vtable for 'C' (10 entries). 0 | offset_to_top (0) 1 | C RTTI -- (A, 0) vtable address -- -- (C, 0) vtable address -- 2 | void C::vfuncA1() 3 | void A::vfuncA2() 4 | void C::vfuncC() 5 | void C::vfuncB1() 6 | offset_to_top (-16) 7 | C RTTI -- (B, 16) vtable address -- 8 | void C::vfuncB1() [this adjustment: -16 non-virtual] method: void B::vfuncB1() 9 | void B::vfuncB2() Thunks for 'void C::vfuncB1()' (1 entry). 0 | this adjustment: -16 non-virtual VTable indices for 'C' (3 entries). 0 | void C::vfuncA1() 2 | void C::vfuncC() 3 | void C::vfuncB1()
从上面可以看出,C 的虚函数表是由 2 部分组成的:
- 首先是 「C 继承 A」,按照上述单一继承的虚函数表生成原则,生成了第一个虚函数表。此时
C::vfuncB1()
对于 A 来说是一个自定义的虚函数,因此虚函数表的第一部分有 4 个函数地址。 - 其次是「C 继承 B」,同样按照单一继承的规则生成,但不用追加
C::vfuncC()
,因为C::vfuncC()
已经在第一部分填入。
可以发现的是:
- C 的虚函数表存在一个重复的函数地址
C::vfuncB1
。 - 虽然 C 有 2 个
vtable
指针,但仍然只有一个虚函数表( 😅 其实也可以理解为 2 个表,不过这 2 个表是紧挨着的),而 2 个vtable
指针指向了虚函数表的不同位置(也许跟编译器的处理有关,至少 clang 下的情况是这样的)。
加入虚函数表后,C 的内存布局如下:
+-----------------------+ |-2: offset_to_top(0) | |-1: C RTTI | +--------+--------+---------------+ +-----------------------+ | offset | size | content | | class C's vtable | +--------+--------+---------------+ +-----------------------+ | 0 | 8 | vtable1 |--------------------->| 0: C::vfuncA1_ptr | | 8 | 1 | aval | | 1: A::vfuncA2_ptr | | 9 | 7 | aligned bytes | | 2: C::vfuncC_ptr | | 16 | 8 | vtable2 |------------+ | 3: C::vfuncB1_ptr | | 24 | 8 | bval | | | 4: offset_to_top(-16) | | 32 | 1 | cval | | | 5: C RTTI | | 33 | 7 | aligned bytes | +-------->| 6: C::vfuncB1_ptr | +--------+--------+---------------+ | 7: B::vfuncB2_ptr | +-----------------------+
如何验证这个想法呢?
class A { public: char aval; virtual void vfuncA1() { cout << "A::vfuncA1()" << endl; } virtual void vfuncA2() { cout << "A::vfuncA2()" << endl; } }; class B { public: double bval; virtual void vfuncB1() { cout << "B::vfuncB1()" << endl; } virtual void vfuncB2() { cout << "B::vfuncB2()" << endl; } }; class C : public A, public B { public: char cval; virtual void vfuncC() { cout << "C::vfuncC()" << endl; } virtual void vfuncA1() { cout << "C::vfuncA1()" << endl; } virtual void vfuncB1() { cout << "C::vfuncB1()" << endl; } }; int main() { __attribute__((unused)) int k = sizeof(C); C c; uint64_t *cvtable = (uint64_t *)*(uint64_t *)(&c); uint64_t *cvtable2 = (uint64_t *)*(uint64_t *)((uint8_t *)(&c) + 16); typedef void (*func_t)(void); cout << "---- vtable1 ----" << endl; ((func_t)(*(cvtable + 0)))(); // C::vfuncA1() ((func_t)(*(cvtable + 1)))(); // A::vfuncA2() ((func_t)(*(cvtable + 2)))(); // C::vfuncC() ((func_t)(*(cvtable + 3)))(); // C::vfuncB1() printf("offset_to_top = %d\n", *(cvtable2 - 2)); // -16 cout << "---- vtable2 ----" << endl; ((func_t)(*(cvtable2 + 0)))(); // C::vfuncB1(), same as cvtable + 6 ((func_t)(*(cvtable2 + 1)))(); // B::vfuncB2(), same as cvtable + 7 }
棱形继承和虚拟继承
如果我们需要用到类似「棱形」的继承链,那么就要通过「虚拟继承」的方式实现。
假设此处的继承链为:
Base / \ A B \ / Child
如果不使用
virtual修饰继承方式:
class Base { public: int value; }; class A : public Base { }; class B : public Base { }; class Child : public A, public B { }; int main() { Child child; child.value; }
那么成员变量
child.value会出现编译时错误 (clang++) ,类似于「命名冲突」。
单一虚拟继承
class Base { char baseval; virtual void vfuncBase1() {} virtual void vfuncBase2() {} }; class A : virtual public Base { double aval; virtual void vfuncBase1() {} virtual void vfuncA() {} }; class B : virtual public Base { double bval; virtual void vfuncBase2() {} virtual void vfuncB() {} };
以 A 为例子进行说明。成员变量布局:
clang++ -Xclang -fdump-record-layouts diamond2.cpp *** Dumping AST Record Layout 0 | class A 0 | (A vtable pointer) 8 | double aval 16 | class Base (virtual base) 16 | (Base vtable pointer) 24 | char baseval | [sizeof=32, dsize=25, align=8, | nvsize=16, nvalign=8]
与上述的「单一继承」不同,此处虚拟继承是会有 2 个
vtable指针的,并且被虚拟继承的目标(即
Base会排列在最后面)。
虚函数表的内容如下:
clang++ -Xclang -fdump-vtable-layouts diamond2.cpp Original map Vtable for 'A' (11 entries). 0 | vbase_offset (16) 1 | offset_to_top (0) 2 | A RTTI -- (A, 0) vtable address -- 3 | void A::vfuncBase1() 4 | void A::vfuncA() 5 | vcall_offset (0) 6 | vcall_offset (-16) 7 | offset_to_top (-16) 8 | A RTTI -- (Base, 16) vtable address -- 9 | void A::vfuncBase1() [this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1() 10 | void Base::vfuncBase2() Virtual base offset offsets for 'A' (1 entry). Base | -24 Thunks for 'void A::vfuncBase1()' (1 entry). 0 | this adjustment: 0 non-virtual, -24 vcall offset offset VTable indices for 'A' (2 entries). 0 | void A::vfuncBase1() 1 | void A::vfuncA()
化简一下:
A vtable: B vtable: - A::vfuncBase1() - B::vfuncBase2() - A::vfuncA() - B::vfuncB() - A::vfuncBase1() - Base::vfuncBase1() - Base::vfuncBase2() - B::vfuncBase2()
从上面可以看出:
- 虚函数表的第一部分
3-4
,按照A
是一个「单一的类」时的规则构造。 - 虚函数表的第二部分
9-10
,按照A
单一继承Base
的规则构造。
棱形继承的成员变量
class Child : public A, public B { char childval; virtual void vfuncC() {} virtual void vfuncB() {} virtual void vfuncA() {} };
Child成员变量内存布局如下:
clang++ -Xclang -fdump-record-layouts diamond.cpp *** Dumping AST Record Layout 0 | class A 0 | (A vtable pointer) 8 | double aval 16 | class Base (virtual base) 16 | char baseval | [sizeof=24, dsize=17, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class B 0 | (B vtable pointer) 8 | double bval 16 | class Base (virtual base) 16 | char baseval | [sizeof=24, dsize=17, align=8, | nvsize=16, nvalign=8] *** Dumping AST Record Layout 0 | class Child 0 | class A (primary base) 0 | (A vtable pointer) 8 | double aval 16 | class B (base) 16 | (B vtable pointer) 24 | double bval 32 | char childval 33 | class Base (virtual base) 33 | char baseval | [sizeof=40, dsize=34, align=8, | nvsize=33, nvalign=8]
在
Child中:
- 成员变量和虚函数指针与「多继承」的情况相同。
Child
把Base
(被虚拟继承的父类)的内容排在最后(比Child
的自定义成员还要后),并且只保留了一份Base
的数据,这就是虚拟继承的作用。
棱形继承的虚函数表
A, B的虚函数表,如「单一虚拟继承」一节所述。
Child的虚函数表如下:
clang++ -Xclang -fdump-vtable-layouts diamond.cpp Original map void Child::vfuncA() -> void A::vfuncA() Vtable for 'Child' (18 entries). 0 | vbase_offset (40) 1 | offset_to_top (0) 2 | Child RTTI -- (A, 0) vtable address -- -- (Child, 0) vtable address -- 3 | void A::vfuncBase1() 4 | void Child::vfuncA() 5 | void Child::vfuncC() 6 | void Child::vfuncB() 7 | vbase_offset (24) 8 | offset_to_top (-16) 9 | Child RTTI -- (B, 16) vtable address -- 10 | void B::vfuncBase2() 11 | void Child::vfuncB() [this adjustment: -16 non-virtual] method: void B::vfuncB() 12 | vcall_offset (-24) 13 | vcall_offset (-40) 14 | offset_to_top (-40) 15 | Child RTTI -- (Base, 40) vtable address -- 16 | void A::vfuncBase1() [this adjustment: 0 non-virtual, -24 vcall offset offset] method: void Base::vfuncBase1() 17 | void B::vfuncBase2() [this adjustment: 0 non-virtual, -32 vcall offset offset] method: void Base::vfuncBase2() Virtual base offset offsets for 'Child' (1 entry). Base | -24 Thunks for 'void Child::vfuncB()' (1 entry). 0 | this adjustment: -16 non-virtual VTable indices for 'Child' (3 entries). 1 | void Child::vfuncA() 2 | void Child::vfuncC() 3 | void Child::vfuncB()
回顾一下 A 和 B 的虚函数表:
A vtable: B vtable: - A::vfuncBase1() - B::vfuncBase2() - A::vfuncA() - B::vfuncB() - A::vfuncBase1() - Base::vfuncBase1() - Base::vfuncBase2() - B::vfuncBase2()
可以看出,
Child的虚函数表有 2 部分:
- 第一部分
3-6, 10-11
,与Child
多继承A, B
的构造规则类似,即合并Avtable[0 - 1]
和Bvtable[0 - 1]
。 - 第二部分
16-17
,合并Avtable[2 - 3]
和Bvtable[2 - 3]
。
总结
场景 | 成员变量 | 虚函数表 |
---|---|---|
单一的类 | 按照声明顺序依次排列,并需要遵循字节对齐的规则 | 在对象的起始 8 个字节的内存中,存放 vtable指针 |
单一继承 | 1. 按照继承的层次顺序,依次排列,并需要遵循字节对齐的规则 2. 只有一个 vtable指针 |
1. 拷贝上一层次父类的虚函数表 2. 如果有自定义的虚函数,在虚函数表后追加对应的地址 3. 如果 Override 了父类虚函数,那么使用新地址覆盖原有地址。 |
多继承 | 1. 多个 vtable指针 2. 按照继承的顺序,依次排列父类的 <vtable, members> |
参考「多继承」一节。 |
单一虚拟继承 | 与普通的单一继承不同,会有多个 vtable指针 |
2 部分:第一部分按照「单一的类」规则和第二部分按照「单一继承」规则。 |
棱形继承 | 1. 与多继承类似 2. 在最后添加被虚拟继承目标的数据 |
参考「棱形继承的虚函数表」一节。 |
- [cpp] C++对象内存布局分析
- cpp——类——内存布局
- linux进程内存布局(转)
- C语言内存布局
- C++ 对象的内存布局深层剖析(上)
- c++内存布局
- Objective-C内存布局
- linux中进程在内存中的布局
- C++程序运行时内存布局之----------this到底是什么?
- C语言程序的内存布局(二)C语言程序段的分类
- C++函数调用中的内存布局
- 从硬件获得内存布局--e820
- Jvm(17),jvm创建对象---JVM对象的内存布局
- C 程序在内存中的布局 [李园7舍_404]
- 对象的内存空间布局
- C++对象模型之内存布局一
- C++对象模型之内存布局二
- 《C++中类对象的内存布局和占用空间》《C++ 类里面,函数占用存储空间问题 》
- C++ 对象内存布局
- [C++基础]043_看图识C++内存布局