c++内存分布之虚函数(单一继承)
阅读原文时间:2023年07月11日阅读:1

1.虚函数表指针 和 虚函数表

  • 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关。多一个父类,派生类就多一个虚函数表指针,同时,派生类的虚函数表就额外增加一个
  • 1.2 派生类和父类同时含有虚函数,派生类的虚函数按照父类声明的顺序(从左往右),存放在继承的第一个父类中虚函数表后面,而不是单独再额外建立一张虚函数表
  • 1.3 按照先声明、先存储、先父类、再子类的顺序存放类的成员变量
  • 1.4 无论是派生类还是父类,当出现了虚函数(普通虚函数、虚析构函数、纯虚函数),排在内存布局最前面的一定是虚函数表指针

2.覆盖继承

其实,覆盖继承不够准确。

2.1 成员变量覆盖

  • 派生类和父类出现了同名的成员变量时,派生类仅仅将父类的同名成员隐藏了,而非覆盖替换
  • 派生类调用成员变量时,按照就近原则,调用自身的同名变量,解决了当调用同名变量时出现的二义性的现象

2.2 成员函数覆盖

需要考虑是否有虚函数的情况

存在虚函数的覆盖继承

父类和派生类出现了同名虚函数函数((普通虚函数、纯虚函数),派生类的虚函数表中将子类的同名虚函数的地址替换为自身的同名虚函数的地址-------多态出现

不存在虚函数的覆盖继承

父类和派生类同时出现同名成员函数,这与成员变量覆盖继承的情况是一样的,派生类屏蔽父类的同名函数

  • 本文代码演示环境: VS2017,演示项目为32位项目

  • 要知道,影响c++内存分布的因素有很多,本文主要概述虚函数对内存分布的影响。

  • 文章篇幅略长,其中夹杂了代码和图片,占用篇幅。但是这样却能更清楚的说明问题。

  • 由于之前没有很细致的了解这方面知识,所以,文章会很啰嗦

  • 本文主要围绕基类派生类展开下面的几种情况的内存分布

  • 代码写的不够规范: 因为多态中,任何带虚函数的基类类的析构函数都应该是虚析构函数。但是我这里没有写出来,目的是缩短文章篇幅。

单一继承(本文主要展开)

序号

情况(单继承)

1

基类是否含有虚函数,无派生类

2

基类和派生类均不含有成员函数(含虚函数),仅仅包含成员变量的继承

3

基类含有虚函数,派生类没有虚函数

4

基类和派生类均含有虚函数(无覆盖)

5

基类不含虚函数,仅派生类含有虚函数

6

派生类定义了与基类同名同类型的成员变量

7

派生类定义了与基类同名同形式的成员函数(非虚函数)

8

派生类定义了与基类同名同姓是的虚函数

多继承

序号

情况(多继承,基类个数:2)

1

基类均无虚函数(A,派生类有虚函数,B,派生类不含有虚函数)

2

两个基类中只有一个类有虚函数(A,派生类有虚函数,B,派生类不含有虚函数)

3

两个基类中都含有虚函数(A,派生类有虚函数,B,派生类不含有虚函数)

类成员函数

函数

说明

虚函数

晚绑定/运行时绑定。编译期间无法确定其地址(多态?)

非虚函数

编译器编译时就已经确定了地址

类的初始化规则

影响因素:基类、派生类(继承)、静态成员变量、常成员变量。初始化规则如下:

1.基类的静态变量或全局变量
2.派生类的静态变量或全局变量
3.基类的成员变量
4.派生类的成员变量

注意

1.成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。
2.如果不使用初始化列表初始化,在构造函数内初始化时,此时与成员变量在构造函数中的位置有关。
3.类中const成员常量必须在构造函数初始化列表中初始化。
4.类中static成员变量,必须在类外初始化。

一个例子,

class base
{
private:
      int _a;
      int _b;
public:
      base():_b(0), _a(_b+3) {}
      void show()
      {
            std::cout << "a = " << _a << ", b = " << _b << std::endl;
      }
};

base ba;
ba.show(); //输出: 0, @¥%……&*

知道了上面的规则,就知道这里输出的是: 0, $%##@(未知)。因为采用初始化列表的方式,成员变量的值只与成员变量声明定义的顺序有关,与初始化列表中的顺序无关。如果不采用初始化列表的方式初始化,将其放入到构造函数中,那么,成员变量的值与赋值的顺序有关。


note 下面先展开 无覆盖继承下的内分布情况


1.1 只有成员变量

1.1.1 代码

定义了一个基类,基类没有虚函数

class baseA
{
private:
  int _ma = 1;
  int _mb = 2;
};

baseA ba;

1.1.2 内存分布

通过VS查看其内存布局情况

1>class baseA    size(8):
1>    +---
1> 0    | _ma
1> 4    | _mb
1>    +---

使用sizeof的结果(预测:两个整形,sizeof(baseA) = 8字节。)

果然,同上面的内存分布输出统一。

1.2 只有成员变量和普通成员函数

依照上面的基础,再增加一个成员函数试试。看看结果。

1.2.1 代码

class baseA
{
public:
    void show()
    {
        std::cout << "&_ma=" << &_ma << std::endl;
        std::cout << "&_mb=" << &_mb << std::endl;

    }

private:
    int _ma = 1;
    int _mb = 2;
};

1.2.2 内存分布

1>class baseA    size(8):
1>    +---
1> 0    | _ma
1> 4    | _mb
1>    +---

sizeof结果:

1.2.3 分析与结论

分析:与只有成员变量的情况一致。函数show是普通成员函数,不是虚函数,故编译器在编译期间就已经确定了其地址(偏移)。

同时也可以得知,成员变量存储是按照先声明先存储的顺序。

2.1 添加一个虚函数

根据只有成员变量的基础,我们再增加一个虚函数(不是纯虚函数)。 观察其内存分布

2.1.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "baseA::show()\n\n"; }

