C++中delete 和 delete []的真正区别

news2024/11/20 10:22:32

1.我们通常从教科书上看到这样的说明:

delete 释放new分配的单个对象指针指向的内存

delete[] 释放new分配的对象数组指针指向的内存

那么,按照教科书的理解,我们看下下面的代码:

int *a = new int[10];

delete a;        //方式1

delete [] a;     //方式2

肯定会有很多人说方式1肯定存在内存泄漏,是这样吗?

要分情况讨论:

(1).针对简单类型,使用new分配后的不管是数组还是非数组形式内存空间用两种方式均可 如:

int *a = new int[10];
delete a;
delete [] a;
 此种情况中的释放效果相同,原因在于:分配简单类型内存时,内存大小已经确定,系 统可以记忆并且进行管理,在析构时,系统并不会调用析构函数,

它直接通过指针可以获取实际分配的内存空间,哪怕是一个数组内存空间(在分配过程中 系统会记录分配内存的大小等信息,此信息保存在结构体_CrtMemBlockHeader中.

(2).针对类Class,两种方式体现出具体差异 

 当你通过下列方式分配一个类对象数组:

class A
   {
   private:
      char *m_cBuffer;
      int m_nLen;
   public:
      A(){ m_cBuffer = new char[m_nLen]; }
      ~A() { delete [] m_cBuffer; }
   };
   A *a = new A[10];
   delete a;    
delete a;    //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏

   delete [] a;      //调用使用类对象的析构函数释放用户自己分配内存空间并且   释放了a指针指向的全部内存空间

   delete   ptr   代表用来释放内存,且只用来释放ptr指向的内存。 

   delete[]   rg   用来释放rg指向的内存,!!还逐一调用数组中每个对象的destructor!!

   对于像int/char/long/int*/struct等等简单数据类型,由于对象没有destructor,所以用delete 和delete [] 是一样的!但是如果是C++对象数组就不同了!

例子:

#include <iostream>
using namespace std;
/class Babe
class Babe
{
public:
    Babe()
    {
        cout << \"Create a Babe to talk with me\" << endl;
    }
    ~Babe()
    {
        cout << \"Babe don\'t go away,listen to me\" << endl;
    }
};
//main function
int main()
{
    Babe* pbabe = new Babe[3];
    delete pbabe;
    pbabe = new Babe[3];
    delete [] pbabe;
    int * pbabebe = new int[3];
    delete pbabebe;
    return 0;
}
从列子中可以看出:

 delete pbabe;是不可以的,会报错

delete [] pbabe; 可以的,不报错。
/*************************************************************

delete用于释放由new创建的单个对象,delete[]用于释放由new创建的数组对象,一般都是配对使用。

但是为什么delete不用于释放数组对象,delete[]为什么不用于释放单个对象,从两者的原理说起。

delete在使用时会经历两步:

调用指针所指向的对象的析构函数
调用free函数回收指针所指向的内存
delete[]也是两步:

调用指针所指向数组中每个对象的析构函数
调用free函数回收指针所指向的内存
两者在第一步都需要调用对象的析构函数,只是一次或是多次的区别,也因此对于数组对象,需要使用delete[],否则除了第一个数组中第一个对象析构函数被调用,之后的对象都不会调用析构函数,在对象持有指针或是一些系统资源如文件句柄,Socket等时,如果不在析构函数中进行释放,将造成内存泄漏。注意到了吗,上文中有个前提是需要调用析构函数,如果对象不显式存在析构函数,如基本数据类型int,char或是自定义数据类型中不显式定义析构函数,这时候delete和delete[]就没有区别,因为不需要调用析构函数。

class TestA
{
public:
    TestA() { }
    virtual ~TestA() { cout << "~A" << endl; }
    int i;
};

class TestB
{
public:
    TestB() { }
    //virtual ~TestB() { cout << "~B" << endl; }
    int i;
};


int main() {
    int* arr = new int[10];
    delete[] arr;

    int* arr2 = new int[10];
    delete arr2;

    TestB* b = new TestB[10];
    delete[] b;

    TestB* bb = new TestB[10];
    delete bb;

    TestA* a = new TestA[10];
    delete[] a;

    TestA* aa = new TestA[10];
    delete aa;        //在这里出错
}

~A
~A
~A
~A
~A
~A
~A
~A
~A
~A
~A

一共调用了11次析构函数,在最后一次程序报错。

但在这里还没有解决问题,为什么delete aa会报错呢,按理来说虽然只调用了一次析构函数,之后的对象都没有调用,但也无所谓啊,最多就是对象中的一些资源没有被释放,但分配的内存还是可以由free函数回收掉的。关于这一点我们先思考一个问题,对于delete[]而言,他是如何知道要调用多少个对象的析构函数的,当我们在new[]时,向操作系统申请一块内存,然后调用构造函数,申请的这块内存由操作系统进行管理,它记录内存首地址和长度,这样在调用free时传入首地址进行回收,试想一下,我们知道对象的大小,比如int四个字节,此时操作系统记录了分配内存的总长度,是不是就可以知道有多少个对象了,但可惜的是操作系统并没有提供访问 内存长度的接口,也因此无法知道内存长度。所有编译器在分配时,会在数组首地址之前再申请一块空间用于记录数组个数(如果没有析构函数就不需要知道个数,编译器也就不会多申请这块空间),对于x64,这个大小存储在数组首地址的前八个字节,对于x86则是前4个字节

class TestA
{
public:
    TestA() { }
    virtual ~TestA() { cout << "~A" << endl; }
    int i;
};

int main() {
    int* arr = new int[10];
    cout << *((long long*)arr - 1) << endl;
    delete[] arr;


    TestA* a = new TestA[10];
    cout << *((long long*)a - 1) << endl;
    delete[] a;

}

-144680349937434461
10
~A
~A
~A
~A
~A
~A
~A
~A
~A
~A

对于int数组,由于没有析构函数所有编译器就不会存储数组大小,所有取前8个字节内容是不确定的,而对于a则记录大小为10。此时我们解决一个问题,在分配含有析构函数的自定义对象时,会多申请8个字节用于记录数组大小,方便调用析构函数时知道要调用多少次,所以在使用delete去释放一个数组对象时,由于传入的是数组首地址,但是申请的内存应该是数组首地址再往前8个字节的位置,数组首地址操作系统并未记录,所以会出错,同样的,使用delete[]去释放单个对象,由于他会访问前8个字节取得大小从而决定调用多少次析构函数,这时候行为将会不确定,却决于前8个字节会是什么值,可能会调用很多次析构函数,但在最后free时会出错,因为new对象时记录的是首地址,而不是首地址-8。

可以使用该代码测试操作系统记录的地址

class TestA
{
public:
    TestA() { }
    ~TestA() { cout << "~A" << endl; }
    int i;
};

int main() {
    TestA* a = new TestA[10];
    free((long long*)a - 1);
    //free(a);    //会出错,因为会多分配8个字节,首地址不为a
}
/***************************************************************

C++ 中 delete和 delete[]的区别,表层原因大家都了解,因为教科书上说得很明白:new和 delete需配对使用, new[]和 delete[]需配对使用。

但若问起在什么情况下针对 new[]申请的资源可以使用 delete释放而不会有任何问题,能讲清楚这点的人就很少了。因为这涉及到对 new、 delete、 new[]、 delete[]内部实现机制的理解。

根本原因在于, delete需要调用内存中一个元素的析构函数,而 delete[]需要调用内存中若干个元素的析构函数,这里就牵涉出一个问题—— delete[]是如何知道内存中元素的数量的?我们知道 delete[]中的 []并不会传入参数,所以这个数量不会是 delete[]传过来的,而是在 new[]的时候保存的,只有这样才得以在 delete[]的时候依据元素数量逐个调用析构函数。

接下来说 new[]如何存储这个数量,首先它会动态申请一段内存,然后在这段内存的首地址空间中存入元素数量,在这个空间之后的内存分配给各元素,new[]的返回值并不是这段动态内存空间的首地址,而是动态内存空间中存放第一个元素的内存地址。

以上说的是 delete[]需要调用元素析构函数的情况,但是C++的哲学是 Zero-cost Abstraction,所以对于并没有显式定义析构函数的 struct/class的对象元素来说,并不需要为其产生析构函数的代码,也就不需要在 delete[]的时候调用元素的析构函数以增加无谓的运算开销,那么, new[]也就不用存储这个元素数量。还有一种情况就是如 int等基本类型作为空间元素的时候,也不存在析构函数的调用,所以跟没有显示定义析构函数的对象元素一样:在 new[]时候不需要存储元素数量,在 delete[]时候不需要调用析构函数。

综上所述, new[]和 delete[]的具体行为受对象元素是否存在必须调用析构函数而有所不同。

一图胜千言,我画了三张图来展现上面说的三种元素情况:

  • int作为基本类型:

int *ptr = new int[5]

  • 定义了一个 class A,但是 A并没有显式定义析构函数:

A *ptr = new A[5]

  • 定义了一个 class B,并且 B显式定义了析构函数:

B *ptr = new B[5]

可以看出,对于 int *ptr = new int[5]和 A *ptr = new A[5],因为不涉及存储元素数量和对析构函数的调用,所以 delete和 delete[]的操作都仅仅是将传入的地址进行释放而不做其他额外事情。这种情况下,你使用 delete或者 delete[]都不会存在任何问题。

但是对于 B *ptr = new B[5]却一定要使用 delete[],因为传过来的并不是真正的动态内存首地址, delete[]的内部处理就会变成从传入的内存地址往前偏移获取真正的动态内存首地址,从该首地址空间获取到元素的数量,然后通过数量逐个调用元素的析构函数,完了再用得到的内存首地址释放动态内存。但若使用 delete就会只调用第一个元素的析构函数,并且将第一个元素的地址作为动态内存首地址进行释放,但是释放错误的内存地址(非申请时候动态内存的首地址)将发生严重错误,如在 visual studio 中会直接触发程序异常并崩溃。

接下来思考另一种情况,如果 B *ptr = new B操作后使用 delete[]释放呢?这也会产生非常严重的错误,因为它会根据这个内存地址往前偏移获取数量,但是这个数量值是个不确定的值,所以接下来发生的行为就是在指针越界访问的情况下调用了无数次析构函数,而这些内存空间中并不存在有效元素,该行为将发生程序崩溃,即便该过程程序照常执行,接下来用偏移地址释放内存也会崩溃,

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

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

相关文章

Java | 详解 Java连接MySQL、编写JdbcUtils工具类、使用数据库连接池、使用JavaTemplate

一、连接mysql数据库 步骤&#xff1a; 1、启动 MySQL &#xff1a;以管理员身份打开 cmd 命令行&#xff0c;输入 net start mysql 2、在 MySQL 创建一张表&#xff0c;用于后面的操作。我这里创建了一张 user 表&#xff0c;有id、name、password三个字段。 3、导入jar包 (1)…

【总结】最系统化的CV内容,有三AI所有免费与付费的计算机视觉课程汇总(2022年12月)...

欢迎大家关注有三AI的计算机视觉课程系列&#xff0c;我们的课程系列共分为5层境界&#xff0c;内容和学习路线图如下&#xff1a;第1层&#xff1a;掌握学习算法必要的预备知识&#xff0c;包括Python编程&#xff0c;深度学习基础&#xff0c;数据使用&#xff0c;框架使用等…

基于xml的自动装配之byType

基于xml的自动装配之byType 使用bean标签的autowire属性设置自动装配效果 自动装配方式&#xff1a;byType byType&#xff1a;根据要赋值的属性的类型&#xff0c;在IOC容器中匹配某个兼容类型的bean&#xff0c;为属性自动赋值 若在IOC中&#xff0c;没有任何一个兼容类型的b…

net/http 库的客户端实现(下)

前言 上一篇文章我们讲了 net/http 库客户端 request 的构建&#xff0c;接下来继续讲构建HTTP请求之后的处理操作 net/http 库的客户端实现(上) 启动事务 构建 HTTP 请求后&#xff0c;接着需要开启HTTP事务进行请求并且等待远程响应&#xff0c;以net/http.Client.Do()方法…

stm32f407VET6 系统学习 day01 GPIO 配置

1. GPIO 的5个配置 GPIO,即通用I/O(输入/输出)端口&#xff0c;是STM32可控制的引脚。STM32芯片的GPIO引脚与外部设备连接起来&#xff0c;可实现与外部通讯、控制外部硬件或者采集外部硬件数据的功能。 STM32F407有7组IO。分别为GPIOA~GPIOG&#xff0c;每组IO有16个IO口&…

首创证券上交所上市:募资19亿市值306亿 上半年净利降14%

雷递网 雷建平 12月23日首创证券股份有限公司&#xff08;简称&#xff1a;“首创证券”&#xff0c;股票代码为&#xff1a;“601136”&#xff09;昨日在上交所上市。首创证券此次发行27,333.38万股&#xff0c;发行价为7.07元&#xff0c;募资总额为19亿元。首创证券首日收盘…

小学生C++编程基础 课程10

938.最小公倍数的简单方法 &#xff08;课程A&#xff09; 难度&#xff1a;1 登录 939.最大公约数的简单方法 ( 课程A&#xff09; 难度&#xff1a;1 登录 940.韩信点兵 &#xff08;课程A&#xff09; 难度&#xff1a;1 登录 941.求123…N的和 &#xff08;课程A&#x…

Bloom filter-based AQM 和 BBR 公平性

设 B 为 Delivery rate&#xff0c;D 为 Delay&#xff0c;将 E B/D 作为衡量效能&#xff0c;所有流量的收敛状态是一个 Nash 均衡&#xff0c;没有任何流量有动机增加或者减少 inflight。参见&#xff1a;更合理的 BBR。 并不是都知道这道理&#xff0c;增加 inflight 能挤…

Java环境配置——Linux 安装JDK

注意这是用普通用户登录后&#xff0c;单独设置用户的java环境变量&#xff0c;非root用户 root用户的编辑命令是 vi /etc/profile 下载安装包 创建java目录 mkdir java 进入目录 cd java 上传安装包 将jdk-8u161-linux-x64.tar.gz上传到java目录 配置环境变量 解压安…

并查集详解

1.并查集原理 某公司今年校招全国总共招生10人&#xff0c;西安招4人&#xff0c;成都招3人&#xff0c;武汉招3人&#xff0c;10个人来自不同的学校&#xff0c;起先互不相识&#xff0c;每个学生都是一个独立的小团体&#xff0c;现给这些学生进行编号&#xff1a;{0, 1, 2,…

156. 如何在 SAP UI5 应用里显示 PDF 文件

SAP 不少标准应用都可以在业务流程进行到某个阶段,根据系统里的业务数据和 SAP 事先开发好的表单模板,生成最终的 PDF 文件并显示在应用页面上。 本文介绍一种在 SAP UI5 页面里嵌入显示 PDF 文件内容的方式,效果如下。 点击屏幕右上角的下载图标,可以将这个显示的 PDF 下…

ASP.NET 企业人力资源管理系统源码 HR源码 前端bootstrap框架开发

中小型企业HR人力资源管理系统源码带使用手册和操作说明 了解更多&#xff0c;可私信我&#xff01; 【程序语言】&#xff1a;.NET 【数据库】&#xff1a;SQL SERVER 2008 【运行环境】&#xff1a;WINDOWSIIS 【其他】&#xff1a;前端bootstrap框架 运行环境&#xff1…

学习记录-mybatis+vue+elementUi实现分页查询(前端部分)

前端这一块最方便的莫过于是element已经提供好了 接口&#xff0c;三个最关键的接口这里首先解决第一个&#xff0c;总数。 //总记录数totalCount:100,我直接在data中将其先初始化为100&#xff0c;之后直接在响应中设置&#xff0c;这是从后端查询到的值&#xff0c;不需要任何…

华为被迫开源,从认知到落地SpringBoot企业级实战手册(完整版)

前言 本手册重在引导读者进入真实的项目开发体验&#xff0c;围绕Spring Boot技术栈全面展开&#xff0c;兼顾相关技术的知识拓展&#xff0c;由浅入深&#xff0c;步步为营&#xff0c;使读者既能学习基础知识&#xff0c;又能掌握. 一定的开发技巧。本书的目标是让读者拥有一…

图文详解 (Kubernetes)K8S 和 容器中的退出状态码含义和原因及解决方法

图文详解 (Kubernetes)K8S 和 容器中的退出状态码含义和原因及解决方法。 什么是容器退出码 当容器终止时,容器引擎使用退出码来报告容器终止的原因。如果您是 Kubernetes 用户,容器故障是 pod 异常最常见的原因之一,了解容器退出码可以帮助您在排查时找到 pod 故障的根本…

Hadoop3.3.4最新版本安装分布式集群部署

Index of /dist/hadoop/commonhttps://archive.apache.org/dist/hadoop/common 集群规划&#xff1a; 注意&#xff1a; NameNode和SecondaryNameNode不要安装在同一台服务器ResourceManager也很消耗内存&#xff0c;不要和NameNode、SecondaryNameNode配置在同一台机器上。 …

PHP 变量

变量是用于存储信息的"容器"&#xff1a; 实例 <?php $x5; $y6; $z$x$y; echo $z; ?> 运行实例 与代数类似 x5 y6 zxy 在代数中&#xff0c;我们使用字母&#xff08;如 x&#xff09;&#xff0c;并给它赋值&#xff08;如 5&#xff09;。 从上面的…

echarts的grid——图表的位置配置

首先还是先认识grid&#xff0c;要弄清楚grid是哪一块区域&#xff0c;这就牵扯到对echarts图表元素的基本认识。为此&#xff0c;我做了一个总结&#xff0c;如图所示&#xff1a; 数学里的笛卡尔坐标系分为直角坐标系 和斜坐标系。而grid只适用于直角坐标系&#xff01; 我们…

分布式理论之分布式互斥

写在前面 本文一起看下分布式理论中的分布式互斥&#xff08;distributed mutual[ˈmjutʃuəl] exclusion&#xff09;问题&#xff0c;以及解决该问题相关算法。 1&#xff1a;什么是分布式互斥 我们先看下什么是临界资源&#xff08;critical resource&#xff09;&#…

如何提高苹果商店ASA广告的展示份额

众所周知&#xff0c;APP获得曝光后&#xff0c;才会有用户的点击率和下载&#xff0c;接下来柚鸥ASO会告诉大家&#xff0c;如何在保障ROI&#xff08;是指投入成本跟获得的收益的比值&#xff09;的情况下&#xff0c;为ASA获得最大的展示份额。 CPM是指通过商家付费&#x…