关于

  • 本文代码演示环境: 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的成员变量,因为其优先级 大于 派生类,最后才是派生类的成员变量。
  • 与前面总结的规律完全一致。

c++内存分布之虚析构函数的更多相关文章

  1. C++浅析——继承类内存分布和虚析构函数

    继承类研究 1. Code 1.1 Cbase, CTEST为基类,CTest2为其继承类,并重新申明了基类中的同名变量 class CBase { public: int Data; CBase() ...

  2. c++内存分布之虚函数(多继承)

    系列 c++内存分布之虚函数(单一继承) c++内存分布之虚函数(多继承) [本文] 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  3. c++内存分布之虚函数(单一继承)

    系列 c++内存分布之虚函数(单一继承) [本文] c++内存分布之虚函数(多继承) 结论 1.虚函数表指针 和 虚函数表 1.1 影响虚函数表指针个数的因素只和派生类的父类个数有关.多一个父类,派生 ...

  4. c/c++: c++继承 内存分布 虚表 虚指针 (转)

    http://www.cnblogs.com/DylanWind/archive/2009/01/12/1373919.html 前部分原创,转载请注明出处,谢谢! class Base  {  pu ...

  5. C++对象的内存分布和虚函数表

    c++中一个类中无非有四种成员:静态数据成员和非静态数据成员,静态函数和非静态函数. 1.非静态数据成员被放在每一个对象体内作为对象专有的数据成员.    2.静态数据成员被提取出来放在程序的静态数据 ...

  6. C++对象内存分布详解(包括字节对齐和虚函数表)

    转自:https://www.jb51.net/article/101122.htm 1.C++对象的内存分布和虚函数表: C++对象的内存分布和虚函数表注意,对象中保存的是虚函数表指针,而不是虚函数 ...

  7. c++内存分布之纯虚函数

    关于 本文演示环境:VS2017+32位程序. 纯虚函数是一种特殊的虚函数.可以预测到:虚函数的结论同样适用纯虚函数,但是纯虚函数是一种特殊的存在,还是看看实际结果. 代码写的不够规范: 因为任何带虚 ...

  8. C++ 继承之虚继承与普通继承的内存分布

    仅供互相学习,请勿喷,有观点欢迎指出~ class A { virtual void aa(){}; }; class B : public virtual A { ]; //加入一个变量是为了看清楚 ...

  9. c++ 虚析构函数[避免内存泄漏]

    c++  虚析构函数: 虚析构函数(1)虚析构函数即:定义声明析构函数前加virtual 修饰, 如果将基类的析构函数声明为虚析构函数时,由该基类所派生的所有派生类的析构函数也都自动成为虚析构函数. ...

随机推荐

  1. Java设计模式之(十四)——策略模式

    1.什么是策略模式? Define a family of algorithms, encapsulate each one, and make them interchangeable. Strat ...

  2. spring-boot spring-MVC自动配置

    Spring MVC auto-configuration Spring Boot 自动配置好了SpringMVC 以下是SpringBoot对SpringMVC的默认配置:==(WebMvcAuto ...

  3. SQL-关联表查询(连表查询)

    0.例如:select * from T1,T2     where T1.a=T2.a 1.连表查询 <=> join(inner join)内连接查询 数据源: Persion表:  ...

  4. EXCEL-对筛选出(单独手动隐藏行还是在统计范围内)的表格数据进行统计

    =SUBTOTAL(3,A1:A5)  #计算筛选出的表格中A1:A5中有几个值. =SUBTOTAL(3,I71:I21447)  ,在I71:I21447之间计数,会自动略去没有筛选上的隐藏单元格 ...

  5. jquery chosen onchange 值改变时触发方法

    jquery chosen onchange 值改变时触发方法如下:$(".chzn-select").chosen().on("change", functi ...

  6. 推荐一个latex简历模板的网站给大家

    http://www.rpi.edu/dept/arc/training/latex/resumes/ Using the LaTeX Resume Templates A group of resu ...

  7. 日常Java(测试 (二柱)修改版)2021/9/22

    题目: 一家软件公司程序员二柱的小孩上了小学二年级,老师让家长每天出30道四则运算题目给小学生做. 二柱一下打印出好多份不同的题目,让孩子做了.老师看了作业之后,对二柱赞许有加.别的老师闻讯, 问二柱 ...

  8. Erda 1.1 版本发布|3 大亮点特性最新解读

    来源|尔达 Erda 公众号 ​ Erda v1.1 Changelog: https://github.com/erda-project/erda/blob/master/CHANGELOG/CHA ...

  9. DBeaver客户端工具连接Hive

    目录 介绍 下载安装 相关配置 1.填写主机名 2.配置驱动 简单使用 主题设置 字体背景色 介绍 在hive命令行beeline中写一些很长的查询语句不是很方便,急需一个hive的客户端界面工具 D ...

  10. web必知,多终端适配

    导读 移动端适配,是我们在开发中经常会遇到的,这里面可能会遇到非常多的问题: 1px问题 UI图完美适配方案 iPhoneX适配方案 横屏适配 高清屏图片模糊问题 ... 上面这些问题可能我们在开发中 ...