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

news/2025/1/11 9:59:05/

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.ppmy.cn/news/6232.html

相关文章

LeetCode HOT 100 —— 581. 最短无序连续子数组

题目 给你一个整数数组 nums &#xff0c;你需要找出一个 连续子数组 &#xff0c;如果对这个子数组进行升序排序&#xff0c;那么整个数组都会变为升序排序。 请你找出符合题意的 最短 子数组&#xff0c;并输出它的长度。 思路 方法一&#xff1a;双指针 排序 最终目的是让…

Python求多元函数的极小值

文章目录minimize_scalarminimize测试minimize_scalar 此为scipy中用于求函数最小值的方法&#xff0c;输入参数如下 minimize_scalar(fun, bracketNone, boundsNone, args(), methodbrent, tolNone, optionsNone)[source]其中&#xff0c;必选的参数只有一个&#xff0c;就是…

【算法】P1 算法简介

算法什么是算法正确与错误的算法算法可以解决什么问题本专栏有哪些算法什么是算法 算法 (Algorithm) 取某个值或集合作为 输入&#xff0c;并产生某个值或集合作为 输出。算法就是把输入转换为输出的计算&#xff0c;描述这个计算的过程来实现输入与输出的关系。 正确与错误的…

业聚医疗在港交所上市:市值约76亿港元,钱永勋、刘桂祯夫妇控股

12月23日&#xff0c;业聚医疗集团控股有限公司&#xff08;下称“业聚医疗”&#xff0c;HK:06929&#xff09;在港交所上市。本次上市&#xff0c;业聚医疗的发行价为8.80港元/股&#xff0c;全球发行发售5463.30万股&#xff0c;募集资金总额约为4.81亿港元&#xff0c;募资…

12【SpringMVC的异常处理】

文章目录六、SpringMVC的异常处理6.1 SpringMVC异常概述6.2 SpringMVC异常处理6.2.1 单个类处理方式6.2.2 处理全局异常6.2.3 注解方式实现全局异常六、SpringMVC的异常处理 6.1 SpringMVC异常概述 我们在处理异常时&#xff0c;通常使用try…catch块来处理程序中发生的异常&…

Hadoop综合项目——二手房统计分析(MapReduce篇)

Hadoop综合项目——二手房统计分析&#xff08;MapReduce篇&#xff09; 文章目录Hadoop综合项目——二手房统计分析&#xff08;MapReduce篇&#xff09;0、 写在前面1、MapReduce统计分析1.1 统计四大一线城市房价的最值1.2 按照城市分区统计二手房数量1.3 根据二手房信息发布…

直播弹幕系统(六)- SpringBoot + STOMP + RabbitMQ(使用MQ替代Spring代理)

直播弹幕系统&#xff08;六&#xff09;- SpringBoot STOMP RabbitMQ&#xff08;使用MQ替代Spring代理&#xff09;前言一. SpringBoot整合RabbitMQ代理Broker1.1 RabbitMQ安装STOMP插件&#xff08;Docker&#xff09;1.2 RabbitMQ相关准备1.3 其他代码二. 前端整合Rabbit…

STM32三条总线(AHB、APB1、APB2)的外设映射情况

STM32三条总线&#xff08;AHB、APB1、APB2&#xff09;的外设映射情况 1、AHB (1)Flash储存器 (2)DMA (3)复位和时钟控制 (4)CRC (5)以太网 (6)SDIO 2、APB1总线(支持低速状态下的工作) (1)定时器TIM2到TIM7 (2)RTC (3)WDT看门狗 (4)SPI2、SPI3 (5)USART2、USART3 (6)UART4、U…