private:
    int _ma = 1;
    int _mb = 2;
};

2.1.2 内存分布

1>class baseA    size(12):
1>    +---
1> 0    | {vfptr}
1> 4    | _ma
1> 8    | _mb
1>    +---
1>
1>baseA::$vftable@:
1>    | &baseA_meta
1>    |  0
1> 0    | &baseA::show
1>

有点意思了,增加一个虚函数后,内存分布中出现传说中的虚函数表虚函数表指针

通过监视,可以更加清楚的看见,对象ba中出现了虚函数指针和虚表,此时,虚函数表指针指向的就是虚表的第一个元素:baseA::show()函数。

2.1.3 sizeof

此时,计算sizeof的结果是:

对比可知:

情形

分析

只有成员变量

大小由_ma和_mb组成:8字节

只有一个虚函数

大小由_ma,_mb 和 vfptr组成: 4 + 4 + 4 = 12字节

2.1.4 分析

  • 虚函数表指针: 当类中出现了虚函数,内存分布中一定会存在对应的虚函数表指针,且占4字节(32位)。
  • 虚函数表:当类中出现虚函数,那么必然出现虚函数表,且存放虚函数的地址。
  • 当类中出现虚函数,那么,排在内存分布图最前的是虚函数表指针,然后再是成员变量。

2.2 增加到2个虚函数

根据2.1的基础,再增加一个虚函数:

2.2.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "baseA::show()\n\n"; }
    virtual void play() { std::cout << "baseA::play()\n\n"; }

private:
    int _ma = 1;
    int _mb = 2;
};

2.2.2 内存分布

1>class baseA    size(12):
1>    +---
1> 0    | {vfptr}
1> 4    | _ma
1> 8    | _mb
1>    +---
1>
1>baseA::$vftable@:
1>    | &baseA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &baseA::play
1>
1>baseA::show this adjustor: 0
1>baseA::play this adjustor: 0

2.2.3 sizeof

此时结果

2.3 分析

  • 为什么sizeof()求出的长度都是12? 因为函数指针只有这一个,只是,它指向的时虚函数表的第一个元素,一个指针在32位下的长度为4个字节。
  • 当出现虚函数时,内存分布中,虚函数指针排在了最前面,然后才是类的成员变量
  • 通过监视可知,虚函数指针的地址和虚函数表的第一个元素的地址(数组的起始地址)不相同,说明是两个变量。这也证明了,含有虚函数的类中的虚函数指针不会随着虚函数个数增加而增加

2.4 结论

  • 一个类存在虚函数时,排在最前面的时虚函数指针,然后时成员变量
  • 一个类存在2个及以上的虚函数,此时,内存分布中,并不会增加额外的虚指针,且sizeof的大小也不会增加。

