关于

  • 本文代码演示环境: 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. python 字典 key 对应多个 value

    基本思路是,将key对应的value设置为list,将对应的值append进去. 示例: f=open("a1.txt") ha={} for i in f: i=i.strip( ...

  2. linux安全性增加

    账户安全问题 Linux  默认会安装很多不必要的用户和用户组,如果不需要某些用户或者组,就要立即删除它,因为账户越多,系统就越不安全,很可能被黑客利用,进而威胁到服务器的安全. Linux系统中可以 ...

  3. Excel—分组然后取每组中对应时间列值最大的或者最小的

    1.MAX(IF(A:A=D2,B:B)) 输入函数公式后,按Ctrl+Shift+Enter键使函数公式成为数组函数公式. Ctrl+Shift+Enter: 按住Ctrl键不放,继续按Shift键 ...

  4. SpringCloud微服务实战——搭建企业级开发框架(二十八):扩展MybatisPlus插件DataPermissionInterceptor实现数据权限控制

    一套完整的系统权限需要支持功能权限和数据权限,前面介绍了系统通过RBAC的权限模型来实现功能的权限控制,这里我们来介绍,通过扩展Mybatis-Plus的插件DataPermissionInterce ...

  5. c/c++在线编译Output Limit Exceeded(OLE)错误

    提示输出错误,有如下两个可能情况: 1. 不符合题目给出的输出格式,自己输出了多余的内容或者格式不正确 2. 输入数据的时候,未考虑到输入错误的情况 针对2,有如下的例子: 错误的情况: 1 int ...

  6. 静态库动态库的编译、链接, binutils工具集, 代码段\数据段\bss段解释

    #1. 如何使用静态库 制作静态库 (1)gcc *.c -c -I../include得到o文件 (2) ar rcs libMyTest.a *.o 将所有.o文件打包为静态库,r将文件插入静态库 ...

  7. 面试一定会问到的-js事件循环

    这篇文章讲讲浏览器的事件循环(nodejs中的事件循环稍有不同),事件循环是js的核心之一,因为js是单线程,所以异步事件实现就是依赖于事件循环机制,理解事件循环可让我们更清晰的处理js异步事件和应对 ...

  8. Vue框架,computed和watch的区别

    computed和watch定义 1.computed是计算属性,类似于过滤器,对绑定到视图的数据进行处理.官网的例子: <div id="example"> < ...

  9. Output of C++ Program | Set 18

    Predict the output of following C++ programs. Question 1 1 #include <iostream> 2 using namespa ...

  10. Git上项目代码拉到本地方法

    1.先在本地打开workspace文件夹,或者自定义的文件夹,用来保存项目代码的地方. 2.然后登陆GitHub账号,点击复制项目路径 3.在刚才文件夹下空白处点击鼠标右键,打开Git窗口 4.在以下 ...