[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。
- 假设 B 继承了 A ,假如我们在运行时有
+---------+ +----------------+
| 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;
}
从这里可以看出,虚函数表的指针默认是存放在一个类的起始位置(一般占用 4 或者 8 字节,视乎机器的字长)。
内存布局:
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()
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++ 类,继承类,有虚函数的类,虚拟继承的类的内存布局,使用vs2010打印布局结果。
本文笔者在青岛逛街的时候突然想到的...最近就有想写几篇关于继承虚函数的笔记,所以回家到之后就奋笔疾书的写出来发布了 应用sizeof函数求类巨细这个问题在很多面试,口试题中很轻易考,而涉及到类的时候 ...
- cl查看类的内存布局
查看单个类的内存布局 Microsoft Visual Studio编译器cl的编译选项可以查看源文件中某个C++类的内存布局,对于想了解某个对象的内存布局的人来说十分直观和方便. • 命令格式 ...
- VS2010下如何查看类的内存布局
用VS2010查看类的内存布局,这里用两种方法 (1)MSVC有个隐藏的"/d1"开关,通过这个开关可以查看项目中类的内存布局情况. 修改项目属性,添加"/d1 repo ...
- c++类的内存布局
问题: 考察了reinterpret_cast和static_cast的区别.顺道发现了一个可以查看c++内存布局的工具(在VS中). 结果: 前两个输出的地址形同,后一个不同. class A{in ...
- 【C++对象模型】使用gcc、clang和VC++显示C++类的内存布局
引言 各种C++实现对C++类/对象的内存布局可能有所不同,包括数据成员的顺序.虚函数表(virtual table: vtbl)的结构.继承关系的处理等.了解C++类/对象的布局,对于理解C++各种 ...
- C++类继承内存布局(一)
转自:http://blog.csdn.net/jiangyi711/article/details/4890889# 一 类布局 不同的继承方式将导致不同的内存布局 1)C结构 C++基于C,所以C ...
- c++中如何查看一个类的内存布局
打开VS command prompt,输入下述命令可以看到对象的内存布局. cl a.cpp -d1 reportSingleClassLayout[classname] // reportSin ...
- C++类继承内存布局(三)
参考:http://blog.csdn.net/jiangyi711/article/details/4890889# (三)成员函数 类X中每一个非静态成员函数都会接受一个特殊的隐藏参数——this ...
- C++类继承内存布局(二)
转自:http://blog.csdn.net/jiangyi711/article/details/4890889# (二 )成员变量 前面介绍完了类布局,接下来考虑不同的继承方式下,访问成员变量的 ...
随机推荐
- JS021. 拦截事件的显式处理与默认动作(Web API: event.preventDefault)
Web API - event.preventDefault( ) Event 接口的 preventDefault( ) 方法,告诉 user agent :如果此事件没有被显式处理,它默认的动作 ...
- stream流思想应用
1.计算集合中某字段数值和 subTotal = subTotal+ complainCountResult.stream().filter(childSource->childSource.g ...
- iframe 内容适用高度
HTML: <div class="content"> <iframe id="frameObj" src="链接" fr ...
- python 直接插入排序
# 先将未排序的元素放到九天之上,一个临时变量temp,上到九天之上去观察前面已经排好的序列, # 然后从后向前对比,只要临时变量小于某个位置的值,就将其向前移动一位,就是给比它下标大 # 1的位置处 ...
- Tars | 第2篇 TarsJava SpingBoot启动与负载均衡源码初探
目录 前言 1. Tars客户端启动 @EnableTarsServer 2. Communicator通信器 3. 客户端的负载均衡调用器LoadBalance 最后 前言 通过源码分析可以得出这样 ...
- 大学四年的Python学习笔记分享之一,内容整理的比较多与仔细
翻到以前在大学坚持记录的Python学习笔记,花了一天的时间整理出来,整理时不经回忆起大学的时光,一眨眼几年就过去了,现在还在上学的你们,一定要珍惜现在,有个充实的校园生活.希望这次的分享对于你们有学 ...
- PHPDebug互动扩展【phpdbg】功能浅析
对于 PHP 开发者来说,单步的断点 Debug 调试并不是我们的必修课,而 Java . C# . C++ 这些静态语言则会经常性地进行这种调试.其实,我们 PHP 也是支持这类调试方式的,特别是对 ...
- win10蓝牙鼠标无法连接,需pin码
从控制面板进到"设备和打印机",点击"添加设备" 鼠标切换到匹配模式,就可以看到自己的鼠标了,点"下一步" 出现了一样的画面,是的,不用输P ...
- P4783-[模板]矩阵求逆
正题 题目链接:https://www.luogu.com.cn/problem/P4783 题目大意 给出一个矩阵,求它的逆矩阵. \(1\leq n\leq 400\) 解题思路 记给出矩阵\(P ...
- Python编码规范(养成好的编码习惯很重要)
学习过程养成良好的编码习惯 1. 类名采用驼峰命名法,即类名的每个首字母都大写,如:class HelloWord,类名不使用下划线 2. 函数名只使用小写字母和下划线 3.定义类后面包含一个文档字符 ...