C++虚函数表实现机制以及用C语言对其进行的模拟实现

陪她去流浪 桃子 编辑 阅读次数:132722

前言

大家都应该知道C++的精髓是虚函数吧? 虚函数带来的好处就是: 可以定义一个基类的指针, 其指向一个继承类, 当通过基类的指针去调用函数时, 可以在运行时决定该调用基类的函数还是继承类的函数. 虚函数是实现多态(动态绑定)/接口函数的基础. 可以说: 没有虚函数, C++将变得一无是处!

既然是C++的精髓, 那么我们有必要了解一下她的实现方式吗? 有必要! 既然C++是从C语言的基础上发展而来的, 那么我们可以尝试用C语言来模拟实现吗? 有可能!

接下来, 就是我一步一步地来解析C++的虚函数的实现方式, 以及用C语言对其进行的模拟.

C++对象的内存布局

想知道C++对象的内存布局, 可以有多种方式, 比如:

  • 输出成员变量的偏移, 通过 offsetof 宏来得到
  • 通过调试器查看, 比如常用的VS

只有数据成员的对象

实现如下:

1
2
3
4
5
6
class Base1
{
public:
    int base1_1;
    int base1_2;
};

对象大小及偏移:

字段 偏移
sizeof(Base1) 8
offsetof(Base1, base1_1) 0
offsetof(Base1, base1_2) 4

可知对象布局:

1-1

可以看到, 成员变量是按照定义的顺序来保存的, 最先声明的在最上边, 然后依次保存!

类对象的大小就是所有成员变量大小之和(严格说是成员变量内存对齐之后的大小之和).

没有虚函数的对象

类实现如下:

1
2
3
4
5
6
7
8
class Base1
{
public:
    int base1_1;
    int base1_2;

    void foo(){}
};

结果如下:

字段 偏移
sizeof(Base1) 8
offsetof(Base1, base1_1) 0
offsetof(Base1, base1_2) 4

前面的结果是一样的? 不需要有什么疑问对吧?

  • 因为如果一个函数不是虚函数,那么他就不可能会发生动态绑定,也就不会对对象的布局造成任何影响.
  • 当调用一个非虚函数时, 那么调用的一定就是当前指针类型拥有的那个成员函数. 这种调用机制在编译时期就确定下来了.

拥有仅一个虚函数的类对象

类实现如下:

1
2
3
4
5
6
7
8
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
};

结果如下:

字段 偏移
sizeof(Base1) 12
offsetof(Base1, base1_1) 4
offsetof(Base1, base1_2) 8

咦? 多了4个字节? 且 base1_1base1_2 的偏移都各自向后多了4个字节! 说明类对象的最前面被多加了4个字节的"东东", what's it?

现在, 我们通过VS2013来瞧瞧类Base1的变量b1的内存布局情况:

  • 由于我没有写构造函数, 所以变量的数据没有根据, 但虚函数是编译器为我们构造的, 数据正确!
  • Debug模式下, 未初始化的变量值为0xCCCCCCCC, 即:-858983460

3-1

看到没? base1_1 前面多了一个变量 __vfptr (常说的虚函数表 vtable 指针), 其类型为void**, 这说明它是一个void*指针(注意不是数组)。

再看看[0]元素, 其类型为void*, 其值为 ConsoleApplication2.exe!Base1::base1_fun1(void), 这是什么意思呢? 如果对 WinDbg 比较熟悉, 那么应该知道这是一种惯用表示手法, 她就是指 Base1::base1_fun1() 函数的地址。

可得, __vfptr的定义伪代码大概如下:

1
2
void*        __fun[1] = { &Base1::base1_fun1 };
const void** __vfptr = &__fun[0];

值得注意的是: 上面只是一种我手写的伪代码方式, 语法不一定能通过。

该类的对象大小为12个字节, 大小及偏移信息如下:

字段 偏移
sizeof(Base1) 12
offsetof(__vfptr) 0
offsetof(base1_1) 4
offsetof(base1_2) 8

大家有没有留意这个__vfptr? 为什么它被定义成一个 指向指针数组的指针, 而不是直接定义成一个 指针数组 呢? 我为什么要提这样一个问题? 因为如果仅是一个指针的情况, 您就无法轻易地修改那个数组里面的内容, 因为她并不属于类对象的一部分. 属于类对象的, 仅是一个指向虚函数表的一个指针__vfptr而已, 下一节我们将继续讨论这个问题.

注意到__vfptr前面的const修饰. 她修饰的是那个虚函数表, 而不是__vfptr

现在的对象布局如下:

3-2

虚函数指针__vfptr位于所有的成员变量之前定义.

注意到: 我并未在此说明__vfptr的具体指向, 只是说明了现在类对象的布局情况.

接下来看一个稍微复杂一点的情况, 我将清楚地描述虚函数表的构成.

拥有多个虚函数的类对象

和前面一个例子差不多, 只是再加了一个虚函数. 定义如下:

1
2
3
4
5
6
7
8
9
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

大小以及偏移信息如下:

4-1

有情况!? 多了一个虚函数, 类对象大小却依然是12个字节!

再来看看VS形象的表现:

4-2

呀, __vfptr所指向的函数指针数组中出现了第2个元素, 其值为Base1类的第2个虚函数base1_fun2()的函数地址。

现在, 虚函数指针以及虚函数表的伪定义大概如下:

1
2
void*        __fun[] = { &Base1::base1_fun1, &Base1::base1_fun2 };
const void** __vfptr = __fun[0];

通过上面两张图表, 我们可以得到如下结论:

  • 更加肯定前面我们所描述的: __vfptr只是一个指针, 她指向一个函数指针数组(即: 虚函数表)
  • 增加一个虚函数, 只是简单地向该类对应的虚函数表中增加一项而已, 并不会影响到类对象的大小以及布局情况

前面已经提到过: __vfptr只是一个指针, 她指向一个数组, 并且: 这个数组没有包含到类定义内部, 那么她们之间是怎样一个关系呢?

不妨, 我们再定义一个类的变量b2, 现在再来看看__vfptr的指向:

4-3

通过Watch 1窗口我们看到:

  • b1b2是类的两个变量, 理所当然, 她们的地址是不同的(见 &b1&b2)
  • 虽然b1b2是类的两个变量, 但是: 她们的__vfptr的指向却是同一个虚函数表

