C++中基类的析构函数为什么要用virtual虚析构函数

news2024/11/26 17:06:04

直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了堆内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

代码演示

现有Base基类,其析构函数为非虚析构函数。Derived1和Derived2为Base的派生类,这两个派生类中均有以string* 指向存储其name的地址空间,name对象是通过new创建在堆上的对象,因此在析构时,需要显式调用delete删除指针归还内存,否则就会造成内存泄漏。
基类:

#include <iostream>
#include <string>
using namespace std;
class Base {
 public:
	Base():age_(new int(18)) {
	  cout << "Base()" << endl;
	}
	~Base(){
	  delete age_;
	  cout << "~Base()" << endl;
	}
	void showAge(){ cout <<"Base::age_==" << *age_ << endl;};
	virtual void showName()=0;
private:
	int age_;
};
//派生类Derived1 
class Derived1 : public Base {
 public:
	 Derived1():name_(new string("NULL")) {}
	 Derived1(const string& n):name_(new string(n)) {}

  ~Derived1() {
	  delete name_;
	  cout << "~Derived1(): name_ has been deleted." << endl;
  }
  void showName() {
	cout << "Derived1::name_==" << *name_ << endl;	
  }
 private:
 	string* name_;
};

//派生类Derived2 
class Derived2 : public Base {
 public:
	 Derived2():name_(new string("NULL")) {}
	 Derived2(const string& n):name_(new string(n)) {}

 ~Derived2() {
	 delete name_;
	 cout << "~Derived2(): name_ has been deleted." << endl;
 }
 void showName() {
	cout << "Derived2::name_==" << *name_ << endl;	
 }
 private:
 	 string* name_;
};

测试调用:

int main() {
	Derived1* d1 = new Derived1();
	Derived2 d2 = Derived2("Bob");
	delete d1;
	return 0;
}

d1为Derived1类的指针,它指向一个在堆上创建的Derived1的对象;
d2为一个在栈上创建的对象;
其中d1所指的对象需要我们显式的用delete 指针才会释放堆中Derived1对象,进而自动调用其析构函数;
如果不对d1进行delete,那么d1指针变量在其生命周期结束时,系统只会回收Derived1类指针d1(注意d1不是Derived1类对象,
真实的Derived1对象在堆内存中,而d1指针指向堆内存中的Derived1对象),而堆内存中的Derived1对象还在,故而不会触发Derived1对象的析构函数。
d2对象在其生命周期结束时,系统会自动调用其析构函数。看下其运行结果:
在这里插入图片描述
可以这么说,只有真正的类的实例(对象)所占内存,被释放时,才有可能触发其对应的析构函数,在析构函数中,对其对象中申请的堆内存空间,进行进一步的释放(比如上方name_指针指向的堆内存)。

上方的示例中Base基类的析构函数并不是虚析构函数,上方的执行结果,派生类的析构函数被调用了,正常的释放了其申请的内存资源。这两者并不矛盾,因为无论是d1还是d2,两者都属于静态绑定,而且其静态类型恰好都是派生类,因此,在析构的时候,即使基类的析构函数为非虚析构函数,也会调用相应派生类的析构函数。

下面我们来看下,当发生动态绑定时,也就是当用基类指针指向派生类对象时,这时候采用delete显式删除指针所指对象时,如果Base基类的析构函数没有加virtual,会发生什么情况?

int main() {
  Base* base[2] = {//base是一个指针数组,数组中的指针变量类型为Base*
    new Derived1(),//堆中生成Derived1对象,返回的对象地址用Base*指针来接收
    new Derived2("Bob")//同上      
  };
  for (int i = 0; i != 2; ++i) {
    delete base[i];//这里是delete 基类类型的指针
  }
  //测试当 delete base[i];时是否
  for (int i = 0; i != 2; ++i) {
    base[i]->show();    
  }
  return 0;
}

