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

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

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

2.覆盖继承

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

2.1 成员变量覆盖

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

2.2 成员函数覆盖

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

存在虚函数的覆盖继承

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

不存在虚函数的覆盖继承

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

  • 演示环境: VS2017 + 32位程序
  • 多继承(本文主要展开)
  • 代码写的不够规范: 因为多态中,任何带虚函数的基类类的析构函数都应该是虚析构函数。但是我这里没有写出来,目的是缩短文章篇幅。

序号

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

1

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

2

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

3

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

1.1 基类均无虚函数,派生类有虚函数

1.1.1 代码

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

class baseB
{
public:
    int _mc = 3;
    int _md = 4;
};

// 派生类
class deriveA : public baseA, public baseB
{
public:
    virtual void print() { std::cout << "deriveA::print()\n\n\n"; }
    int _me = 3;
    int _mf = 4;
};

1.1.2 内存分布

1>class deriveA    size(28):
1>    +---
1> 0    | {vfptr}
1> 4    | +--- (base class baseA)
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | _mc
1>16    | | _md
1>    | +---
1>20    | _me
1>24    | _mf
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &deriveA::print
  • 因为派生类存在虚函数,故排在最前的是虚函数表指针(此时,虚函数表指针属于派生类,而非基类),接着在世基类成员变量,这里先是基类baseA,然后才是基类baseB,最后才是派生类成员变量。基类成员变量存储顺序与声明顺序有关(从靠近派生类的基类开始,先基类baseA,然后是基类baseB)。这个顺序与之前总结的规律一致: 先基类再派生类,先声明,先存储。
  • 虚函数表。由于只有派生类存在虚函数,故虚函数表中只有派生类的虚函数地址。

1.1.3 sizeof

28字节 = 基类和派生类总共六个int成员变量 + 虚函数表指针4字节

1.1.4 监视结果

1.2 基类无虚函数,派生类也没有虚函数

1.2.1 代码

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

class baseB
{
public:
    int _mc = 3;
    int _md = 4;
};

// 派生类
class deriveA : public baseA, public baseB
{
public:
    int _me = 3;
    int _mf = 4;
};

1.2.2 内存布局

1>class deriveA    size(24):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | _ma
1> 4    | | _mb
1>    | +---
1> 8    | +--- (base class baseB)
1> 8    | | _mc
1>12    | | _md
1>    | +---
1>16    | _me
1>20    | _mf
1>    +---
  • 虚函数表指针。因为不存在虚函数,故没有虚函数表指针表和虚函数表。
  • 同样的,靠近派生类的是baseA,然后才是B,故按照先声明先存储的顺序,先是基类baseA的成员变量,然后是基类baseB的成员变量,最后才是派生类的成员变量。

1.2.3 sizeof

24字节= 基类总4个int成员变量 + 派生类的2个int成员变量 = 4 * 4 + 4 * 2 = 24

1.2.4 监视结果

2.1 两个基类中只有一个类有虚函数,派生类有虚函数

2.1.1 代码

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

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

class baseB
{
public:
    int _mc = 3;
    int _md = 4;
};

// 派生类
class deriveA : public baseA, public baseB
{
public:
    virtual void print() { std::cout << "virtual deriveA::print() \n\n"; }
    int _me = 3;
    int _mf = 4;
};

2.1.2 内存布局

1>class deriveA    size(28):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | _mc
1>16    | | _md
1>    | +---
1>20    | _me
1>24    | _mf
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::print
  • 虚函数表指针。

    • 由于基类和派生类都存在虚函数,故存在虚函数表指针。
    • 内存布局顺序:先基类,再派生,先声明先存储。代码中,基类baseA存在虚函数且基类baseA先声明,所以,{vfptr}的位置放在了基类baseA中,且排在第一位,然后才是基类baseA的成员变量,接着是基类baseB的成员变量,最后才是派生类的成员变量。
  • 虚函数表。

    • 存储虚函数的虚函数表。本例中,虚函数只有基类baseA和派生类才有,故,按照先声明,先存储的顺序。依次为:&baseA::show,&deriveA::print。

2.1.3 sizeof

28字节 = 基类总共4个int成员变量 + 派生类的2个int成员变量 + 一个虚函数表指针4字节

