我的文章会先发布到个人博客后,再更新到,可以到个人博客或者公众号获取更多内容。
何为C++对象模型?
C++对象模型可以概括为以下2部分:
- 语言中直接支持面向对象程序设计的部分
- 对于各种支持的底层实现机制
来自《深度探索C++对象模型》
类内基本元素
- 成员变量 (静态和非静态)
- 成员函数 (静态、非静态、virtual)
影响类大小的因素
- 非静态成员变量
- 虚表指针
- 基类虚表指针
- 内存对齐
虚表结构
虚表结构 | |
---|---|
virtual call offsets | 虚继承时会出现,父类引用或指针指向子类对象,调用被子类重写的方法时,用于对this指针进行调整,方便成功调用被重写的方法。 |
virtual base offsets | 虚继承时会出现,对象在对象布局中与指向虚基类虚表的指针地址的偏移量 |
offsets to top | 指到对象起始地址的偏移量,只有多重继承的情形才有可能不为0,单继承或无继承的情形都为0 |
RTTI information | 是一个对象指针,运行时类型识别信息,它用于唯一地标识该类型 |
virtual functions pointers | 虚函数指针 |
如何查看对象的布局
简单例子:
class Base {
public:
Base() = default;
virtual ~Base() = default;
void Func() {}
private:
int a;
int b;
};
int main(int argc, const char **argv) {
Base a;
return 0;
}
gcc
在gcc中,低版本选项-fdump-class-hierarchy,高版本选项-fdump-lang-class
使用g++(8.3.1)和-fdump-lang-class 选项命令, 就可以生成一个文件明天同名添加后缀(001l.class)的文件
g++ -fdump-lang-class Base.cpp
ls
Base.cpp Base.cpp.001l.class
查看 cat Base.cpp.001l.class
Vtable for Base
Base::_ZTV4Base: 4 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI4Base)
16 (int (*)(...))Base::~Base
24 (int (*)(...))Base::~Base
Class Base
size=16 align=8
base size=16 base align=8
Base (0x0x7f5c01b14960) 0
vptr=((& Base::_ZTV4Base) + 16)
//_ZTV4Base 用命令`c++filt _ZTV4Base`得到该字符串意思为(vtable for Base)
gdb
set print object on
set print vtbl on
set print pretty on
//以上可以定义一个gdb命令的alias,简化命令
p 实例
info vtbl 实例
实际打印和gcc类似
Clang
// 查看对象的布局
clang -Xclang -fdump-record-layouts -stdlib=libc++ -c Base.cpp
*** Dumping AST Record Layout
0 | class Base
0 | (Base vtable pointer)
8 | int a
12 | int b
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
// 虚函数表的布局
clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c Base.cpp
Vtable for 'Base' (4 entries).
0 | offset_to_top (0)
1 | Base RTTI
-- (Base, 0) vtable address --
2 | Base::~Base() [complete]
3 | Base::~Base() [deleting]
VTable indices for 'Base' (2 entries).
0 | Base::~Base() [complete]
1 | Base::~Base() [deleting]
由于clang的打印更加友好,手边有现成的工具,所以本文后续都采用clang(version 7.0.1)查看对象模型布局
普通对象的布局
class NormalA {
public:
NormalA() = default;
~NormalA() = default;
void func() {}
private:
int a1;
int b1;
};
class NormalB :public NormalA {
public:
NormalB() = default;
~NormalB() = default;
void func() {}
private:
int a2;
int b2;
};
int main(int argc, const char **argv) {
NormalA a;
NormalB b;
return 0;
}
类的内存布局:
*** Dumping AST Record Layout
0 | class NormalA
0 | int a1
4 | int b1
| [sizeof=8, dsize=8, align=4,
| nvsize=8, nvalign=4]
0 | class NormalB
0 | class NormalA (base)
0 | int a1
4 | int b1
8 | int a2
12 | int b2
| [sizeof=16, dsize=16, align=4,
| nvsize=16, nvalign=4]
由于没有虚函数,所以没有虚表布局
含有虚函数对象的布局
单个对象
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA() {}
virtual void funcB() {}
void funcC() {}
private:
int a;
int b;
};
int main(int argc, const char **argv) {
A a;
return 0;
}
类内存布局如下
*** Dumping AST Record Layout
0 | class A
0 | (A vtable pointer)
8 | int a
12 | int b
| [sizeof=16, dsize=16, align=8,
| nvsize=16, nvalign=8]
从layout中可以看到含有虚函数的结构体,在对象前方增加了8个字节的虚表指针,指向虚表的位置。
虚表布局如下
Vtable for 'A' (6 entries).
0 | offset_to_top (0)
1 | A RTTI
-- (A, 0) vtable address --
2 | A::~A() [complete]
3 | A::~A() [deleting]
4 | void A::funcA()
5 | void A::funcB()
Vtable for A
A::_ZTV1A: 6 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1A)
16 (int (*)(...))A::~A
24 (int (*)(...))A::~A
32 (int (*)(...))A::funcA
40 (int (*)(...))A::funcB
VTable indices for 'A' (4 entries).
0 | A::~A() [complete]
1 | A::~A() [deleting]
2 | void A::funcA()
3 | void A::funcB()
offset_to_top(0):表明这个虚表地址距离对象顶部的偏移量,因为在对象头部所以是0。
RTTI指针:虚表第一个元素,指针指向了对象运行时类型信息(type_info)的地址,用于运行时的类型识别,用于typeid和dynamic_cast。
"A::~A() [complete]":虚表指针指向的元素,因为对象可以在栈内和堆内创建,本虚构函数对应栈内创建的实例,只需要调用析构函数不用delete相应的内存。
"A::~A() [deleting]":那么带deleteing的虚构函数对应堆内创建的实例,需要调用析构函数和delete相应的内存,释放资源。
单继承的布局
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA() {}
virtual void funcB() {}
void funcC() {}
private:
int a;
int b;
};
class B : public A {
public:
virtual void funcA() {}
virtual void funcD() {}
void funcC() {}
private:
int a;
int b;
};
int main(int argc, const char **argv) {
A a;
B b;
return 0;
}
子类的内存布局
*** Dumping AST Record Layout
0 | class B
0 | class A (primary base)
0 | (A vtable pointer)
8 | int a
12 | int b
16 | int a
20 | int b
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
同样可以看到在对象前方增加了8个字节的虚表指针,指向虚表的位置。
子类的虚函数布局
Vtable for 'B' (7 entries).
0 | offset_to_top (0)
1 | B RTTI
-- (A, 0) vtable address --
-- (B, 0) vtable address --
2 | B::~B() [complete]
3 | B::~B() [deleting]
4 | void B::funcA()
5 | void A::funcB()
6 | void B::funcD()
Vtable for B
B::_ZTV1B: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1B)
16 (int (*)(...))B::~B
24 (int (*)(...))B::~B
32 (int (*)(...))B::funcA
40 (int (*)(...))A::funcB
48 (int (*)(...))B::funcD
VTable indices for 'B' (4 entries).
0 | B::~B() [complete]
1 | B::~B() [deleting]
2 | void B::funcA()
4 | void B::funcD()
可以看到当父类的有虚函数时
- RTTI中有了两项,表明此时子类中A、B虚表位置是相同的
- 子类的虚表中虚函数按先父类再再子类顺序排列
- 当子类重写父类的虚函数时,父类对应位置的虚函数指针会被替换成子类的虚函数指针
多重继承
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA1() {}
virtual void funcA2() {}
void funcA3() {}
private:
int a;
int b;
};
class B : public A {
public:
virtual void funcA1() {}
virtual void funcB1() {}
virtual void funcB2() {}
void funcB3() {}
private:
int a;
int b;
};
class C : public B {
public:
virtual void funcA2() {}
virtual void funcB1() {}
virtual void funcC1() {}
void funcC2() {}
private:
int a;
int b;
};
int main(int argc, const char **argv) {
A a;
B b;
C c;
return 0;
}
子类的内存布局
*** Dumping AST Record Layout
0 | class C
0 | class B (primary base)
0 | class A (primary base)
0 | (A vtable pointer)
8 | int a
12 | int b
16 | int a
20 | int b
| [sizeof=24, dsize=24, align=8,
| nvsize=24, nvalign=8]
子类的虚表布局
Vtable for 'C' (9 entries).
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (B, 0) vtable address --
-- (C, 0) vtable address --
2 | C::~C() [complete]
3 | C::~C() [deleting]
4 | void B::funcA1()
5 | void C::funcA2()
6 | void C::funcB1()
7 | void B::funcB2()
8 | void C::funcC1()
Vtable for C
C::_ZTV1C: 7 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1C)
16 (int (*)(...))C::~C
24 (int (*)(...))C::~C
32 (int (*)(...))B::funcA1
40 (int (*)(...))C::funcA2
48 (int (*)(...))C::funcB1
56 (int (*)(...))B::funcB2
64 (int (*)(...))C::funcC1
可以看到多重继承和单继承区别不大
- RTTI中有了三项,表明此时子类中A、B、C虚表位置是相同的
- 子类有且仅有一个虚表,虚表中中虚函数按多重继承的顺序先第一个父类再第二个再子类顺序排列,如果你更换A,B的继承顺序,对应的虚函数表中的顺序也会按继承顺序相应更改
- 当子类重写父类的虚函数时,父类对应位置的虚函数指针会被替换成子类的虚函数指针
多继承
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA1() {}
virtual void funcA2() {}
void funcA3() {}
private:
int a;
int b;
};
class B {
public:
B() = default;
virtual ~B() = default;
virtual void funcB1() {}
virtual void funcB2() {}
void funcB3() {}
private:
int a;
int b;
};
class C : public A, public B {
public:
virtual void funcA1() {}
virtual void funcB1() {}
virtual void funcC1() {}
void funcC2() {}
private:
int c;
};
class D : public B, public A {
public:
virtual void funcA1() {}
virtual void funcB1() {}
virtual void funcC1() {}
void funcC2() {}
private:
int c;
};
int main(int argc, const char **argv) {
A a;
B b;
C c;
return 0;
}
子类的内存布局如下
*** Dumping AST Record Layout
0 | class C
0 | class A (primary base)
0 | (A vtable pointer)
8 | int a
12 | int b
16 | class B (base)
16 | (B vtable pointer)
24 | int a
28 | int b
32 | int c
| [sizeof=40, dsize=36, align=8,
| nvsize=36, nvalign=8]
可以看到多继承的内存布局中有两个虚表指针,按定义时继承的父类顺序分布,中间参杂者对应类的成员变量,最后为派生类的成员变量。
子类的虚表结构如下
Vtable for 'C' (14 entries).
0 | offset_to_top (0)
1 | C RTTI
-- (A, 0) vtable address --
-- (C, 0) vtable address --
2 | C::~C() [complete]
3 | C::~C() [deleting]
4 | void C::funcA1()
5 | void A::funcA2()
6 | void C::funcB1()
7 | void C::funcC1()
8 | offset_to_top (-16)
9 | C RTTI
-- (B, 16) vtable address --
10 | C::~C() [complete]
[this adjustment: -16 non-virtual]
11 | C::~C() [deleting]
[this adjustment: -16 non-virtual]
12 | void C::funcB1()
[this adjustment: -16 non-virtual]
13 | void B::funcB2()
Vtable for C
C::_ZTV1C: 14 entries
0 (int (*)(...))0
8 (int (*)(...))(& _ZTI1C)
16 (int (*)(...))C::~C
24 (int (*)(...))C::~C
32 (int (*)(...))C::funcA1
40 (int (*)(...))A::funcA2
48 (int (*)(...))C::funcB1
56 (int (*)(...))C::funcC1
64 (int (*)(...))-16
72 (int (*)(...))(& _ZTI1C)
80 (int (*)(...))C::_ZThn16_N1CD1Ev
88 (int (*)(...))C::_ZThn16_N1CD0Ev
96 (int (*)(...))C::_ZThn16_N1C6funcB1Ev
104 (int (*)(...))B::funcB2
可以看到多继承和单继承、多重继承差别很大:
- offset_to_top(0):表明这个虚表地址(A、C)距离对象顶部的偏移量,因为在对象头部所以是0。
- 第一个RTTI指针:含有A、C,表明子类中A、C的虚函数表位置一样,类A、B中的虚函数都在该虚表中
- offset_to_top(-16):表明这个虚表地址(类B)距离对象顶部的偏移量,为什么是16呢?从对象的内存布局可知中间两个虚表指针中间隔了2个int类型的变量,所以是16,如果2个整型去掉,就是-8了。这里用于this指针偏移。当基类B的指针指向派生类的对象C时,执行b->funcB1()时,由于funcB1已经在派生类C中重写了,故此时由于this指针指向的是B类型的对象,需要对this指针进行调整,就是offse_to_top(-16),所以this指针向上调整了16字节,之后调用funcB1(), 就调用重写之后的C的虚表中的funcB1(), 这些带adjustment标记的函数都是需要进行this指针调整的。
- 第二个RTTI指针:含有B,表明下面为B虚表。
- 子类有几个父类就有几个虚表,虚表按冒号后面的顺序排列,子类自己定义的虚函数放在第一个虚表内,未重写的虚函数按声明顺序放在第一个虚表最后面。
- 同样当子类重写父类的虚函数时,父类对应位置的虚函数指针会被替换成子类的虚函数指针
子类的内存布局(当更改多继承中父类顺序为B、A时)如下
*** Dumping AST Record Layout
0 | class D
0 | class B (primary base)
0 | (B vtable pointer)
8 | int a
12 | int b
16 | class A (base)
16 | (A vtable pointer)
24 | int a
28 | int b
32 | int c
| [sizeof=40, dsize=36, align=8,
| nvsize=36, nvalign=8]
可以看到虚表指针顺序也随之调换了下顺序。
子类中虚表的布局(当更改多继承中父类顺序为B、A时)如下
Vtable for 'D' (14 entries).
0 | offset_to_top (0)
1 | D RTTI
-- (B, 0) vtable address --
-- (D, 0) vtable address --
2 | D::~D() [complete]
3 | D::~D() [deleting]
4 | void D::funcB1()
5 | void B::funcB2()
6 | void D::funcA1()
7 | void D::funcC1()
8 | offset_to_top (-16)
9 | D RTTI
-- (A, 16) vtable address --
10 | D::~D() [complete]
[this adjustment: -16 non-virtual]
11 | D::~D() [deleting]
[this adjustment: -16 non-virtual]
12 | void D::funcA1()
[this adjustment: -16 non-virtual]
13 | void A::funcA2()
可以看到虚表结构也有所不同,B、D公用一个虚表地址,在offset 0 的位置,A的虚表变成放在后面了。
虚继承
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA1() {}
virtual void funcA2() {}
virtual void funcA3() {}
virtual void funcA4() {}
private:
int a;
};
class B : virtual public A {
public:
B() = default;
virtual ~B() = default;
virtual void funcA1() {}
virtual void funcA2() {}
virtual void funcB1() {}
virtual void funcB2() {}
void funcB3() {}
private:
int b;
};
int main(int argc, const char **argv) {
A a;
B b;
return 0;
}
子类的内存布局如下
*** Dumping AST Record Layout
0 | class B
0 | (B vtable pointer)
8 | int b
16 | class A (virtual base)
16 | (A vtable pointer)
24 | int a
| [sizeof=32, dsize=28, align=8,
| nvsize=12, nvalign=8]
可以看到和单继承的完全不一样,有两个虚表指针,内存布局先子类的虚表和成员变量再是父类的虚表指针和成员变量。
子类的虚表布局如下
Vtable for 'B' (22 entries).
0 | vbase_offset (16)
1 | offset_to_top (0)
2 | B RTTI
-- (B, 0) vtable address --
3 | B::~B() [complete]
4 | B::~B() [deleting]
5 | void B::funcA1()
6 | void B::funcA2()
7 | void B::funcB1()
8 | void B::funcB2()
9 | vcall_offset (0)
10 | vcall_offset (0)
11 | vcall_offset (-16)
12 | vcall_offset (-16)
13 | vcall_offset (-16)
14 | offset_to_top (-16)
15 | B RTTI
-- (A, 16) vtable address --
16 | B::~B() [complete]
[this adjustment: 0 non-virtual, -24 vcall offset offset]
17 | B::~B() [deleting]
[this adjustment: 0 non-virtual, -24 vcall offset offset]
18 | void B::funcA1()
[this adjustment: 0 non-virtual, -32 vcall offset offset]
19 | void B::funcA2()
[this adjustment: 0 non-virtual, -40 vcall offset offset]
20 | void A::funcA3()
21 | void A::funcA4()
Virtual base offset offsets for 'B' (1 entry).
A | -24
Thunks for 'B::~B()' (1 entry).
0 | this adjustment: 0 non-virtual, -24 vcall offset offset
Thunks for 'void B::funcA1()' (1 entry).
0 | this adjustment: 0 non-virtual, -32 vcall offset offset
Thunks for 'void B::funcA2()' (1 entry).
0 | this adjustment: 0 non-virtual, -40 vcall offset offset
Vtable for B
B::_ZTV1B: 22 entries
0 16
8 (int (*)(...))0
16 (int (*)(...))(& _ZTI1B)
24 (int (*)(...))B::~B
32 (int (*)(...))B::~B
40 (int (*)(...))B::funcA1
48 (int (*)(...))B::funcA2
56 (int (*)(...))B::funcB1
64 (int (*)(...))B::funcB2
72 0
80 0
88 18446744073709551600
96 18446744073709551600
104 18446744073709551600
112 (int (*)(...))-16
120 (int (*)(...))(& _ZTI1B)
128 (int (*)(...))B::_ZTv0_n24_N1BD1Ev
136 (int (*)(...))B::_ZTv0_n24_N1BD0Ev
144 (int (*)(...))B::_ZTv0_n32_N1B6funcA1Ev
152 (int (*)(...))B::_ZTv0_n40_N1B6funcA2Ev
160 (int (*)(...))A::funcA3
168 (int (*)(...))A::funcA4
- "vbase_offset(16)":对象在对象布局中与指向虚基类虚表指针地址的偏移量,从子类中的内存布局可以看到正好是16。
- "vcall_offset(0)":当虚基类A的指针或引用a,实际接受的是B的对象时,执行a->funcA3() 或a->funcA4()的时候,由于funcA3、funcA4未被重写,所以不需要对this指针进行调整,就是0。
- "vcall_offset(-16)":当虚基类A的指针或引用a,实际接受的是B的对象时,执行a->funcA1() 或a->funcA2()的时候,由于funcA1、funcA2已经在派生内中重写,而此时this指针指向的时A类型的对象,需要对this指针进行调整,所以是-16,this指针向上调整后,之后调用相应的函数,就调用到了实际的函数。
- "thunk":表示上面的虚函数表中带有adjustment字段的函数需要先对this指针做调整后,才可以调用到重写后的真正函数。
菱形继承
class A {
public:
A() = default;
virtual ~A() = default;
virtual void funcA1() {}
virtual void funcA2() {}
virtual void funcA3() {}
virtual void funcA4() {}
private:
int a;
};
class B : virtual public A {
public:
B() = default;
virtual ~B() = default;
virtual void funcA1() {}
virtual void funcA2() {}
virtual void funcB1() {}
virtual void funcB2() {}
void funcB3() {}
private:
int b;
};
class C : virtual public A {
public:
C() = default;
virtual ~C() = default;
virtual void funcA1() {}
virtual void funcA3() {}
virtual void funcC1() {}
virtual void funcC2() {}
void funcC3() {}
private:
int c;
};
class D: public B, public C {
public:
D() = default;
virtual ~D() = default;
virtual void funcA1() {}
virtual void funcA3() {}
virtual void funcB1() {}
virtual void funcC1() {}
virtual void funcD1() {}
private:
int d;
};
int main(int argc, const char **argv) {
A a;
B b;
C c;
D d;
return 0;
}
子类的内存布局如下
*** Dumping AST Record Layout
0 | class D
0 | class B (primary base)
0 | (B vtable pointer)
8 | int b
16 | class C (base)
16 | (C vtable pointer)
24 | int c
28 | int d
32 | class A (virtual base)
32 | (A vtable pointer)
40 | int a
| [sizeof=48, dsize=44, align=8,
| nvsize=32, nvalign=8]
可以看到当不看虚继承的A,其他部分和多继承的布局相同,同时可以看到虚继承的类型A保证了在内存只有一份A的成员变量。
我们可以看下当不用虚继承时,即去掉类A、B的虚继承时,子类的内存布局。
*** Dumping AST Record Layout
0 | class D
0 | class B (primary base)
0 | class A (primary base)
0 | (A vtable pointer)
8 | int a
12 | int b
16 | class C (base)
16 | class A (primary base)
16 | (A vtable pointer)
24 | int a
28 | int c
32 | int d
| [sizeof=40, dsize=36, align=8,
| nvsize=36, nvalign=8]
可以看到派生类D中含有两份A的成员变量,分别来自类型B、C,同时函数和成员变量的调用也存在二义性。 所以这就出现菱形继承时为啥要用到虚继承的原因。
当然在虚继承中也有二义性的问题,如funcA1,如果D中不重写funcA1,由于B、C都重写了funcA1,那么编译也会报错存在二义性。
子类的虚表布局如下
Vtable for 'D' (34 entries).
0 | vbase_offset (32)
1 | offset_to_top (0)
2 | D RTTI
-- (B, 0) vtable address --
-- (D, 0) vtable address --
3 | D::~D() [complete]
4 | D::~D() [deleting]
5 | void D::funcA1()
6 | void B::funcA2()
7 | void D::funcB1()
8 | void B::funcB2()
9 | void D::funcA3()
10 | void D::funcC1()
11 | void D::funcD1()
12 | vbase_offset (16)
13 | offset_to_top (-16)
14 | D RTTI
-- (C, 16) vtable address --
15 | D::~D() [complete]
[this adjustment: -16 non-virtual]
16 | D::~D() [deleting]
[this adjustment: -16 non-virtual]
17 | void D::funcA1()
[this adjustment: -16 non-virtual]
18 | void D::funcA3()
[this adjustment: -16 non-virtual]
19 | void D::funcC1()
[this adjustment: -16 non-virtual]
20 | void C::funcC2()
21 | vcall_offset (0)
22 | vcall_offset (-32)
23 | vcall_offset (-32)
24 | vcall_offset (-32)
25 | vcall_offset (-32)
26 | offset_to_top (-32)
27 | D RTTI
-- (A, 32) vtable address --
28 | D::~D() [complete]
[this adjustment: 0 non-virtual, -24 vcall offset offset]
29 | D::~D() [deleting]
[this adjustment: 0 non-virtual, -24 vcall offset offset]
30 | void D::funcA1()
[this adjustment: 0 non-virtual, -32 vcall offset offset]
31 | void B::funcA2()
[this adjustment: 0 non-virtual, -40 vcall offset offset]
32 | void D::funcA3()
[this adjustment: 0 non-virtual, -48 vcall offset offset]
33 | void A::funcA4()
Virtual base offset offsets for 'D' (1 entry).
A | -24
Thunks for 'D::~D()' (2 entries).
0 | this adjustment: -16 non-virtual
1 | this adjustment: 0 non-virtual, -24 vcall offset offset
Thunks for 'void D::funcA1()' (2 entries).
0 | this adjustment: -16 non-virtual
1 | this adjustment: 0 non-virtual, -32 vcall offset offset
Thunks for 'void D::funcA3()' (2 entries).
0 | this adjustment: -16 non-virtual
1 | this adjustment: 0 non-virtual, -48 vcall offset offset
Thunks for 'void D::funcC1()' (1 entry).
0 | this adjustment: -16 non-virtual
可以看到像是在多继承的基础上加上了虚继承的内容。
后续完善
通过以上测试我们了解到了单继承、多重继承、多继承、虚继承和菱形继承的对象的内存布局和虚表布局,同样也应该明白了派生类的析构函数为啥有必要是虚函数。以及为啥菱形继承会用到虚继承。
有时间其实可以通过上面的内存布局及虚表布局,对派生类的指针进行偏移,然后得到各个虚表的地址,再通过虚表地址得到具体的虚函数地址来调用相应的虚函数来对之前的布局进行验证,加深自己的对这一块的理解。
后续完善验证和画出各类型详细的布局图。
希望看到的大佬指出文章得不足。
参考资料
https://www.cnblogs.com/qg-whz/p/4909359.html
欢迎关注我的其它发布渠道
个人博客
公众号