基类虚函数的情况已经分析结束,下面将开始分析 单一继承情况下含有虚函数的内存分布。


3.1 只含有成员变量的继承

基类只有成员变量_ma和_mb,它是派生类deriveA的基类,派生类无成员变量和成员函数。观察派生类的内存分布情况

3.1.1 代码

class baseA
{
public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{
    ;
};

3.1.2 内存分布

1>class deriveA    size(8):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | _ma
1> 4    | | _mb
1>    | +---
1>    +---

3.1.3 sizeof

3.1.4 分析

  • sizeof计算的长度与只有成员变量的情况是一样的,内存分布输出,派生类继承了基类的成员变量,所以:长度为8字节。
  • 派生类的内存分布最前面是基类的成员变量。

3.2 派生类增加成员变量

基类不变,派生类增加成员变量,观察派生类的内存分布情况

3.2.1 代码

class baseA
{
public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

3.2.2 内存分布

1>class deriveA    size(16):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | _ma
1> 4    | | _mb
1>    | +---
1> 8    | _derive_c
1>12    | _derive_d
1>    +---

3.2.3 sizeof

此时,增加派生类的特有成员变量后,再计算sizeof的结果

3.2.4 分析

  • 内存排布:先基类成员变量,再是派生类成员变量。
  • 派生类计算sizeof=基类成员变量长度+派生类成员变量长度之和。

Note: 成员函数不是虚函数,故内存分布图中不会出现虚函数表指针和虚函数表。


基类含有虚函数,派生类(不含有虚函数)继承后,观察派生类内存布局

4.1 只有一个虚函数的基类

4.1.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }

public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{

private:
    int _derive_c = 3;
    int _derive_d = 4;
};

4.1.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show

4.1.3 sizeof

内存分布已经给出了答案: sizeof(deriveA) = 20. 根据上面的经验,基类含有一个虚函数,故sizeof(baseA) = 4 + 4 + 4 = 12.

4.1.4 分析

  • 派生类的内存分布

    • 1.基类的虚函数指针
    • 2.基类的成员变量(先声明先存储)
    • 3.派生类的成员变量
  • 派生类的虚函数表指针仍然只有一个。看看监视结果:

监视可知,最先是虚函数表指针,然后是基类的成员变量,最后是派生类的成员变量。其与内存分布图是一致的。

4.2 基类再加一个虚函数

4.2.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
    virtual void play() { std::cout << "virtual baseA::play() \n\n"; }

public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{

private:
    int _derive_c = 3;
    int _derive_d = 4;
};

4.2.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &baseA::play

4.2.3 sizeof

预测输出: sizeof(派生类) = 20, sizeof(基类) = 12.

4.2.4 分析

  • 监视输出

    • 最先是基类虚函数表指针,然后是基类成员变量,最后才是派生类成员变量
    • 这与上面的内存分布输出结果一致
  • 虚函数表

    • 对比4.1可知,基类增加了一个虚函数,虚函数表也增加了对应的虚函数。

标题有点绕: A、基类和派生类都有虚函数,B、派生类继承一个基类。

5.1 基类和派生类都只有一个虚函数

此时,基类和派生类同时出现了虚函数,派生类的内存分布情况是?可以先预测下。

5.1.1 代码

// 基类
class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
public:
    int _ma = 1;
    int _mb = 2;
};

// 派生类
class deriveA : public baseA
{
public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

5.1.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::action

5.1.3 sizeof

sizeof()

单位(字节)

基类

12

派生类

20

5.1.4 分析

  • 监视

    • 监视可以看到,派生类在此时,内存先后依次为:虚函数表指针、基类成员变量、派生类成员变量。 这与章节4中的内存布局是一致的。
    • 增加虚函数后,派生类此时的虚函数表指针仍然只有一个。
  • 虚函数表

    • 这与章节4.2中的情况是一致的,当增加一个虚函数时,虚函数表中就增加对应虚函数地址。
    • 虚函数表中的函数顺序: 先基类,再派生类,先声明先存储。

5.2 基类有2个虚函数,派生类只有一个虚函数

上面观察到基类和派生类各自都只有一个虚函数的情况,那么,基类的虚函数增加到2个后,派生类的内存分布情况呢?

5.2.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
    virtual void play() { std::cout << "virtual baseA::play() \n\n"; }
public:
    int _ma = 1;
    int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

5.2.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &baseA::play
1> 2    | &deriveA::action
  • 注意到了吧,虚函数表又额外增加了一个虚函数: 基类新增的虚函数。但是内存分布并没有发生变化。

5.2.3 sizeof

sizeof()

单位(字节)

基类

12

派生类

20

5.2.4 分析

