c++内存分布之虚析构函数

时间:2023-03-09 14:58:50
c++内存分布之虚析构函数

关于

  • 本文代码演示环境: VS2017+32程序
  • 虚析构函数是一种特殊的虚函数,可以知道,虚函数影响的内存分布规律应该也适用虚析构函数。看看实际结果。
  • Note,一个类中,虚析构函数只能有一个。
  • 本文将展开 单一继承和多继承两种情况

结论

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

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

2.覆盖继承

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

2.1 成员变量覆盖

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

2.2 成员函数覆盖

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

存在虚函数的覆盖继承

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

不存在虚函数的覆盖继承

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

1.类存在虚析析构函数

1.1 不含成员函数是虚函数

1.1.1 代码

class baseD
{
public:
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; }
int _mz = 100;
};

1.1.2 内存分布

1>class baseD	size(8):
1> +---
1> 0 | {vfptr}
1> 4 | _mz
1> +---
1>
1>baseD::$vftable@:
1> | &baseD_meta
1> | 0
1> 0 | &baseD::{dtor}
  • 虚函数表指针
    • 因为存在虚析构函数,所以排在最前的是虚函数表指针
  • 虚函数表
    • 虚函数表存放的是析构函数地址
  • 这与基类只有一个虚函数的内存分布情况是一致的。

1.2 含成员函数是虚函数

1.2.1 代码

class baseD
{
public:
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } virtual void turning() { std::cout << "virtual baseD::turning()\n\n"; }
int _mz = 100;
};

1.2.2 内存分布

1>class baseD	size(8):
1> +---
1> 0 | {vfptr}
1> 4 | _mz
1> +---
1>
1>baseD::$vftable@:
1> | &baseD_meta
1> | 0
1> 0 | &baseD::{dtor}
1> 1 | &baseD::turning
  • 虚函数表指针
    • 虚函数表指针的个数依然只有一个,不会因为是虚析构函数而增加
  • 虚函数表
    • 存放虚函数地址,按照声明的顺序。
  • 可能你会说,虚析构函数区别于成员函数,它与成员函数的声明顺序是否会影响它在虚函数表中的顺序?接着往下看。

1.2.3 交换虚析构函数与虚函数的声明顺序


class baseD
{
public:
virtual void turning() { std::cout << "virtual baseD::turning()\n\n"; }
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; }
int _mz = 100;
};

1.2.4 交换虚析构函数与虚函数的声明顺序后,内存分布

1>	+---
1> 0 | {vfptr}
1> 4 | _mz
1> +---
1>
1>baseD::$vftable@:
1> | &baseD_meta
1> | 0
1> 0 | &baseD::turning
1> 1 | &baseD::{dtor}

你肯定看到了,虚析构函数与虚成员函数的声明顺序将决定他们在虚函数表中的顺序,而不会因为是虚析构函数就放在最前面。


单一继承结果

2. 单一继承

2.1 基类的析构函数是虚析构函数

观察派生类的内存分布情况

2.1.1 代码

class baseD
{
public:
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; }
int _mz = 100;
}; // 派生类
class deriveA : public baseD
{
public:
int _me = 3;
int _mf = 4;
};

2.1.2 内存分布

1>class deriveA	size(16):
1> +---
1> 0 | +--- (base class baseD)
1> 0 | | {vfptr}
1> 4 | | _mz
1> | +---
1> 8 | _me
1>12 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
1>
1>deriveA::{dtor} this adjustor: 0
1>deriveA::__delDtor this adjustor: 0
1>deriveA::__vecDelDtor this adjustor: 0
  • 虚函数表指针
    • 排在最前的是虚函数表指针,因为基类存在虚析构函数。然后是基类的成员变量,最后是派生类的成员变量。
    • 虚函数表指针,基类A可以用,派生类B也可以使用(继承的结果)
  • 虚函数表
    • 注意,这里存放的是派生类的析构函数。
    • 派生类中,没有明确写出类的析构函数,使用的是编译器自动为其生成的默认析构函数。
    • 为什么存放的是派生类的析构函数地址?
      • 因为基类的析构函数加上virtual关键字,当用基类指针保存派生类的对象new后的对象,对象析构时,代码先调用的是派生类的析构函数,再调用基类的析构函数,这样能保证申请自*存储区中的内存能正确析构。所以,将派生类的析构函数地址保存下来就是用在这里,析构的时候知道派生类的析构函数地址。如果没有,则无法析构派生类申请自*存储区的内存。
      • 基类指针对象析构时,发现自身的类型是基类指针,不知道自己指向的类型是怎么样的。所以,当发生析构时,需要指明指针指向的内存的析构函数。否则,析构时,仅释放基类申请自*存储区的内存,派生类申请自*存储区的内存无法正确释放。