执行结果如下:
在这里插入图片描述
注意:delete base[i];//这里是delete 基类类型的指针,首先判断指针的静态类型即基类的析构函数是否为虚析构函数,如果不是虚析构函数,就是静态绑定,不会调派生类的析构函数,直接调指针的静态类型(基类类型)的析构函数,因为指针的静态类型为基类类型。如果是虚析构函数,就是动态绑定,会先调派生类的析构函数,然后再调基类的析构函数。虚函数是实现多态(动态绑定)的基础

从上面结果我们看到,尽管派生类中定义了析构函数来释放其申请的资源,但是并没有得到调用。原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。

在这里插入图片描述
从上图中也可以看到,虽然派生类的析构函数没有得到执行,但是派生类对象在堆内存是被释放了,在基类的析构函数中需要释放的内存被释放了,而只是在派生类对象的析构函数中,需要回收那部分内存没有得到释放。对象的释放,会触发析构函数的执行,但是析构函数没有被执行(执行的是基类的析构函数),并不代表对象没有被释放。

也就是说,在基类的析构函数为非虚析构函数的时候,并不一定会造成内存泄漏;当派生类对象的析构函数中有内存需要收回,并且在编程过程中采用了基类指针指向派生类对象,如为了实现多态,并且通过基类指针将该对象销毁,这时,就会因为基类的析构函数为非虚析构函数而不触发动态绑定,从而没有调用派生类的析构函数而导致内存泄漏。
因此,为了防止这种情况下内存泄漏的发生,最好将基类的析构函数写成virtual虚析构函数。

下面把Base基类的析构函数改为虚析构函数:

virtual ~Base(){
	  delete age_;
	  cout << "~Base()" << endl;
	}

这样就会实现动态绑定,派生类的析构函数就会得到调用,然后再调用基类的析构函数,从而避免了内存泄漏。

创建子类对象时:
构造函数的执行顺序: 先执行基类的构造函数(如果基类还有自己的父类,那就先执行它父类的构造,一层一层的执行),再执行子类
的构造函数。
默认都是调用基类的无参构造,想要调用基类的有参构造,需要子类在构造函数,参数列表里显示调用基类有参
构造。
注意:不管基类是抽象类都会调其构造函数,因为创建子类对象时,调用基类的构造函数,并不会产生基类对象。只是借助基类的构造函
数,来初始化,那些子类从基类继承而来的那些成员属性,这点可以根据隐式的this来判断。

析构函数的执行顺序: 先执行子类的析构函数,再执行基类的析构函数,如果基类还有自己的父类,那就再执行它父类的析构,一层一层
的执行。
注意:子类对象在销毁时,调用基类的析构函数,基类析构函数里面,需要处理的数据也是子类对象下的数据。这点可以根据隐式的this
来判断。

注意:上图中继承基类时应该去掉:public后的class关键字,这里就不在修改图了。

析构函数和虚析构函数特点:
当基类的析构函数是虚函数时,那么它的派生类的析构函数也默认是虚函数,隐式的和显示的都是虚函数。
如果基类的析构函数不是虚函数,而此时它的派生类的析构函数手动加上virtual变为虚函数,那么当delete 基类指针时,判断基类的析构
函数不是虚函数,那么也是静态绑定。不可能会触发派生类的析构函数。

记住一句话:虚函数是实现类多态的基础,关于虚函数的动态绑定原理,可以看这篇文章:虚函数讲解。
另外需要注意的是:构造函数及析构函数都不能被继承。而且构造函数不能为虚函数,析构函数可以为虚函数。

故: 继承时,要养成的一个好习惯就是,基类析构函数中,加上virtual。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1393064.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

给科研人的 ML 开源发布工具包

什么是开源发布工具包&#xff1f; 恭喜你的论文成功发表&#xff0c;这是一个巨大的成就&#xff01;你的研究成果将为学界做出贡献。 其实除了发表论文之外&#xff0c;你还可以通过发布研究的其他部分&#xff0c;如代码、数据集、模型等&#xff0c;来增加研究的可见度和采…