  • 内存分布与5.1中的相同,唯一不同的是虚函数表额外增加了一个函数:play。同时可以知道,虚函数表中存储的顺序与上面5.1中总结的顺序时一致的: 先基类,再派生类,先定义,先存储的顺序。

  • 监视结果

    监视结果与5.1中显示的结果时一致的。说明我们之前的结论时正确的: 含有多个虚函数的类对应的虚函数表指针有且只有一个,不会随着虚函数个数增加而增加,且继承关系中也不会发生变化。

5.3 单个虚函数的基类和2个虚函数的派生类

接着开始查看:基类只有单个虚函数和派生类有2个虚函数的情况。观察派生类的内存分布

5.3.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
public:
    int _ma = 1;
    int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
    virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

5.3.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::action
1> 2    | &deriveA::dangcing
  • 你肯定又注意到了,虚函数表仅仅增加了派生了新增的虚函数,且其成员变量的内存分布并没有发生变化。

5.3.3 sizoef

sizeof()

单位(字节)

基类

12

派生类

20

5.3.4 分析

  • 监视结果

  • 成员变量内存分布

    • 此时,与5.15.2中的内存分布是一致的。先是基类的虚函数表指针,再是基类的成员变量,最后是派生类成员变量
  • 虚函数表

    • 增加了一个派生类的虚函数dancing

5.4 基类含有2个虚函数,派生类含有2个虚函数

这种情况,更像是上面5.25.3的汇总。但还是要本着实事求是的精神,看看派生类的内存分布结果。

5.4.1 代码

class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
    virtual void play() { std::cout << "virtual baseA::play() \n\n"; }
public:
    int _ma = 1;
    int _mb = 2;
};
// 派生类
class deriveA : public baseA
{
public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
    virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

5.4.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &baseA::play
1> 2    | &deriveA::action
1> 3    | &deriveA::dangcing

5.4.3 sizeof

sizeof()

单位(字节)

基类

12

派生类

20

5.4.4

  • 监视结果

  • 成员变量内存分布

    • 与前面5.25.3章节相同
  • 虚函数表

    • 相对5.3,虚函数表增加了函数play。注意其顺序。

5.5 总结