由此我们可以总结出:

同一个类的不同实例共用同一份虚函数表, 她们都通过一个所谓的虚函数表指针__vfptr(定义为void**类型)指向该虚函数表.

是时候该展示一下类对象的内存布局情况了:

4-4

不出意外, 很清晰明了地展示出来了吧? :-) hoho~~

那么问题就来了! 这个虚函数表保存在哪里呢? 其实, 我们无需过分追究她位于哪里, 重点是:

  • 她是编译器在编译时期为我们创建好的, 只存在一份
  • 定义类对象时, 编译器自动将类对象的__vfptr指向这个虚函数表

单继承且本身不存在虚函数的继承类的内存布局

前面研究了那么多啦, 终于该到研究继承类了! 先研究单继承!

依然, 简单地定义一个继承类, 如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;
};

我们再来看看现在的内存布局(定义为Derive1 d1):

5-1

没错! 基类在上边, 继承类的成员在下边依次定义! 展开来看看:

5-2

经展开后来看, 前面部分完全就是Base1的东西: 虚函数表指针+成员变量定义. 并且, Base1的虚函数表的[0][1]两项还是其本身就拥有的函数: base1_fun1()base1_fun2()

现在类的布局情况应该是下面这样:

5-3

本身不存在虚函数(不严谨)但存在基类虚函数覆盖的单继承类的内存布局

标题本身不存在虚函数的说法有些不严谨, 我的意思是说: 除经过继承而得来的基类虚函数以外, 自身没有再定义其它的虚函数.

Ok, 既然存在基类虚函数覆盖, 那么来看看接下来的代码会产生何种影响:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    // 覆盖基类函数
    virtual void base1_fun1() {}
};

可以看到, Derive1类 重写了Base1类的base1_fun1()函数, 也就是常说的虚函数覆盖. 现在是怎样布局的呢?

6-1

特别注意我高亮的那一行: 原本是Base1::base1_fun1(), 但由于继承类重写了基类Base1的此方法, 所以现在变成了Derive1::base1_fun1()!

那么, 无论是通过Derive1的指针还是Base1的指针来调用此方法, 调用的都将是被继承类重写后的那个方法(函数), 多态发生了!!!

那么新的布局图:

6-2

定义了基类没有的虚函数的单继承的类对象布局

说明一下: 由于前面一种情况只会造成覆盖基类虚函数表的指针, 所以接下来我不再同时讨论虚函数覆盖的情况.

继续贴代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive1 : public Base1
{
public:
    int derive1_1;
    int derive1_2;

    virtual void derive1_fun1() {}
};

和第5类不同的是多了一个自身定义的虚函数. 和第6类不同的是没有基类虚函数的覆盖.

7-1

咦, 有没有发现问题? 表面上看来几乎和第5种情况完全一样? 为嘛呢? 现在继承类明明定义了自身的虚函数, 但不见了??

那么, 来看看类对象的大小, 以及成员偏移情况吧:

7-2

居然没有变化!!! 前面12个字节是Base1的, 有没有觉得很奇怪?

好吧, 既然表面上没办法了, 我们就只能从汇编入手了, 来看看调用derive1_fun1()时的代码:

1
2
3
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun1();

要注意: 我为什么使用指针的方式调用? 说明一下: 因为如果不使用指针调用, 虚函数调用是不会发生动态绑定的哦! 你若直接 d1.derive1_fun1();, 是不可能会发生动态绑定的, 但如果使用指针: pd1->derive1_fun1(); , 那么 pd1就无从知道她所指向的对象到底是Derive1 还是继承于Derive1的对象, 虽然这里我们并没有对象继承于Derive1, 但是她不得不这样做, 毕竟继承类不管你如何继承, 都不会影响到基类, 对吧?

1
2
3
4
5
6
7
pd1->derive1_fun1();
00825466  mov         eax,dword ptr [pd1]  
00825469  mov         edx,dword ptr [eax]  
0082546B  mov         esi,esp  
0082546D  mov         ecx,dword ptr [pd1]  
00825470  mov         eax,dword ptr [edx+8]  
00825473  call        eax

汇编代码解释:

  • 第2行: 由于pd1是指向d1的指针, 所以执行此句后 eax 就是d1的地址。
  • 第3行: 又因为Base1::__vfptrBase1的第1个成员, 同时也是Derive1的第1个成员, 那么: &__vfptr == &d1, clear? 所以当执行完 mov edx, dword ptr[eax] 后, edx就得到了__vfptr的值, 也就是虚函数表的地址
  • 第5行: 由于是__thiscall调用, 所以把this保存到ecx中。
  • 第6行: 一定要注意到那个 edx+8, 由于edx是虚函数表的地址, 那么 edx+8将是虚函数表的第3个元素, 也就是__vftable[2]!
  • 第7行: 调用虚函数.

结果:

  • 现在我们应该知道内幕了! 继承类Derive1的虚函数表被加在基类的后面! 事实的确就是这样!
  • 由于Base1只知道自己的两个虚函数索引[0][1], 所以就算在后面加上了[2], Base1根本不知情, 不会对她造成任何影响.
  • 如果基类没有虚函数呢? 这个问题我们留到第9小节再来讨论!

最新的类对象布局表示:

7-3

多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局

真快, 该看看多继承了, 多继承很常见, 特别是接口类中!

依然写点小类玩玩:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Base2
{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 基类虚函数覆盖
    virtual void base1_fun1() {}
    virtual void base2_fun2() {}

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

代码变得越来越长啦! 为了代码结构清晰, 我尽量简化定义.

初步了解一下对象大小及偏移信息:

8-1

貌似, 若有所思? 不管, 来看看VS再想:

8-2

哇, 不摆了! 一丝不挂啊! :-)

结论:

  • 按照基类的声明顺序, 基类的成员依次分布在继承中.
  • 注意被我高亮的那两行, 已经发生了虚函数覆盖!
  • 我们自己定义的虚函数呢? 怎么还是看不见?!

好吧, 继承反汇编, 这次的调用代码如下:

1
2
3
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();

反汇编代码如下:

1
2
3
4
5
6
7
pd1->derive1_fun2();
00995306  mov         eax,dword ptr [pd1]  
00995309  mov         edx,dword ptr [eax]  
0099530B  mov         esi,esp  
0099530D  mov         ecx,dword ptr [pd1]  
00995310  mov         eax,dword ptr [edx+0Ch]  
00995313  call        eax

解释下, 其实差不多:

  • 第2行: 取d1的地址
  • 第3行: 取Base1::__vfptr的值
  • 第6行: 0x0C, 也就是第4个元素(下标为[3])

结论:

Derive1的虚函数表依然是保存到第1个拥有虚函数表的那个基类的后面的.

看看现在的类对象布局图:

  • 注:图中有点错误,右上角应该是 void* __vftable[4],多谢 shadow3002 的提醒
  • 注:图中有点错误,Derive1是存在虚函数覆盖的。源图丢失,请读者注意不要被误导。多谢 Oyster 的提醒

8-3

如果第1个基类没有虚函数表呢? 进入第9节!

如果第1个直接基类没有虚函数(表)

这次的代码应该比上一个要稍微简单一些, 因为把第1个类的虚函数给去掉了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Base1
{
public:
    int base1_1;
    int base1_2;
};

class Base2
{
public:
    int base2_1;
    int base2_2;

    virtual void base2_fun1() {}
    virtual void base2_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

来看看VS的布局:

9-1

这次相对前面一次的图来说还要简单啦! Base1已经没有虚函数表了! (真实情况并非完全这样, 请继续往下看!)

现在的大小及偏移情况: 注意: sizeof(Base1) == 8

9-2

重点是看虚函数的位置, 进入函数调用(和前一次是一样的):

1
2
3
Derive1 d1;
Derive1* pd1 = &d1;
pd1->derive1_fun2();

反汇编调用代码:

1
2
3
4
5
6
7
pd1->derive1_fun2();
012E4BA6  mov         eax,dword ptr [pd1]  
012E4BA9  mov         edx,dword ptr [eax]  
012E4BAB  mov         esi,esp  
012E4BAD  mov         ecx,dword ptr [pd1]  
012E4BB0  mov         eax,dword ptr [edx+0Ch]  
012E4BB3  call        eax

这段汇编代码和前面一个完全一样!, 那么问题就来了! Base1 已经没有虚函数表了, 为什么还是把b1的第1个元素当作__vfptr呢?

不难猜测: 当前的布局已经发生了变化, 有虚函数表的基类放在对象内存前面!? , 不过事实是否属实? 需要仔细斟酌.

我们可以通过对基类成员变量求偏移来观察:

9-3

可以看到:

1
2
3
4
5
6
&d1==0x~d4
&d1.Base1::__vfptr==0x~d4
&d1.base2_1==0x~d8
&d1.base2_2==0x~dc
&d1.base1_1==0x~e0
&d1.base1_2==0x~e4

所以不难验证: 我们前面的推断是正确的, 谁有虚函数表, 谁就放在前面!

现在类的布局情况:

9-4

那么, 如果两个基类都没有虚函数表呢?

What if 两个基类都没有虚函数表

代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Base1
{
public:
    int base1_1;
    int base1_2;
};

class Base2
{
public:
    int base2_1;
    int base2_2;
};

// 多继承
class Derive1 : public Base1, public Base2
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

前面吃了个亏, 现在先来看看VS的基本布局:

10-1

可以看到, 现在__vfptr已经独立出来了, 不再属于Base1Base2!

看看求偏移情况:

10-2

Ok, 问题解决! 注意高亮的那两行, &d1==&d1.__vfptr, 说明虚函数始终在最前面!

不用再废话, 相信大家对这种情况已经有底了.

对象布局:

10-3

如果有三个基类: 虚函数表分别是有, 没有, 有!

这种情况其实已经无需再讨论了, 作为一个完结篇....

上代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Base1
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Base2
{
public:
    int base2_1;
    int base2_2;
};

class Base3
{
public:
    int base3_1;
    int base3_2;

    virtual void base3_fun1() {}
    virtual void base3_fun2() {}
};

// 多继承
class Derive1 : public Base1, public Base2, public Base3
{
public:
    int derive1_1;
    int derive1_2;

    // 自身定义的虚函数
    virtual void derive1_fun1() {}
    virtual void derive1_fun2() {}
};

以下是偏移图:

11-1

以下是对象布局图(多谢 Oyster 的手绘):

只需知道: 谁有虚函数表, 谁就往前靠!

C++中父子对象指针间的转换与函数调用

讲了那么多布局方面的东东, 终于到了尾声!

通过前面的讲解内容, 大家至少应该明白了各类情况下类对象的内存布局了. 如果还不会.....呃.....

进入正题~

由于继承完全拥有父类的所有, 包括数据成员与虚函数表, 所以:把一个继承类强制转换为一个基类是完全可行的.

如果有一个Derive1的指针, 那么:

  • 得到Base1的指针: Base1* pb1 = pd1;
  • 得到Base2的指针: Base2* pb2 = pd1;
  • 得到Base3的指针: Base3* pb3 = pd1;

非常值得注意的是:

这是在基类与继承类之间的转换, 这种转换会自动计算偏移! 按照前面的布局方式!

也就是说: 在这里极有可能: pb1 != pb2 != pb3 ~~, 不要以为她们都等于 pd1

至于函数调用, 我想, 不用说大家应该知道了:

  • 如果不是虚函数, 直接调用指针对应的基本类的那个函数
  • 如果是虚函数, 则查找虚函数表, 并进行后续的调用. 虚函数表在定义一个时, 编译器就为我们创建好了的. 所有的, 同一个类, 共用同一份虚函数表.

用C语言完全模拟C++虚函数表的实现与运作方式

如果对前面两大节的描述仔细了解了的话, 想用C语言来模拟C++的虚函数以及多态, 想必是轻而易举的事情了!

前提

但是, 话得说在前面, C++的编译器在生成类及对象的时候, 帮助我们完成了很多事件, 比如生成虚函数表!

但是, C语言编译器却没有, 因此, 很多事件我们必须手动来完成, 包括但不限于:

  • 手动构造父子关系
  • 手动创建虚函数表
  • 手动设置__vfptr并指向虚函数表
  • 手动填充虚函数表
  • 若有虚函数覆盖, 还需手动修改函数指针
  • 若要取得基类指针, 还需手动强制转换
  • ......

总之, 要想用C语言来实现, 要写的代码绝对有点复杂.

C++原版调用

接下来, 我们都将以最后那个, 最繁杂的那个3个基类的实例来讲解, 但作了一些简化与改动:

  • 用构造函数初始化成员变量
  • 减少成员变量的个数
  • 减少虚函数的个数
  • 调用函数时产生相关输出
  • Derive1增加一个基类虚函数覆盖

以下是对类的改动, 很少:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Base1
{
public:
    Base1() : base1_1(11) {}
    int base1_1;
    virtual void base1_fun1() {
        std::cout << "Base1::base1_fun1()" << std::endl;
    }
};

class Base2
{
public:
    Base2() : base2_1(21) {}
    int base2_1;
};

class Base3
{
public:
    Base3() : base3_1(31) {}
    int base3_1;
    virtual void base3_fun1() {
        std::cout << "Base3::base3_fun1()" << std::endl;
    }
};

class Derive1 : public Base1, public Base2, public Base3
{
public:
    Derive1() : derive1_1(11) {}
    int derive1_1;

    virtual void base3_fun1() {
        std::cout << "Derive1::base3_fun1()" << std::endl;
    }
    virtual void derive1_fun1() {
            std::cout << "Derive1::derive1_fun1()" << std::endl;
    }
};

为了看到多态的效果, 我们还需要定义一个函数来看效果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void foo(Base1* pb1, Base2* pb2, Base3* pb3, Derive1* pd1)
{
    std::cout << "Base1::\n"
        << "    pb1->base1_1 = " << pb1->base1_1 << "\n"
        << "    pb1->base1_fun1(): ";
    pb1->base1_fun1();

    std::cout << "Base2::\n"
        << "    pb2->base2_1 = " << pb2->base2_1
        << std::endl;

    std::cout << "Base3::\n"
        << "    pb3->base3_1 = " << pb3->base3_1 << "\n"
        << "    pb3->base3_fun1(): ";
    pb3->base3_fun1();

    std::cout << "Derive1::\n"
        << "    pd1->derive1_1 = " << pd1->derive1_1<< "\n"
        << "    pd1->derive1_fun1(): ";
    pd1->derive1_fun1();
    std::cout<< "    pd1->base3_fun1(): ";
    pd1->base3_fun1();
    
    std::cout << std::endl;
}

调用方式如下:

1
2
Derive1 d1;
foo(&d1, &d1, &d1, &d1);

输出结果:

c-1

可以看到输出结果全部正确(当然了! :-), 哈哈~

同时注意到 pb3->base3_fun1() 的多态效果哦!

用C语言来模拟

必须要把前面的理解了, 才能看懂下面的代码!

为了有别于已经完成的C++的类, 我们分别在类前面加一个大写的C以示区分(平常大家都是习惯在C++写的类前面加C, 今天恰好反过来, 哈哈).

C语言无法实现的部分

C/C++是两个语言, 有些语言特性是C++专有的, 我们无法实现! 不过, 这里我是指调用约定, 我们应该把她排除在外.

对于类的成员函数, C++默认使用__thiscall, 也即this指针通过ecx传递, 这在C语言无法实现, 所以我们必须手动声明调用约定为:

  • __stdcall, 就像微软的组件对象模型(COM)那样
  • __cdecl, 本身就C语言的调用约定, 当然能使用了.

上面那种调用约定, 使用哪一种无关紧要, 反正不能使用__thiscall就行了.

因为使用了非__thiscall调用约定, 我们就必须手动传入this指针, 通过成员函数的第1个参数!

从最简单的开始: 实现 Base2

由于没有虚函数, 仅有成员变量, 这个当然是最好模拟的咯!

1
2
3
4
struct CBase2
{
    int base2_1;
};

有了虚函数表的Base1, 但没被覆盖

下面是Base1的定义, 要复杂一点了, 多一个__vfptr:

1
2
3
4
5
struct CBase1
{
    void** __vfptr;
    int base1_1;
};

因为有虚函数表, 所以还得单独为虚函数表创建一个结构体的哦! 但是, 为了更能清楚起见, 我并未定义前面所说的指针数组, 而是用一个包含一个或多个函数指针的结构体来表示! 因为数组能保存的是同一类的函数指针, 不太很友好! 但他们的效果是完全一样的, 希望读者能够理解明白!

1
2
3
4
struct CBase1_VFTable
{
    void(__stdcall* base1_fun1)(CBase1* that);
};
  • 注意: base1_fun1 在这里是一个指针变量!
  • 注意: base1_fun1 有一个CBase1的指针, 因为我们不再使用__thiscall, 我们必须手动传入! Got it?

Base1的成员函数base1_fun1()我们也需要自己定义, 而且是定义成全局的:

1
2
3
4
void __stdcall base1_fun1(CBase1* that)
{
    std::cout << "base1_fun1()" << std::endl;
}

有虚函数覆盖的 Base3

虚函数覆盖在这里并不能体现出来, 要在构造对象初始化的时候才会体现, 所以: base3其实和Base1是一样的.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct CBase3
{
    void** __vfptr;
    int base3_1;
};

struct CBase3_VFTable
{
    void(__stdcall* base3_fun1)(CBase3* that);
};

Base3的成员函数:

1
2
3
4
void __stdcall base3_fun1(CBase3* that)
{
    std::cout << "base3_fun1()" << std::endl;
}

定义继承类 CDerive1

相对前面几个类来说, 这个类要显得稍微复杂一些了, 因为包含了前面几个类的内容:

1
2
3
4
5
6
7
8
struct CDerive1
{
    CBase1 base1;
    CBase3 base3;
    CBase2 base2;