tinyxml2

使用tinyxml2&#xff0c;得知道一些xml基础 xml tutorial--菜鸟 tinyxml2类对象 链接 结构 XMLNode 什么是节点 节点&#xff1a;元素、声明、文本、注释等。 XMLDocument xml文档(文件)对象。 作用&#xff1a; 加载xml文件&#xff0c; tinyxml2作用 1&#xff0c;…

数据守护盾牌:敏感数据扫描与脱敏,让安全合规无忧

前言 在信息时代&#xff0c;数据已经成为企业和组织的核心资产&#xff0c;其价值与日俱增。然而&#xff0c;随着数据使用的普及和复杂度的提升&#xff0c;数据安全与合规问题也变得越来越突出。敏感数据的保护显得尤为重要&#xff0c;因为这些数据一旦泄露或被不当使用&a…

Servlet系列两种创建方式

一、使用web.xml的方式配置&#xff08;Servlet2.5之前使用&#xff09; 在早期版本的Java EE中&#xff0c;可以使用XML配置文件来定义Servlet。在web.xml文件中&#xff0c;可以定义Servlet的名称、类名、初始化参数等。然后&#xff0c;在Java代码中实现Servlet接口&#x…

【机器学习300问】9、梯度下降是用来干嘛的?

当你和我一样对自己问出这个问题后&#xff0c;分析一下&#xff01;其实我首先得知道梯度下降是什么&#xff0c;也就它的定义。其次我得了解它具体用在什么地方&#xff0c;也就是使用场景。最后才是这个问题&#xff0c;梯度下降有什么用&#xff1f;怎么用&#xff1f; 所以…

muduo网络库剖析——监听者EpollPoller类

muduo网络库剖析——监听者EpollPoller类 前情从muduo到my_muduo 概要epoll原理解析epoll提供的接口epoll的触发模式epoll实现多路复用 框架与细节成员函数使用方法 源码结尾 前情 从muduo到my_muduo 作为一个宏大的、功能健全的muduo库&#xff0c;考虑的肯定是众多情况是否…

低代码配置-属性配置面板设计

模块设计 tab项切换 组件基础属性组件数据属性组件事件属性表单属性 模块输出函数设计 tab切换函数 列表表单属性 数据来源&#xff1a; 调用接口时一次赋予&#xff0c;无需使用selectItem&#xff0c;如需使用&#xff0c;归入基础属性列表标题是否展示筛选区域 示例&am…

前端框架前置学习Webpack(1) 常用webpack配置

什么是Webpack? 定义 本质上,Webpack是用于现代JavaScript应用程序的静态模块打包工具.当webpack处理应用程序时,它会在内部从一个或多个入口点构建一个依赖图(dependency graph),然后将你项目中所需的每一个模块组合成一个或多个bundles,它们均为静态资源,用于展示你的内容.…

数学建模--论文

内容来自数学建模BOOM&#xff1a;【快速入门】北海&#xff1a;数模建模基础MATLAB入门论文写作数学模型与算法(推荐数模美赛国赛小白零基础必看教程)_哔哩哔哩_bilibili 目录 一、论文整体模版 1.整体框架 2.示例 二、标题 1.标题主题事项 三、摘要 1.摘要三要素&am…

LaTeX 多栏文档 Multiple columns如何插入图片并修改样式

在今天写报告的时候用到了 latex 的多栏列表&#xff0c;插入图片的时候感觉很无助 如果不喜欢让Latex自动安排图片位置&#xff0c;可以使用float包&#xff0c;然后可以使用\begin{figure}[H]。 记得提前导入这个包 \usepackage{float} 为了让我的图片的caption居中&#xf…

Go 语言中高效切片拼接和 GO 1.22 提供的新方法