  • 虚函数表存储顺序:先基类,再派生类,先声明先存储。
  • 单一继承中,基类和派生类中的虚函数个数不会影响虚函数表指针的个数。
  • 含有虚函数的基类和派生类,计算类所占长度=成员变量长度+虚函数表指针的长度(32位下4字节,64位下8字节)
  • 派生类的成员变量存储顺序与虚函数表中的存储顺序是一致的: 先基类再派生,先声明,先存储。

note: 注意到没有,前面漏掉了一种情况:基类无虚函数且派生类含有虚函数。 下面紧接着的一个章节将概述。


6.1 派生类含有1个虚函数

基类不存在虚函数,但是派生类存在一个虚函数,观察派生类的内存分布情况

6.1.1 代码

class baseA
{
public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{

public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

6.1.2 内存布局

1>    +---
1> 0    | {vfptr}
1> 4    | +--- (base class baseA)
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &deriveA::action

Note: 注意到没有?{vfptr}出现的位置与上面的几中情况都不一样,{vfptr}放在了最前面,且放在了基类成员变量之前。此时,虚函数是声明在派生类,基类没有虚函数。

6.1.3 sizeof

sizeof()

单位(字节)

基类

8(4 + 4)

派生类

20(4(_ma)+4(_mb) +4(虚函数表指针)+4(_derive_c) +4(_derive_d))

6.1.4 分析

  • 监视结果

  • 虚函数表指针{vfptr} 现在属于派生类拥有,而非基类拥有。因为基类没有虚函数,派生类才有虚函数。

  • 虚函数表,与上面的虚函数表存储方式一致。

6.2 派生类含有2个虚函数,基类不含有虚函数

既然如此,继续增加派生类的虚函数到2个,基类仍然没有虚函数,观察派生类的内存分布情况

6.2.1 代码

class baseA
{
public:
    int _ma = 1;
    int _mb = 2;
};

class deriveA : public baseA
{
public:
    virtual void action() { std::cout << "virtual deriveA::action() \n\n"; }
    virtual void dangcing() { std::cout << "virtual deriveA::dangcing() \n\n"; }
private:
    int _derive_c = 3;
    int _derive_d = 4;
};

6.2.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | {vfptr}
1> 4    | +--- (base class baseA)
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _derive_d
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &deriveA::action
1> 1    | &deriveA::dangcing
1>
1>deriveA::action this adjustor: 0
1>deriveA::dangcing this adjustor: 0
  • 仅虚函数表中增加了新增的虚函数dancing。其他与6.1.2中的布局一致

6.2.3 sizeof

sizeof()

单位(字节)

基类

8(4 + 4)

派生类

20(4(_ma)+4(_mb) +4(虚函数表指针)+4(_derive_c) +4(_derive_d))

  • 这与6.1.3中结果完全一致。

6.2.4 分析

  • 监视结果

  • 虚函数表

    • 存储顺序与章节5总结的一致
  • 内存布局

    • 由于基类不存在虚函数,派生类存在虚函数,故此时,虚函数表指针属于派生类而非基类。
  • 虚函数表指针

    • 虚函数表指针仍然不会随着派生类的虚函数个数增加而增加,依然保持1个。

  • note: 前面观察了没有覆盖继承的情况下,派生类的内存分布情况。下面展开继承覆盖.
  • 覆盖
    • 覆盖成员变量
    • 覆盖成员函数

1.1 派生类成员变量覆盖某个基类成员变量

基类定义了两个成员变量,派生类也定义了两个成员变量,不同的的是,派生类与基类定义了一个同名成员变量_mb。

1.1.1 代码

// 基类
class baseA
{
public:
    int _ma = 1;
    int _mb = 2;
};

// 派生类
class deriveA : public baseA
{
public:
    int _derive_c = 3;
    int _mb = 4;
};

deriveA da;
std::cout << da._mb; // 将输出4

1.1.2 内存布局

1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | _ma
1> 4    | | _mb
1>    | +---
1> 8    | _derive_c
1>12    | _mb
1>    +---
  • 按照前面总的经验:成员变量存储顺序为 先基类,再派生类,先声明,先存储。 所以: 先存储的是: 基类的_ma和_mb成员变量,然后是派生类的成员变量_derive_c, _mb。
  • 侧面证明该结论适用单一继承中。

1.1.3 sizeof

sizeof()

单位(字节)

基类

8

派生类

16

1.1.4 分析

监视结果

  • 成员变量存储顺序:这与虚函数表存储顺序是一致的,先基类,再派生类,先声明,先存储。
  • 派生类与基类存在同名成员变量_mb,使用时,派生类中若需要使用基类的成员变量_mb, 需要使用作用类名+范围解析运算符(::)限定。否则,按照就近原则,则使用的是派生类的_mb。 所以,da._mb的值为4. 说了这么多,总结下: 如果派生类覆盖了基类的成员变量,派生类对象调用该成员变量时是调用派生类对象的该成员变量。
  • 但是根绝内存分布图和调用结果可知,如果派生类的成员变量发生覆盖,派生类仅仅是将基类的覆盖的成员变量隐藏了

下面展开基类与派生类具有相同形式成员函数(不是虚函数)的情况


2.1 代码

// 基类
class baseA
{
public:
    void show() { std::cout << "baseA::show() \n\n"; }
    void run() { std::cout << "baseA::run() \n\n"; }
    int _ma = 1;
    int _mb = 2;
};

// 派生类
class deriveA : public baseA
{
public:
    // 与基类中的声明一致,仅仅是函数体不同而已。
    void run()  { std::cout << "deriveA::run() \n\n"; }
    int _derive_c = 3;
    int _mb = 4;
};

deriveA da;
da.run();
  • 代码中,派生类和基类存在同形式函数run. 此时发生了覆盖。派生类覆盖了基类的run函数。

2.2 内存分布

1>class deriveA    size(16):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | _ma
1> 4    | | _mb
1>    | +---
1> 8    | _derive_c
1>12    | _mb
1>    +---
  • 因为没有虚函数,故不会出现虚函数表指针和虚函数表。

2.3 sizeof

2.4 分析

监视结果

  • 内存分布: 尽管存在同形式的成员函数(非虚函数),但是,内存布局不会变化,因为这些函数在编译期间已经确定了其地址。并且,不是虚函数,是不会添加到虚函数表中的,当然,也不会有虚函数表指针。
  • 代码da.run();执行的是派生类中run函数体的结果,而非基类的run函数。类比上一个章节中的同名成员变量,派生类隐藏了基类的同名同形式函数run,按照就近原则,发现派生类存在run,则调用派生类的run函数,非基类的run函数。

单一继承中,覆盖总结

派生类覆盖类型

派生类调用结果

成员变量

隐藏基类成员变量,按照就近原则调用派生类的成员变量

成员函数

隐藏基类成员函数,按照就近原则调用派生类的成员函数



下面展开相同形式相同名字的虚函数覆盖情况


3.1 派生类的某个虚函数与基类的某个虚函数同形式,也带有virtual关键字

此时,发生了成员函数的覆盖,预测下派生类的内存分布情况和虚函数表?

3.1.1 代码

下面的代码展示了,基类和派生类共有一个同形式的函数run,仅函数体不同而已。

// 基类
class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
    virtual void run() { std::cout << "virtual baseA::run() \n\n"; }
    int _ma = 1;
    int _mb = 2;
};

// 派生类
class deriveA : public baseA
{
public:
    // 与基类中的声明一致,仅仅是函数体不同而已。
    virtual void run()  { std::cout << "virtual deriveA::run() \n\n"; }
    int _derive_c = 3;
    int _mb = 4;
};

3.1.2 内存布局

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _mb
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::run
  • 内分布