    int derive1_1;
};

特别注意: CBase123的顺序不能错!

另外: 由于Derive1本身还有虚函数表, 而且所以项是加到第一个虚函数表(CBase1)的后面的, 所以此时的CBase1::__vfptr不应该单单指向CBase1_VFTable, 而应该指向下面这个包含Derive1类虚函数表的结构体才行:

1
2
3
4
5
struct CBase1_CDerive1_VFTable
{
    void (__stdcall* base1_fun1)(CBase1* that);
    void(__stdcall* derive1_fun1)(CDerive1* that);
};

因为CDerive1覆盖了CBase3base3_fun1()函数, 所以不能直接用Base3的那个表:

1
2
3
4
struct CBase3_CDerive1_VFTable
{
    void(__stdcall* base3_fun1)(CDerive1* that);
};

Derive1覆盖Base3::base3_fun1()的函数以及自身定义的derive1_fun1()函数:

1
2
3
4
5
6
7
8
9
void __stdcall base3_derive1_fun1(CDerive1* that)
{
    std::cout << "base3_derive1_fun1()" << std::endl;
}

void __stdcall derive1_fun1(CDerive1* that)
{
    std::cout << "derive1_fun1()" << std::endl;
}

构造各类的全局虚函数表

由于没有了编译器的帮忙, 在定义一个类对象时, 所有的初始化工作都只能由我们自己来完成了!

首先构造全局的, 被同一个类共同使用的虚函数表!

1
2
3
4
5
6
7
// CBase1 的虚函数表
CBase1_VFTable __vftable_base1;
__vftable_base1.base1_fun1 = base1_fun1;

// CBase3 的虚函数表
CBase3_VFTable __vftable_base3;
__vftable_base3.base3_fun1 = base3_fun1;

然后构造CDerive1CBase1共同使用的虚函数表:

1
2
3
4
// CDerive1 和 CBase1 共用的虚函数表
CBase1_CDerive1_VFTable __vftable_base1_derive1;
__vftable_base1_derive1.base1_fun1 = base1_fun1;
__vftable_base1_derive1.derive1_fun1 = derive1_fun1;

再构造CDerive1覆盖CBase3后的虚函数表: 注意: 数覆盖会替换原来的函数指针。

1
2
CBase3_CDerive1_VFTable __vftable_base3_derive1;
__vftable_base3_derive1.base3_fun1 = base3_derive1_fun1;

开始! 从 CDerive1 构造一个完整的 Derive1 类

先初始化成员变量与__vfptr的指向: 注意不是指错了!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
CDerive1 d1;
d1.derive1 = 1;

d1.base1.base1_1 = 11;
d1.base1.__vfptr = reinterpret_cast<void**>(&__vftable_base1_derive1);

d1.base2.base2_1 = 21;

d1.base3.base3_1 = 31;
d1.base3.__vfptr = reinterpret_cast<void**>(&__vftable_base3_derive1);

由于目前的CDerive1是我们手动构造的, 不存在真正语法上的继承关系, 如要得到各基类指针, 我们就不能直接来取, 必须手动根据偏移计算:

1
2
3
4
5
char* p = reinterpret_cast<char*>(&d1);
Base1* pb1 = reinterpret_cast<Base1*>(p + 0);
Base2* pb2 = reinterpret_cast<Base2*>(p + sizeof(CBase1) + sizeof(CBase3));
Base3* pb3 = reinterpret_cast<Base3*>(p + sizeof(CBase1));
Derive1* pd1 = reinterpret_cast<Derive1*>(p);

真正调用:

1
foo(pb1, pb2, pb3, pd1);

调用结果:

last

结果相当正确!!!

源代码

我以为我把源代码搞丢了,结果过了一年多发现其实并没有。--- 2015-12-24(每个圣诞我都在写代码)

有两个,忘了区别了:Source1.cpp, Source2.cpp

标签:C++ · 总结

文章评论 116 发表评论 登出
  1. z

    通俗易懂,很接地气的博客

    1. 桃子

      谢谢🥰

    2. 蟑螂大奖

      写的太好了

      1. 桃子

        谢谢!

      2. clivenz

        通透!

        1. 桃子

          谢谢!

        2. 下山买菜

          为什么桃子已经很久没有写C++了呀,是转行了嘛,哈哈

          1. 桃子

            确实转行了,还在 CS,还是不写 C++ 了🥹

          2. xiaowk5516

            感谢分享这么好的文章,受益良多

            1. 桃子

              谢谢❤️

            2. たいが

              讲的太详细了!!!大赞,简直就是良心拯救者

              1. 桃子

                谢谢 😊

              2. 青木

                楼主:你好。这句话不是很理解哎。 《这是在基类与继承类之间的转换, 这种转换会自动计算偏移! 按照前面的布局方式!》 不应该都是派生类对象的地址吗?

                1. 桃子

                  转换成基类的时候,就是相对于基类来说的地址了,不然基类的函数操作的内存地址不就是错误了的吗? 如果有多个基类,每个都有自己的地址。每继承一次,只是往内存后面追加布局。

                  1. 青木

                    懂你什么意思了,虽然都是派生类这个实例对象,但你转换的时候都要找到具体 继承基类的地址。继承一个基类时,根本不存在这种情况,开始位置就是基类, 指针肯定相等。

                    1. 桃子

                      是的是的,你说得对。

                2. bernardli

                  好文, 讲得很清晰!

                  1. 桃子

                    谢谢。

                  2. lht

                    对于这句话 “类对象的大小就是所有成员变量大小之和 ”, 应该是错的, 因为要进行对齐操作, 比如如下代码 class Base { public: char c; int a; }; cout<<sizeof(Base) << endl; 结果是8

                    1. 桃子

                      是的是的,确实是按对齐后的大小来算的。感谢指正,我修改一下文章。

                    2. &~&

                      厉害,很详细,一下子通透了

                      1. 桃子

                        厉害厉害,一下子就通透!

                      2. TMAIAM

                        感谢博主!!

                        1. 桃子

                          不客气!

                        2. Yip

                          感谢博主分享,之前看了老是忘,这回弄懂了哈哈

                          1. 桃子

                            虽然文章是我写的,但是过了这么多年,有人问我的时候,我也有点忘记了!看来还是得温故知新。😂

                          2. 桃子粉

                            感谢博主,跟着用vs调试了一遍豁然开朗,比摆一大堆理论的强多了。 语言也是通俗易懂,生动活泼hhh

                            1. 桃子

                              感谢阅读😀完整跑一遍并弄懂,估计一天就没了😂

                            2. zReagle

                              博主你好,请问子类覆盖了基类的虚函数之后,子类对象的vptr和基类对象的vptr是否就指向了不同的虚函数表?

                              1. 桃子

                                是不同的。不然原来的基类怎么访问自己本身的虚表呢?把子类转换成基类指针后得到的虚表仍然是子类的。

                              2. Typedef

                                博主写的这么好,竟然不搞C++了,痛心