2.1.4 监视结果

2.1.5 交换baseA和baseB的顺序呢?

  • 代码

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

    int _ma = 1;
    int _mb = 2;

    };

    class baseB
    {
    public:
    int _mc = 3;
    int _md = 4;
    };

    // 派生类
    class deriveA : public baseB, public baseA
    {
    public:
    virtual void print() { std::cout << "virtual deriveA::print() \n\n"; }
    int _me = 3;
    int _mf = 4;
    };

  • 内存分布

    1>class deriveA size(28):
    1> +---
    1> 0 | +--- (base class baseA)
    1> 0 | | {vfptr}
    1> 4 | | _ma
    1> 8 | | _mb
    1> | +---
    1>12 | +--- (base class baseB)
    1>12 | | _mc
    1>16 | | _md
    1> | +---
    1>20 | _me
    1>24 | _mf
    1> +---
    1>
    1>deriveA::$vftable@:
    1> | &deriveA_meta
    1> | 0
    1> 0 | &baseA::show
    1> 1 | &deriveA::print
    1>
    1>deriveA::print this adjustor: 0

  • 你肯定也发现了。这与2.1.2内存布局的情况是一摸一样的。

  • 存储顺序:先基类,再派生,先声明,先存储。由于存在虚函数,所以,这个需要优先考虑。

  • 尽管baseB更靠近派生类,但是baseA的优先级更高,因为基类baseA存在虚函数而基类baseB不存在。

  • 虚表依然仅按照先声明像存储的顺序存储虚函数。基类优先级 > 派生类优先级。

  • 2.1.5.3 sizeof

    2.1.3 sizeof相同。因为没有成员变量增加和减少。

2.2 两个基类中只有一个类有虚函数,派生类没有虚函数

2.2.1 代码

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

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

class baseB
{
public:
    int _mc = 3;
    int _md = 4;
};

// 派生类
class deriveA : public baseA, public baseB
{
public:
    int _me = 3;
    int _mf = 4;
};

2.2.1 内存分布

1>class deriveA    size(28):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | _mc
1>16    | | _md
1>    | +---
1>20    | _me
1>24    | _mf
1>    +---
1>
1>deriveA::$vftable@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
  • 虚函数表指针。 因为只有基类baseA存在虚函数,故需要第一个考虑虚函数表指针。 且基类baseA先声明。
  • 虚函数表,因为只有基类baseA存在虚函数,故虚函数表只保存了基类的虚函数地址。
  • 存储顺序: 先基类,再派生,先声明像存储。所以;最先是baseA,然后是baseB,最后才是派生类。

2.2.2 交换基类顺序呢?

  • 代码

    class deriveA : public baseB, public baseA
    {
    …..
    }

  • 内存分布

    1>
    1>class deriveA size(28):
    1> +---
    1> 0 | +--- (base class baseA)
    1> 0 | | {vfptr}
    1> 4 | | _ma
    1> 8 | | _mb
    1> | +---
    1>12 | +--- (base class baseB)
    1>12 | | _mc
    1>16 | | _md
    1> | +---
    1>20 | _me
    1>24 | _mf
    1> +---
    1>
    1>deriveA::$vftable@:
    1> | &deriveA_meta
    1> | 0
    1> 0 | &baseA::show

  • 你肯定又发现了,这与上面的内存分布情况是一样的。

  • 为什么?因为基类有两个,且只有其中一个存在虚函数,所以,按照先声明存储的规则,且考虑到有虚函数的基类的优先级大于没有虚函数的基类。故,含有虚函数的类成员变量排在内存分布图的虚函数表指针的后面,而且是紧挨着。

2.2.3 sizeof

2.2.4 监视结果

3.1 两个基类中都含有虚函数,派生类有虚函数

3.1.1 代码

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

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

class baseB
{
public:
    virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
    int _mc = 3;
    int _md = 4;
};

// 派生类
class deriveA : public baseA, public baseB
{
public:
    virtual void print() { std::cout << "deriveA::print()\n\n"; }
    int _me = 3;
    int _mf = 4;
};

3.1.2 内存分布