    • 你肯定也注意到了,deriveA的内存布局依然和上面章节总结的规律是一样的。先是虚函数表指针,再是基类成员变量,最后是派生类成员变量。
  • 虚函数表

    • 虚函数表中存放的是派生类的run函数,非基类中的run函数
  • 虚函数表指针

    • 尽管发生了覆盖,但是,虚函数表指针仍然只有一个

3.1.3 sizeof

3.1.4 run函数输出

下面的代码创建了一个派生类对象,并执行run函数。

deriveA da;
da.run();// 输出:virtual deriveA::run()

查看输出

3.1.5 分析

  • 内存布局: 与之前分析的规律是一致的。因为存在虚函数,所以,排在最前的是虚函数表指针,然后是基类的成员变量,最后是派生类的成员变量。
  • 虚函数表:
    • 当基类的虚函数全部放入虚函数表后,发现派生类中出现了与基类同形式同名的虚函数,可视为基类run函数的重写,于是将虚函数表中与基类同名同形式的虚函数替换为派生类中的虚函数。
    • 可以反过来考虑,如果不替换,虚函数表中就出现了2个run函数,当执行run函数时,虚函数表指针应该指向谁?二义性。矛盾!
    • 当执行da.run();,发现虚表中的run函数为派生类的run函数,则输出派生类run函数的结果,不是基类的run函数结果。多态出现了
  • 虚函数表指针
    • 发生了成员函数覆盖的情况,虚函数表指针仍然只有一个。

3.2 派生类中不是虚函数呢?

派生类中实现了基类中的某个虚函数。此时,派生类的内存分布和虚函数表是?

下面的代码中,派生类实现了基类的虚函数run。

3.2.1 代码

// 基类
class baseA
{
public:
    virtual void show() { std::cout << "virtual baseA::show() \n\n"; }
    virtual void run() { std::cout << "virtual baseA::run() \n\n"; }
    int _ma = 1;
    int _mb = 2;
};

// 派生类
class deriveA : public baseA
{
public:
    void run()  { std::cout << "deriveA::run() \n\n"; }

    int _derive_c = 3;
    int _mb = 4;
};

3.2.2 内存分布

1>class deriveA    size(20):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | _derive_c
1>16    | _mb
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::run

3.2.3 分析

  • 虚函数表

    • 可见,这与上面一种情况,完全一样,因为派生类覆盖了基类的run函数,就需要将虚函数表中基类的run函数地址替换为派生类中run函数的地址。
  • 虚函数表指针

    • 虚函数表指针的个数依然维持了一个。

3.2.4 监视结果

注意图中高亮的这一行,这一行已经替换为派生类的run函数,而非基类的run函数