                                1. Typedef

                                  不过,似乎没提到虚表的合并

                                  1. Typedef

                                    理解有误,此回复当废话

                                  2. 桃子

                                    还有好多有趣的东西等着我去玩儿呢😂,C++生态不怎么好,我积极跑路了。

                                    1. lings

                                      博主去哪嗨了?

                                      1. 桃子

                                        我学 Go 语言写服务器去了。(标准库用来来不要太爽!)

                                  3. huvas

                                    牛逼,大哥你是真的牛逼

                                    1. 桃子

                                      哈哈,感谢耐心看完~😀

                                    2. DEWEY

                                      tql c++弱鸡看的明明白白

                                      1. 桃子

                                        嘿嘿嘿嘿😏

                                      2. SoliRaven
                                        牛批,之前一直不理解虚函数表和虚函数指针的区别,甚至一度认为子类对象内存空间第一项存的就是虚函数表,点赞!
                                        1. 桃子
                                          厉害厉害,给你点赞!?
                                        2. CapMocha
                                          如果是父类指针 指向子类对象,且调用子类对象中 覆写的父类虚函数,但是该子类覆写的虚函数中调用了 子类中的方法(父类也有同名方法),则调用的回是父类中的方法还是子类覆盖后的方法。
                                          1. 桃子
                                            我猜应该是子类中的方法? 你描述得不太清楚,举个例子验证一下就好了。
                                          2. CapMocha
                                            小姐姐在哪里工作,是做C++开发吗,讲的很通俗易懂啊。
                                            1. 桃子
                                              已经不做C++很多年了。
                                            2. rookie

                                              我想知道,两个基类都继承了同一个根类,他们都拥有了根类相同的虚函数,子类又多重继承了两个基类,这样的情况内存是怎么来布局的?

                                              如COM组件一样。

                                               1
                                               2
                                               3
                                               4
                                               5
                                               6
                                               7
                                               8
                                               9
                                              10
                                              11
                                              12
                                              13
                                              14
                                              15
                                              16
                                              17
                                              18
                                              19
                                              20
                                              21
                                              22
                                              23
                                              
                                              interface IX : public IUnknown
                                              {
                                              	virtual void __stdcall Fx1() = 0;
                                              	virtual void __stdcall Fx2() = 0;
                                              };
                                              
                                              interface IY : public IUnknown
                                              {
                                              	virtual void __stdcall Fy1() = 0;
                                              	virtual void __stdcall Fy2() = 0;
                                              };
                                              
                                              class CA : public IX, public IY
                                              {
                                              public:
                                              	HRESULT __stdcall QueryInterface(REFIID riid, void **ppv);
                                              	ULONG __stdcall AddRef(void);
                                              	ULONG __stdcall Release(void);
                                              	void __stdcall Fx1();
                                              	void __stdcall Fx2();
                                              	void __stdcall Fy1();
                                              	void __stdcall Fy2();
                                              }
                                              
                                              1. 桃子
                                                这个应该跟第8种情况类似?我没有验证,现在没有C++环境?
                                              2. 还在找工作的本科弟弟
                                                博主您好,有个问题搜集资料以后没太看明白,想问问您。对于虚拟继承,子类中会新增一个虚函数表指针和偏移指针,新增的虚函数表指针我可以理解,但是这个偏移指针是什么呢?可以帮忙解答一下吗?这里有个网址含有关于虚拟继承的问题https://blog.csdn.net/xy913741894/article/details/52981011博主可以帮忙解答一下吗?谢谢您!
                                                1. 桃子
                                                  抱歉啊?,我已经好几年没写C++了,连个开发环境都没有了。所以你自己多研究一下下?
                                                2. DeJavu
                                                  个人感觉楼主这篇文章总体写的不错,但是似乎有个很严重的问题,子类好像是有自己的虚表吧,并不是说和基类共用一个,楼主写的很有迷惑性,比如C和B都继承于A,如果按楼主的写的逻辑,那A中某个函数应该是被B还是被C覆盖呢? 我觉得这篇文章好像写的更有道理https://www.jianshu.com/p/00dc0d939119
                                                  1. 桃子
                                                    我的意思不是表明继承类和父类共用一个虚函数表呀。 子类继承于父类,我表达的意思是子类有一部分和父类是 完全一样的,但这一部分是子类本身的。这很重要。 这是子类能转换成父类来使用的原因。不同子类的实例使用不同的虚函数表呀,没有彼此覆盖。
                                                  2. 阿苏格拉纳
                                                    答主真的太厉害了,最近看探索对象模型那本书很抽象,看完你的文章豁然开朗。希望答主能多写点东西,让我们学习。再次感谢您的文章。
                                                    1. 阿苏格拉纳
                                                      希望您能写一下对于c++的学习经验。
                                                      1. 桃子
                                                        不客气!帮到你们很开心。? 最近没有研究C++,写得少。?
                                                    2. eren
                                                      厉害,学习啦! 还需要消化一下
                                                      1. 桃子
                                                        嗯,如果发现错误要提醒我哈。
                                                      2. hey
                                                        写的很好啊,感谢感谢
                                                        1. 桃子
                                                          嘿嘿嘿嘿?
                                                        2. 酒香不怕巷子深
                                                          很好的学习资料,排版舒适图解详细,赞赞赞! BTW,我是从 https://github.com/huihut/interview#%E8%99%9A%E5%87%BD%E6%95%B0%E6%8C%87%E9%92%88%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8 过来的
                                                          1. 桃子
                                                            哈哈,欢迎远道而来的朋友? 上面那个C++的面试资料我也看了,很不错!
                                                            1. hi

                                                              一样从GitHub那来的

