测试C++中的零碎(一):内存对齐和虚函数

122 阅读4分钟

以下所有测试都运行在 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
	+---

对齐.drawio.png

但实际上还会以最大的基本类型作为自然对齐长度,例如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
	+---

内存对齐.drawio (11).png

虚函数

【含有虚函数的类型】其实例会为虚函数创建虚函数表,并将虚表指针放在实例内存模型中的首地址。

struct A {
    int a;
    virtual void fooA();
};
class A	size(8):
	+---
 0	| {vfptr}
 4	| a
	+---
A::$vftable@:
	| &A_meta
	|  0
 0	| &A::fooA

内存对齐.drawio (3).png

继承

继承【有虚函数的类型】的类型,其实例会创建一个虚函数表,将父类虚函数表拷贝后再存放自己的虚函数。

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

内存对齐.drawio (5).png

注意,当子类重写父类的虚函数时,子类的虚函数表中该虚函数就会被覆盖为子类的虚函数实现,否则依旧是父类的虚函数实现。

对于类型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

内存对齐.drawio (7).png

菱形继承

相较于多继承,菱形继承的类型,其实例会将【父类的父类】的成员变量多次拷贝。

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

内存对齐.drawio (8).png

虚继承

如果虚继承类型自己存在虚函数,则其实例会为自己的虚函数先创建虚函数表,存放自己的虚函数。然后创建虚基类表,存放所有【虚函数表指针】相对【虚基类表指针】的偏移量,例如自己的虚表指针相对偏移量为-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

虚函数.drawio (9).png

菱形虚继承

菱形虚继承的类型,其实例参考虚继承的内存模型。其父类的指针和成员数据排在内存模型中的最前,且各有一个虚基类表指针。其【父类的父类】成员变量排在成员数据之后,且相比菱形继承只存在一份,消除了重复冲突。

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

内存对齐.drawio (10).png