Note: 明白了基类的析构函数定义为虚析构函数的好处。

2.2 基类和派生类的析构函数都是虚析构函数

观察派生类的内存分布情况

2.2.1 代码

基类和派生类的析构函数都是虚析构函数,

class baseD
{
public:
//virtual void turning() { std::cout << "virtual baseD::turning()\n\n"; }
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } int _mz = 100;
}; // 派生类
class deriveA : public baseD
{
public:
virtual ~deriveA(){}
int _me = 3;
int _mf = 4;
};

2.2.2 内存分布

1>class deriveA	size(16):
1> +---
1> 0 | +--- (base class baseD)
1> 0 | | {vfptr}
1> 4 | | _mz
1> | +---
1> 8 | _me
1>12 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
  • 内存分布情况与2.1中的情况是一致的。

2.3 基类不是虚析构函数而派生类是虚析构函数的情况

观察派生类的内存分布情况

2.3.1 代码

class baseD
{
public:
//virtual void turning() { std::cout << "virtual baseD::turning()\n\n"; }
~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } int _mz = 100;
}; // 派生类
class deriveA : public baseD
{
public:
virtual ~deriveA(){}
int _me = 3;
int _mf = 4;
};

2.3.2 内存分布

1>class deriveA	size(16):
1> +---
1> 0 | {vfptr}
1> 4 | +--- (base class baseD)
1> 4 | | _mz
1> | +---
1> 8 | _me
1>12 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
  • 注意,对比上面2.22.1中内存分布, 此时,{vfptr} 位置虽然也在最前面,但是,没有放在基类baseD的下面了。 也就是说,基类不具备操作该虚函数表指针的特性。
  • 这里也能很清楚的明白,基类baseD的内存分布,没有虚函数表指针。
  • 若此时用基类指针保存一个申请自*存储区的派生类对象,发生析构时,就会出现异常。
HEAP[xxx.exe]:Invalid Address specified to RtlValidateHeap

那么,析构时,用一个基类指针指向一个不属于基类指针的内容时会发什么呢? 答案:异常。

一定要注意这样的情况,避免异常发生。

2.4 基类和派生类都不含有虚析构函数

  • 既然不存在虚析构函数,不是这里需要探讨的范围。
  • 有了上面的分析,这种情况下的内存分布只会有基类和派生类的成员变量,且不存在虚函数表指针和虚函数表。

3. 下面开始探讨多继承的情况

4. 基类是虚析构函数。

4.1 派生类的析构函数不是虚析构函数

基类有2个,每个基类的析构函数都是虚析构函数。

4.1.1 代码

class baseD
{
public:
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; }
int _mz = 100;
}; class baseE
{
public:
virtual ~baseE() { std::cout << "virtual baseE::~baseE()\n\n"; }
int _my = 99;
};
// 派生类
class deriveA : public baseD, public baseE
{
public:
int _me = 3;
int _mf = 4;
};

4.1.2 内存模型

1>class deriveA	size(24):
1> +---
1> 0 | +--- (base class baseD)
1> 0 | | {vfptr}
1> 4 | | _mz
1> | +---
1> 8 | +--- (base class baseE)
1> 8 | | {vfptr}
1>12 | | _my
1> | +---
1>16 | _me
1>20 | _mf
1> +---
1>
1>deriveA::$vftable@baseD@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
1>
1>deriveA::$vftable@baseE@:
1> | -8
1> 0 | &thunk: this-=8; goto deriveA::{dtor}
  • 虚函数表指针
      1. 因为派生类有2个基类,且每个基类均存在虚析构函数,所以,首先是含有虚析构函数的基类先存储。 由于baseD比baseE基类先声明,所以先存储的是baseD的虚函数表指针、成员变量,再是基类baseE的虚函数表指针、成员变量。
      1. 然后才是派生类的成员变量。
  • 虚函数表
      1. deriveA::$vftable@baseD@ 内容很容易理解, 与上面总结的是一致的。
      1. deriveA::$vftable@baseE@ 怎么理解呢?虽然不能全明白这是什么意思,但是单词表面传达的意思是: A.baseE的析构函数地址的偏移(&thunk: this-=8;); B.派生类的析构函数的地址(goto deriveA::{dtor})。 一句话: 存放的是派生类A的析构函数地址。如果不能理解,请对比理解虚函数表【deriveA::$vftable@baseD@】。