                                                            2. 安特列的猫
                                                              谢谢...很好的学习资料
                                                              1. 桃子
                                                                不客气!
                                                              2. 楚门有家石头记 https://github.com
                                                                博主关于虚表的相关探究很有用,谢谢!
                                                                1. 桃子
                                                                  谢谢阅读!
                                                                2. baozzz
                                                                  最近正在校招求职阶段,博主关于C++虚函数底层实现的内容真是非常详尽! 感谢博主的指导~
                                                                  1. 桃子
                                                                    不客气!祝取得心仪offers。
                                                                  2. inhzus https://iun.im
                                                                    博主写的真清晰. 感谢博主总结分享!
                                                                    1. 桃子
                                                                      感谢阅读!?
                                                                    2. Oyster
                                                                      8. 多继承且存在虚函数覆盖同时又存在自身定义的虚函数的类对象布局 这一部分的内存布局图貌似有点小错误? Base1部分的虚函数表中的Base1::base1_fun1应该是Derive1::base1_fun1才对?这里出现了重载的。 下面的Base2部分的虚函数表中的Base2:base2_fun2也应该是Base1::base2_fun2才对 另外,作者后面那些多继承的内存布局图中貌似表明作用域的Base1/Base2有些会打错成Base/Base1,函数名也是......
                                                                      1. 桃子
                                                                        多谢指正!多谢耐心的阅读!? 我仔细看了一下,这张图确实存在较多的错误,你说的是对的。对照上面VS的图明显可以看出错误,可能是当时写得匆忙,抱歉! 我改一下文章说明一下。但是那个图,当时是用Visio画的,源文件已经丢了,改不了了。?
                                                                        1. 曹尼玛
                                                                          涛爷牛逼
                                                                          1. 桃子
                                                                            你是哪个!莫搞事情。
                                                                      2. 龙的传人 http://pengsi.imwork.net:8000
                                                                        这篇文章通俗易懂,已转载
                                                                        1. 桃子
                                                                          貌似你没转载完整。。。
                                                                          1. 龙的传人 http://pengsi.imwork.net:8000
                                                                            剩下的准备转为另一篇,虚函数表看前面的就够了
                                                                        2. pp
                                                                          看这个文章的风格就是女生写的~,内容很好,就是结构有点乱,估计一般的看到一般都奔溃了。 正确顺序大致应该是:无虚函数,虚函数,单继承,重复继承啥的
                                                                          1. 桃子
                                                                            啊哈哈???,终于有人说乱的了,其实我也觉得非常乱。。。 内容是很多年前写的了,那时候C++并不熟悉,胡乱写了一通。
                                                                          2. luckyscript
                                                                             1
                                                                             2
                                                                             3
                                                                             4
                                                                             5
                                                                             6
                                                                             7
                                                                             8
                                                                             9
                                                                            10
                                                                            11
                                                                            12
                                                                            13
                                                                            14
                                                                            15
                                                                            16
                                                                            17
                                                                            18
                                                                            19
                                                                            20
                                                                            21
                                                                            22
                                                                            23
                                                                            
                                                                            class B {
                                                                            public:
                                                                                virtual void foo() {}
                                                                            };
                                                                            
                                                                            class D: public B {
                                                                            public:
                                                                                D() : mA(0) {}
                                                                                virtual void foo() {
                                                                                    cout<<"D::foo::mA "<<mA<<endl;
                                                                                }
                                                                                int mA;
                                                                            };
                                                                            
                                                                            int main() {
                                                                                D d1;
                                                                                D* pD = &d1;
                                                                                cout<<pD<<endl;
                                                                                typedef void (*PFun)();
                                                                                PFun fun =  (PFun)((long *)*((long *)*(long*)(pD)));
                                                                                fun();
                                                                                cout<<"D::pD::mA: "<<pD->mA<<endl;
                                                                            }
                                                                            

                                                                            博主想问下网上看到的这串代码,为啥是用(long *)来对指针做强类型转换来取到虚函数表的呢?不是很懂这里。

                                                                            1. 桃子
                                                                              *(long*)(pD) 用这种方式能取到虚函数表指针,但是并不好。 因为虚函数指针是二级指针,即便要用 long 也应该 long**。 另外,sizeof(任意指针) 结果都是一样的。
                                                                              1. luckyscript

                                                                                多谢解答。 我试了一下

                                                                                1
                                                                                
                                                                                PFun fun =  (PFun)(*(*(long**)(pD)));
                                                                                

                                                                                这样也是可以的。但是不是很明白为啥指向虚函数表需要强类型转换一下这里的指针。

                                                                                1. luckyscript
                                                                                  直接*(*pD)这样不可以么。
                                                                                  1. 桃子
                                                                                    pD的类型是D*,*pD得到D,然后 *D ??? 这是什么操作?
                                                                                    1. luckyscript
                                                                                      明白了。我刚才自己也理解通了。多谢多谢。博客蛮好的,收藏了嘻嘻。^_^
                                                                                      1. 桃子
                                                                                        谢谢~~?
                                                                            2. ooooooz
                                                                              请教大神 一个问题。我看你讨论到了一个点就是 为什么是把指针放到内存,而不是直接把虚表放到内存, 我理解你的意思是,那么之后虚表里面要是有新的东西了,也不会改变内存的layout。 那么问题就是 ,为什么非要把 虚指针 放到最前面呢。 放到最前面有什么好处呢。 请指教。多谢 ~
                                                                              1. 桃子
                                                                                放在最前面的好处,简单来说,就是简单。方便拿到虚函数表。目前的多数编译器实现都是这样的。
                                                                                1. ooooooz
                                                                                  感谢大神回复。 至于,为什么是把指针放到内存,而不是直接吧虚表放到内存的原因 有没有可能也是为了 节省空间嘛,比如实例化了1000个base1. 如果把虚表放到内存,岂不是要存1000次 用指针的话,就存同一个指针就可以了 是吧~~ 那么基于这个逻辑。你在例子4中说,对象b1 和b2虽然各自保存了一个虚指针,但指针 其实一样,也就是 指向了同一个虚表 但是在例子8中,为什么 b1 和b2的虚指针的地址又不同了呢。 十分感谢解答,,我刚接触了几天c++,如果问了什么白痴问题,也请见谅。
                                                                                  1. ooooooz
                                                                                    好吧 ,请忽略我的问题,,我没看仔细,不好意思 啦
                                                                                    1. 桃子
                                                                                      说得没错。同一个类的不同实例是共享虚函数表的,完全没有必要直接存虚表,一个指针指过去就好了。 这篇文章需要慢慢看,很慢很慢。确保把前面讲的都看懂了再继续往后。
                                                                                2. ooooooz
                                                                                  我是通过你的那张 无敌 炫酷的 图片,寻址找进来的。 真是赞。。厉害而有趣。
                                                                                  1. 桃子
                                                                                    哈哈,是CSDN上面吧?? 破CSDN抄袭太多,我已经完全阻止了来自CSDN的图片盗用。
                                                                                  2. luver
                                                                                    你好,首先谢谢你的文章,看完收获很多。然后我看到这句话,不太理解,如果你有时间看到,能否帮我再讲讲,先谢谢了。 这是在基类与继承类之间的转换, 这种转换会自动计算偏移! 按照前面的布局方式! 也就是说: 在这里极有可能: pb1 != pb2 != pb3 ~~, 不要以为她们都等于 pd1!
                                                                                    1. 桃子
                                                                                      意思就是说,编译器知道转换的这几个类之间具有继承关系。 你在把一个继承类指针转换为基类指针的时候,编译器会自动减去某个偏移,使其符合基类的内存布局。 因为继承类本身拥有的成员是排布在基类后面的嘛,所以,「减去」某个偏移就可以转换成基类了。 既然减了,那么,它们的指针值未必就相同了。 反之亦然。
                                                                                      1. luver
                                                                                        谢谢你耐心的解答,棒!!
                                                                                        1. 桃子
                                                                                          不客气,有问题欢迎继续讨论?
                                                                                          1. luver
                                                                                            好的 不过还是要感谢你的文章!
                                                                                    2. tianzex
                                                                                      第八节最后那个图是不是写错了,为什么虚函数表里面没有Derive1覆盖的函数呢
                                                                                      1. 桃子
                                                                                        呃,红色的那两行不就是嘛?
                                                                                        1. tianzex
                                                                                           1
                                                                                           2
                                                                                           3
                                                                                           4
                                                                                           5
                                                                                           6
                                                                                           7
                                                                                           8
                                                                                           9
                                                                                          10
                                                                                          11
                                                                                          
