以下所有测试都运行在 VS2022 Debug x86 环境下
内存对齐
对于一个【空的类型】,其大小仍为1个字节。
struct A{}
class A size(1)
对于【仅有 char
类型成员变量的类型】,其大小也为1个字节。
struct A{
char c;
}
class A size(1):
+---
0 | c
+---
32位操作系统以4字节为基本单位进行内存对齐,结构体A的大小不是预想中的9字节大小而是为char
类型的变量补上了3字节。
struct A {
char c;
int x;
};
class A size(8):
+---
0 | c
| <alignment member> (size=3)
4 | x
+---
但实际上还会以最大的基本类型作为自然对齐长度,例如double
长度为8,因此在以4字节作为基本单位对齐后,还会以8字节作为自然对齐长度进行补偿。
struct A{
char c;
int i;
short s;
double d;
};
struct B{
char c;
short s;
int i;
double d;
};
如下类型A的内存模型可知,由于【1字节的char
类型】之后为【4字节的int
类型】,因此char
类型需要在其后补偿3字节以对齐【4字节的基本单位】,而【2字节的short
类型】之后为【8字节的double
类型】,除了补偿2字节以对齐【4字节的基本单位】外,仍需要再补偿4字节以对齐【8字节的自然长度】。
class A size(24):
+---
0 | c
| <alignment member> (size=3)
4 | i
8 | s
| <alignment member> (size=6)
16 | d
+---
class B size(16):
+---
0 | c
| <alignment member> (size=1)
2 | s
4 | i
8 | d
+---
虚函数
【含有虚函数的类型】其实例会为虚函数创建虚函数表,并将虚表指针放在实例内存模型中的首地址。
struct A {
int a;
virtual void fooA();
};
class A size(8):
+---
0 | {vfptr}
4 | a
+---
A::$vftable@:
| &A_meta
| 0
0 | &A::fooA
继承
继承【有虚函数的类型】的类型,其实例会创建一个虚函数表,将父类虚函数表拷贝后再存放自己的虚函数。
struct B : public A {
int b;
//virtual void fooA() override;
virtual void fooB();
};
class B size(12):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | a
| +---
8 | b
+---
B::$vftable@:
| &B_meta
| 0
0 | &A::fooA
1 | &B::fooB
注意,当子类重写父类的虚函数时,子类的虚函数表中该虚函数就会被覆盖为子类的虚函数实现,否则依旧是父类的虚函数实现。
对于类型B,通过对其实例取地址可以获取其内存模型首地址,即虚函数表指针的地址。将该地址转为int*
整型指针后进行取地址操作*((int*)(&b))
,得到虚函数表首地址。同样地,再次将该地址转为int*
整型指针,然后取地址*(int*)(*((int*)(&b)))
,即可得到虚函数表中第一个虚函数的地址,或者在取地址前通过+1获取第二个虚函数地址等等。
B b;
&b;
*((int*)(&b));
*(int*)(*((int*)(&b)))
经过实验可以发现,当子类未重写父类虚函数时,子类虚表会直接拷贝父类虚表中的虚函数地址。若子类重写了父类的虚函数,就会用新的虚函数地址覆盖掉父类。
多重继承
【多继承的类型】其实例会为每一个含有虚函数的父类创建一个虚函数表,将父类虚函数表分别拷贝后,将自己的虚函数按顺序存放在继承的首个父类。
struct B {
int b;
virtual void fooB();
};
struct C {
int c;
virtual void fooC();
};
struct D : public B, public C {
int d;
virtual void fooD();
};
class D size(20):
+---
0 | +--- (base class B)
0 | | {vfptr}
4 | | b
| +---
8 | +--- (base class C)
8 | | {vfptr}
12 | | c
| +---
16 | d
+---
D::$vftable@B@:
| &D_meta
| 0
0 | &B::fooB
1 | &D::fooD
D::$vftable@C@:
| -8
0 | &C::fooC
菱形继承
相较于多继承,菱形继承的类型,其实例会将【父类的父类】的成员变量多次拷贝。
struct B : public A {
int b;
virtual void fooB();
};
struct C : public A {
int c;
virtual void fooC();
};
struct D : public B, public C {
int d;
virtual void fooD();
};
class D size(28):
+---
0 | +--- (base class B)
0 | | +--- (base class A)
0 | | | {vfptr}
4 | | | a
| | +---
8 | | b
| +---
12 | +--- (base class C)
12 | | +--- (base class A)
12 | | | {vfptr}
16 | | | a
| | +---
20 | | c
| +---
24 | d
+---
D::$vftable@B@:
| &D_meta
| 0
0 | &A::fooA
1 | &B::fooB
2 | &D::fooD
D::$vftable@C@:
| -12
0 | &A::fooA
1 | &C::fooC
虚继承
如果虚继承类型自己存在虚函数,则其实例会为自己的虚函数先创建虚函数表,存放自己的虚函数。然后创建虚基类表,存放所有【虚函数表指针】相对【虚基类表指针】的偏移量,例如自己的虚表指针相对偏移量为-4。
如果虚继承类型自己没有虚函数,则实例首地址为虚基类表指针,且自己的虚表指针相对偏移量为0。虚继承类型把虚基类的信息放在最后,因此如下所示的虚继承类型中共有三个额外指针。
struct A {
int a;
virtual void fooA();
};
struct B : virtual public A {
int b;
virtual void fooB();
};
class B size(20):
+---
0 | {vfptr}
4 | {vbptr}
8 | b
+---
+--- (virtual base A)
12 | {vfptr}
16 | a
+---
B::$vftable@B@:
| &B_meta
| 0
0 | &B::fooB
B::$vbtable@:
0 | -4
1 | 8 (Bd(B+4)A)
B::$vftable@A@:
| -12
0 | &A::fooA
菱形虚继承
菱形虚继承的类型,其实例参考虚继承的内存模型。其父类的指针和成员数据排在内存模型中的最前,且各有一个虚基类表指针。其【父类的父类】成员变量排在成员数据之后,且相比菱形继承只存在一份,消除了重复冲突。
struct B : virtual public A {
int b;
virtual void fooB();
};
struct C : virtual public A {
int c;
virtual void fooC();
};
struct D : public B, public C {
int d;
virtual void fooD();
};
class D size(36):
+---
0 | +--- (base class B)
0 | | {vfptr}
4 | | {vbptr}
8 | | b
| +---
12 | +--- (base class C)
12 | | {vfptr}
16 | | {vbptr}
20 | | c
| +---
24 | d
+---
+--- (virtual base A)
28 | {vfptr}
32 | a
+---
D::$vftable@B@:
| &D_meta
| 0
0 | &B::fooB
1 | &D::fooD
D::$vftable@C@:
| -12
0 | &C::fooC
D::$vbtable@B@:
0 | -4
1 | 24 (Dd(B+4)A)
D::$vbtable@C@:
0 | -4
1 | 12 (Dd(C+4)A)
D::$vftable@A@:
| -28
0 | &A::fooA