首页 > 系统相关 >c++内存分布之虚析构函数

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

时间:2023-06-23 10:12:00浏览次数:46  
标签:之虚析构 baseD 函数 deriveA c++ +--- 内存 派生类 class

关于

  • 本文代码演示环境: 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.2和2.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的虚函数表指针、成员变量。
      2. 然后才是派生类的成员变量。
  • 虚函数表
      1. deriveA::$vftable@baseD@ 内容很容易理解, 与上面总结的是一致的。
      2. 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的成员变量,因为其优先级 大于 派生类,最后才是派生类的成员变量。
  • 与前面总结的规律完全一致。

出处:https://www.cnblogs.com/pandamohist/

 

 

标签:之虚析构,baseD,函数,deriveA,c++,+---,内存,派生类,class
From: https://www.cnblogs.com/im18620660608/p/17498763.html

相关文章

  • c++中虚析构函数如何实现多态的、内存布局如何?
    作者:冯Jungle链接:https://www.zhihu.com/question/36193367/answer/2242824055来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。之前Jungle写过一篇文章《探究C++:虚函数表究竟怎么回事?》,主要通过测试代码来验证虚函数表的存在,进而说明C++的多态机制......
  • c++ 64位输出地址超过int类型使用longlong类型输出
    #include<iostream>usingnamespacestd;intmain(){intarr[10]={1,2,3,4,5,6,7,8,9,20};cout<<"整个数组所占内存空间为"<<sizeof(arr)<<endl;cout<<"每个元素所占内存空间为"<<sizeof(arr[0])<<endl;cout<<"......
  • C++面试八股文:override和finial关键字有什么作用?
    某日二师兄参加XXX科技公司的C++工程师开发岗位第22面:(二师兄好苦逼,节假日还在面试。。。)面试官:C++的继承了解吗?二师兄:(不好意思,你面到我的强项了。。)了解一些。面试官:什么是虚函数,为什么需要虚函数?二师兄:虚函数允许在基类中定义一个函数,然后在派生类中进行重写(override)。二......
  • C++面试八股文:什么是左值,什么是右值?
    C++面试八股文:什么是左值,什么是右值?某日二师兄参加XXX科技公司的C++工程师开发岗位第16面:面试官:什么是左值,什么是右值?二师兄:简单来说,左值就是可以使用&符号取地址的值,而右值一般不可以使用&符号取地址。inta=42; //a是左值,可以&aint*p=&a;int*p=&42; //42是右......
  • Redis–内存淘汰机制(涉及到过期策略)
    这个博客的内容包括以下几个点:1.redis内存淘汰机制2.若有大量的key需要设置同一时间过期,一般需要注意什么?3.过期键删除策略4.redis如何保证数据都是热点数据一、redis内存淘汰机制1,概念:内存淘汰机制:redis配置文件可以设置maxmemory,内存的最大使用量,达到限度会执行内存淘汰......
  • UE5 C++ TSet
    概念TSet是一种快速容器类,用于在排序不重要的情况下存储唯一元素TSet也是值类型,支持常规复制、赋值和析构函数操作,以及其元素较强的所有权TSet被销毁时,其元素也将被销毁。键类型也必须是值类型创建TSet<FString>FruitSet;添加元素Add将提供键加入setFruitSet.......
  • 带宽翻倍更能超!影驰HOF Classic D5-7000内存评测:超至7800MHz仍有余力
    一、前言:影驰带来DDR5-7000内存C32时序、1.45V规格亮眼DDR5内存诞生初期被大众吐槽时序高、价格高,甚至表现不如高频DDR4内存,极大地阻碍了其普及,所以Intel12/13代酷睿也同时保留了对DDR4的支持。不过,时隔仅仅一年半,DDR5的这些问题就已经解决得差不多了。现在高频DDR5内存遍地开......
  • UE5 C++ TMap
    概述映射的元素类型为键值对,元素类型实际上是TPair<KeyType,ElementType>,只将键用于存储和获取TMap和TMultiMap两者之间的不同点是TMap中的键是唯一的,而TMultiMap可存储多个相同的键TMap是散列容器,这意味着键类型必须支持GetTypeHash函数,并提供运算符==来比较各个键是否......
  • C++入门教程
    C++入门教程----------------------------------------------------------一.初识C++---------------------------------------------------------1.什么是C++.c++是一种较为基础的编程语言,虽然没有Python,Scratch那么高级,但是它应用范围很广.不论是信息奥赛还是国......
  • UE5 C++ TArray
    概述TArray是UE4中最常用的容器类。其速度快、内存消耗小、安全性高TArray类型由两大属性定义:元素类型和可选分配器元素类型是存储在数组中的对象类型。TArray被称为同质容器。换言之,其所有元素均完全为相同类型。单个TArray中不能存储不同类型的元素。分配器常被省略,默......