                                                                                          public:
                                                                                              int derive1_1;
                                                                                              int derive1_2;
                                                                                          
                                                                                              // 基类虚函数覆盖
                                                                                              virtual void base1_fun1() {}
                                                                                              virtual void base2_fun2() {}
                                                                                          
                                                                                              // 自身定义的虚函数
                                                                                              virtual void derive1_fun1() {}
                                                                                              virtual void derive1_fun2() {}
                                                                                          

                                                                                          上边两个呢

                                                                                          1. 桃子
                                                                                            咦,我发现这个图好像错得有点多[冷汗],覆盖也没体现出现。。。 看上面VS的图应该可以看懂。那个图我记得是用Visio做的,源图已经丢失了,改不了了。
                                                                                            1. tianzex
                                                                                              已经很好了,您是我在网上查的介绍的最详细的,谢谢您
                                                                                              1. 桃子
                                                                                                嗯嗯,不客气?
                                                                                      2. 666
                                                                                        good
                                                                                        1. badaji
                                                                                          请教博主一个问题,析构的时候,子类是如何调用到基类的虚析构函数的呢
                                                                                          1. 桃子
                                                                                            析构函数我没研究过是怎么调用的。 不过猜测应该是在子类的析构函数代码的结尾插入了一段对基类的析构函数的调用。 你可以研究一下,顺便分享一下?
                                                                                            1. badaji
                                                                                              调研了一下,博主的猜想是对的,编译器会自动生成一个析构函数,在析构函数中运行完用户自定义代码后调用基类的析构函数。
                                                                                              1. 桃子
                                                                                                好的,多谢。?
                                                                                          2. shadow3002

                                                                                            首先感谢博主的文章, 对我的帮助很大.

                                                                                            8. 多继承且存在虚函数... 这里最后一张图的右上角应该是 void* _vftable[4] 虽然是微不足道的... 然后查看偏移和对象大小可以在源文件属性->C\C++->命令行 加入一下代码 /d1 reportAllClassLayout

                                                                                            然后在输出就会有所有被声明的类的相关信息, 比如

                                                                                            1>class Base5   size(12):
                                                                                            1>      +---
                                                                                            1> 0    | {vfptr}
                                                                                            1> 4    | base1_1
                                                                                            1> 8    | base1_2
                                                                                            1>      +---
                                                                                            
                                                                                            1>class Derive5 size(20):
                                                                                            1>      +---
                                                                                            1> 0    | +--- (base class Base5)
                                                                                            1> 0    | | {vfptr}
                                                                                            1> 4    | | base1_1
                                                                                            1> 8    | | base1_2
                                                                                            1>      | +---
                                                                                            1>12    | derive1_1
                                                                                            1>16    | derive1_2
                                                                                            1>      +---
                                                                                            
                                                                                            1>Base5::$vftable@:
                                                                                            1>      | &Base5_meta
                                                                                            1>      |  0
                                                                                            1> 0    | &Base5::base1_fun1
                                                                                            1> 1    | &Base5::base1_fun2
                                                                                            
                                                                                            1>Derive5::$vftable@:
                                                                                            1>      | &Derive5_meta
                                                                                            1>      |  0
                                                                                            1> 0    | &Base5::base1_fun1
                                                                                            1> 1    | &Base5::base1_fun2
                                                                                            
                                                                                            1. shadow3002
                                                                                              还有一个细节的地方, 就是虚函数表末尾会有一个终止标记, 所以虚函数表大小总会比其包含的虚函数指针大一, 这一点可以在VS2017的监视工具中看到, 一开始因为博主没有包含使我困惑许久, 还以为是编译器的原因. 我才刚开始学Cpp, 想了很久还是想不透为什么要在末尾加上一个终止标记...
                                                                                              1. 桃子
                                                                                                终止标记这个我当时没有研究到。 估计是为了某种错误检测弄出来的吧。 或者习惯性地在最后增加一个空?
                                                                                              2. 桃子
                                                                                                嘿嘿,多谢提醒,已经改正啦。(源图丢了,只能加文字说明了?) 哇,如果早知道这个选项,就可以省很多事情了?
                                                                                              3. kingkey
                                                                                                good,nice
                                                                                                1. zjsxcm
                                                                                                  void __stdcall base3_fun1(CBase1* that) 作为CBase3的虚函数 这个应该传进来的是 CBase3* 型的函数指针吧
                                                                                                  1. 女孩不哭
                                                                                                    呃,貌似写错了? 写了很久了,一时之间难看出问题。:-)
                                                                                                  还没有用户发表过评论,我要发表评论
                                                                                                  编辑评论