1>class deriveA    size(32):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | {vfptr}
1>16    | | _mc
1>20    | | _md
1>    | +---
1>24    | _me
1>28    | _mf
1>    +---
1>
1>deriveA::$vftable@baseA@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1> 1    | &deriveA::print
1>
1>deriveA::$vftable@baseB@:
1>    | -12
1> 0    | &baseB::play
1>
1>deriveA::print this adjustor: 0
  • 等等,你是不是也发现了,内存模型中出现了2个{vfptr}。一个属于基类baseA,另一个属于基类baseB。

  • 虚函数表。 发现了吧:1.派生类的虚函数表多了一张表,2.派生类的虚函数是放在第一张虚函数表中。

  • 按照先前的顺序:先基类,再派生,先声明,先存储。但是有虚函数的类要优先考虑。这里基类baseA和baseB还有派生类都含有虚函数。那么先看基类,按照先声明先存储的顺序,baseA基类相对baseB基类先声明,故基类baseA的虚函数表指针首先被存储,接着再是基类baseA的成员变量,然后是基类baseB的虚函数表指针,基类baseB的成员变量。最后是派生类。

  • 为什么派生类的虚函数是追加在第一张虚表的后面? 请看下面的一段汇编(没学过汇编,不献丑)结论: 派生类的虚函数是追加在第一张虚表的后面。当需要使用派生类的虚函数是,用第一张表的虚函数表指针指向派生类的虚函数即可。(个人观点)下面的汇编也应该是这样:1,找到虚函数表的起始地址,2.找到派生类的虚函数偏移,3.使用虚函数表指针指向派生类的虚函数。

    deriveA *pda = &da;

    00A7A02E lea eax,[ebp-28h]
    00A7A031 mov dword ptr [ebp-34h],eax
    pda->print();
    00A7A034 mov eax,dword ptr [ebp-34h]
    00A7A037 mov edx,dword ptr [eax]
    00A7A039 mov esi,esp
    00A7A03B mov ecx,dword ptr [ebp-34h]
    00A7A03E mov eax,dword ptr [edx+4]
    00A7A041 call eax
    00A7A043 cmp esi,esp
    00A7A045 call 00A714C4

3.1.3 sizeof

32字节 = 基类总共4个int成员变量 + 派生类的2个int变量 + 2个虚函数表指针 = 4 * 4 + 2 * 4 + 2 * 4 = 32

3.1.4 监视结果

监视结果中看不到派生类的虚函数存储情况。

3.2 两个基类中都含有虚函数,派生类没有虚函数

3.2.1 代码

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

// 派生类
class deriveA : public baseA , public baseB
{
public:
    int _me = 3;
    int _mf = 4;
};

3.2.2 内存布局

1>class deriveA    size(32):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | {vfptr}
1>16    | | _mc
1>20    | | _md
1>    | +---
1>24    | _me
1>28    | _mf
1>    +---
1>
1>deriveA::$vftable@baseA@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1>
1>deriveA::$vftable@baseB@:
1>    | -12
1> 0    | &baseB::play
  • 虚函数表指针。基类baseA先于基类baseB声明,两个基类同时含有虚函数,但是先声明,先存储。故先存储基类baseA的虚函数表指针。
  • 虚函数表。 多继承,故每张表各自存储自己的虚函数表的信息。

3.2.3 sizeof

3.2.4 监视结果

4.1 3个基类都有虚函数, 派生类没有虚函数

4.1.1 代码

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

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

class baseB
{
public:
    virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
    int _mc = 3;
    int _md = 4;
};

class baseC
{
public:
    virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
    int _mh = 7;
    int _mi = 8;
};

// 派生类
class deriveA : public baseA, public baseB, public baseC
{
public:
    int _me = 3;
    int _mf = 4;
};

4.1.2 内存布局

1>class deriveA    size(44):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | {vfptr}
1>16    | | _mc
1>20    | | _md
1>    | +---
1>24    | +--- (base class baseC)
1>24    | | {vfptr}
1>28    | | _mh
1>32    | | _mi
1>    | +---
1>36    | _me
1>40    | _mf
1>    +---
1>
1>deriveA::$vftable@baseA@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseA::show
1>
1>deriveA::$vftable@baseB@:
1>    | -12
1> 0    | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1>    | -24
1> 0    | &baseC::listening