Table Contents 切片拼接的必要性基本拼接方法及其局限性使用 append 函数高效拼接的策略控制容量和避免副作用利用 Go 1.22 的新特性切片动态扩容的深入理解内存重新分配与数据迁移性能优化策略结论在 Go 语言中,切片拼接是一项常见的操作,但如果处理不当,可能会导致性能问…

Verilog刷题笔记15

题目&#xff1a; An adder-subtractor can be built from an adder by optionally negating one of the inputs, which is equivalent to inverting the input then adding 1. The net result is a circuit that can do two operations: (a b 0) and (a ~b 1). See Wikipe…

[go语言]输入输出

目录 知识结构 输入 1.Scan ​编辑 2.Scanf 3.Scanln 4.os.Stdin --标准输入&#xff0c;从键盘输入 输出 1.Print 2.Printf 3.Println 知识结构 输入 为了展示集中输入的区别&#xff0c;将直接进行代码演示。 三者区别的结论&#xff1a;Scanf格式化输入&#x…

中科院罗小舟团队提出 UniKP 框架,大模型 + 机器学习高精度预测酶动力学参数

作者&#xff1a;李宝珠 编辑&#xff1a;三羊 中国科学院深圳先进技术研究院罗小舟团队提出了&#xff0c;基于酶动力学参数预测框架 (UniKP)&#xff0c;实现多种不同的酶动力学参数的预测。 众所周知&#xff0c;生物体内的新陈代谢是通过各种各样的化学反应来实现的。这…

SpringBoot 统计API接口用时该使用过滤器还是拦截器?

统计请求的处理时间&#xff08;用时&#xff09;既可以使用 Servlet 过滤器&#xff08;Filter&#xff09;&#xff0c;也可以使用 Spring 拦截器&#xff08;Interceptor&#xff09;。两者都可以在请求处理前后插入自定义逻辑&#xff0c;从而实现对请求响应时间的统计。 …

Modelsim SE 10.5安装教程

ModelSim 是一种功能强大的硬件描述语言 (HDL&#xff0c;Hardware Description Language) 仿真和验证工具&#xff0c;可以单独仿真&#xff0c;也可以联合Quartus/Vivado等软件联合仿真&#xff0c;仿真速度快&#xff0c;广泛应用于数字电路设计和验证领域。 大学老师爱教VH…

JavaWeb后端——Maven

maven主要服务于基于Java平台的项目构建、依赖管理和项目信息管理 maven项目对象模型简称POM&#xff0c; maven解决问题&#xff1a; 1. 添加第三方jar包&#xff0c;maven将 jar 包放在本地仓库中统一管理&#xff0c;使用时用坐标的方式引用即可 2. 解决 jar 包之间的依…

计算机网络-计算机网络的概念 功能 发展阶段 组成 分类

文章目录 计算机网络的概念 功能 发展阶段总览计算机网络的概念计算机网络的功能计算机网络的发展计算机网络的发展-第一阶段计算机网络的发展-第二阶段-第三阶段计算机网络的发展-第三阶段-多层次ISP结构 小结 计算机网络的组成与分类计算机网络的组成计算机网络的分类小结 计…

springBoot 添加自定义类库包

一、新建SpringBoot Web 二、添加类库包 com.saas.pdf 删除掉多余的类&#xff0c;新建类&#xff1a;PdfUtil.java package com.saas.pdf;public class PdfUtil {public static void Save(String filePath) {System.out.println("保存成功&#xff01;");} }三、…

阿里云服务器4核8G配置收费标准及新老用户优惠价格整理

阿里云服务器4核8g配置云服务器u1价格是955.58元一年&#xff0c;4核8G配置还可以选择ECS计算型c7实例、计算型c8i实例、计算平衡增强型c6e、ECS经济型e实例、AMD计算型c8a等机型等ECS实例规格&#xff0c;规格不同性能不同&#xff0c;价格也不同&#xff0c;阿里云服务器网al…