4.2 派生类的析构函数是虚析构函数

派生类的析构函数和基类的析构函数都是虚析构函数,查看派生类的内存分布

4.2.1 代码

class baseD
{
public:
virtual ~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } int _mz = 100;
}; class baseE
{
public:
virtual ~baseE() { std::cout << "virtual baseE::~baseE()\n\n"; } int _my = 99;
};
// 派生类
class deriveA : public baseD, public baseE
{
public:
virtual ~deriveA() { std::cout << "virtual ~deriveA::deriveA()\n\n"; }
int _me = 3;
int _mf = 4;
};

4.2.2 内存分布

1>class deriveA	size(24):
1> +---
1> 0 | +--- (base class baseD)
1> 0 | | {vfptr}
1> 4 | | _mz
1> | +---
1> 8 | +--- (base class baseE)
1> 8 | | {vfptr}
1>12 | | _my
1> | +---
1>16 | _me
1>20 | _mf
1> +---
1>
1>deriveA::$vftable@baseD@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
1>
1>deriveA::$vftable@baseE@:
1> | -8
1> 0 | &thunk: this-=8; goto deriveA::{dtor}
1>
1>deriveA::{dtor} this adjustor: 0
1>deriveA::__delDtor this adjustor: 0
1>deriveA::__vecDelDtor this adjustor: 0
  • 可以看出,和上面4.1中的内存分布是一致的。派生类加上virtual是为了告诉编译器,派生类也可以继续派生,且可以用其指向一个继承自自己的派生类的对象,可正确析构派生类的*存储区申请的数据。

4.3 基类中只有一个类是虚析构函数呢, 派生类不是虚析构函数

派生类的析构函数不是虚析构函数。

4.3.1 代码

基类中,baseD的析构函数不是虚析构函数,而baseE的析构函数是虚析构函数。

class baseD
{
public:
~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } int _mz = 100;
}; class baseE
{
public:
virtual ~baseE() { std::cout << "virtual baseE::~baseE()\n\n"; } int _my = 99;
}; // 派生类
class deriveA : public baseD, public baseE
{
public:
int _me = 3;
int _mf = 4;
};

4.3.2 内存分布

1>class deriveA	size(20):
1> +---
1> 0 | +--- (base class baseE)
1> 0 | | {vfptr}
1> 4 | | _my
1> | +---
1> 8 | +--- (base class baseD)
1> 8 | | _mz
1> | +---
1>12 | _me
1>16 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
1>
1>deriveA::{dtor} this adjustor: 0
1>deriveA::__delDtor this adjustor: 0
1>deriveA::__vecDelDtor this adjustor: 0
  • 这与多继承中基类有只有一个含有虚函数的情况是一致的。这里就不赘诉了。
  • 简而言之:谁有虚函数谁就靠前;基类的优先级大于 派生类的优先级。

4.4 基类中只有一个类是虚析构函数呢, 派生类是虚析构函数

4.4.1 代码

class baseD
{
public:
~baseD() { std::cout << "virtual baseD::~baseD()\n\n"; } int _mz = 100;
}; class baseE
{
public:
virtual ~baseE() { std::cout << "virtual baseE::~baseE()\n\n"; } int _my = 99;
}; // 派生类
class deriveA : public baseD, public baseE
{
public:
virtual ~deriveA() { std::cout << "virtual ~deriveA::deriveA()\n\n"; }
int _me = 3;
int _mf = 4;
};

4.4.2 内存分布

1>class deriveA	size(20):
1> +---
1> 0 | +--- (base class baseE)
1> 0 | | {vfptr}
1> 4 | | _my
1> | +---
1> 8 | +--- (base class baseD)
1> 8 | | _mz
1> | +---
1>12 | _me
1>16 | _mf
1> +---
1>
1>deriveA::$vftable@:
1> | &deriveA_meta
1> | 0
1> 0 | &deriveA::{dtor}
1>
1>deriveA::{dtor} this adjustor: 0
1>deriveA::__delDtor this adjustor: 0
1>deriveA::__vecDelDtor this adjustor: 0
  • 还是再罗索一点。 先是基类存在虚函数,故 基类baseE排在内存的最前面,然后是基类baseD的成员变量,因为其优先级 大于 派生类,最后才是派生类的成员变量。
  • 与前面总结的规律完全一致。