4.2 三个基类中每个基类都有虚函数,派生类也有虚函数

4.2.1 代码

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

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

class baseB
{
public:
    virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
    int _mc = 3;
    int _md = 4;
};

class baseC
{
public:
    virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
    int _mh = 7;
    int _mi = 8;
};

// 派生类
class deriveA : public baseA, public baseB, public baseC
{
public:
    virtual void show() { std::cout << "virtual deriveA::show() \n\n"; }
    int _me = 3;
    int _mf = 4;
};

4.2.2 内存布局

1>class deriveA    size(44):
1>    +---
1> 0    | +--- (base class baseA)
1> 0    | | {vfptr}
1> 4    | | _ma
1> 8    | | _mb
1>    | +---
1>12    | +--- (base class baseB)
1>12    | | {vfptr}
1>16    | | _mc
1>20    | | _md
1>    | +---
1>24    | +--- (base class baseC)
1>24    | | {vfptr}
1>28    | | _mh
1>32    | | _mi
1>    | +---
1>36    | _me
1>40    | _mf
1>    +---
1>
1>deriveA::$vftable@baseA@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &deriveA::show
1>
1>deriveA::$vftable@baseB@:
1>    | -12
1> 0    | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1>    | -24
1> 0    | &baseC::listening
1>
1>deriveA::show this adjustor: 0
  • Note: 派生类的虚函数是放在第一张虚函数表中的。

4.3 三个基类中其中2个基类都有虚函数,另一个基类没有虚函数,派生类没有虚函数

4.3.1 代码

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

class baseB
{
public:
    virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
    int _mc = 3;
    int _md = 4;
};

class baseC
{
public:
    virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
    int _mh = 7;
    int _mi = 8;
};

// 派生类
class deriveA : public baseA, public baseB, public baseC
{
public:
    int _me = 3;
    int _mf = 4;
};

4.3.2 内存布局

1>class deriveA    size(40):
1>    +---
1> 0    | +--- (base class baseB)
1> 0    | | {vfptr}
1> 4    | | _mc
1> 8    | | _md
1>    | +---
1>12    | +--- (base class baseC)
1>12    | | {vfptr}
1>16    | | _mh
1>20    | | _mi
1>    | +---
1>24    | +--- (base class baseA)
1>24    | | _ma
1>28    | | _mb
1>    | +---
1>32    | _me
1>36    | _mf
1>    +---
1>
1>deriveA::$vftable@baseB@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseB::play
1>
1>deriveA::$vftable@baseC@:
1>    | -12
1> 0    | &baseC::listening

4.4 三个基类中其中2个基类都有虚函数,另一个基类没有虚函数,派生类有虚函数

4.4.1 代码

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

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

class baseB
{
public:
    virtual void play() { std::cout << "virtual baseB::play() \n\n"; }
    int _mc = 3;
    int _md = 4;
};

class baseC
{
public:
    virtual void listening() { std::cout << "virtual baseC::listening() \n\n"; }
    int _mh = 7;
    int _mi = 8;
};

// 派生类
class deriveA : public baseA, public baseB, public baseC
{
public:
    virtual void show() { std::cout << "virtual deriveA::show() \n\n"; }
    int _me = 3;
    int _mf = 4;
};

4.4.2 内存分布

1>
1>class deriveA    size(40):
1>    +---
1> 0    | +--- (base class baseB)
1> 0    | | {vfptr}
1> 4    | | _mc
1> 8    | | _md
1>    | +---
1>12    | +--- (base class baseC)
1>12    | | {vfptr}
1>16    | | _mh
1>20    | | _mi
1>    | +---
1>24    | +--- (base class baseA)
1>24    | | _ma
1>28    | | _mb
1>    | +---
1>32    | _me
1>36    | _mf
1>    +---
1>
1>deriveA::$vftable@baseB@:
1>    | &deriveA_meta
1>    |  0
1> 0    | &baseB::play
1> 1    | &deriveA::show
1>
1>deriveA::$vftable@baseC@:
1>    | -12
1> 0    | &baseC::listening
1>
1>deriveA::show this adjustor: 0
  • Note: 派生类的虚函数存放在第一张虚函数表中。