深入理解C语言的new[]和delete[]

 更新时间:2021年09月07日 10:58:53   作者:chls  
new和delete既是C++中的关键字也是一种特殊的运算符。这篇文章主要介绍了C++的new和delete详解,需要的朋友可以参考下

c++的动态内存管理方式和c语言不一样,在c++中使用new和delete来替换c语言中的malloc和free。这里有几个点不一样,

1、new和delete是操作符,malloc和free是函数(我的理解是c++将new和delete约定为操作符而已,new和delete操作符重载函数本质上还是函数)

2、c++有了类的概念,类对象的初始化除了要分配内存,还需要对内存进行初始化!所以,c++必须引入一种新的内存分配方式,既可以像malloc一样开辟内存,还要能够调用类对象的构造函数(delete的引入同理)。

3、new[]和delete[]是c++完全新增的内存操作符,他们和new和delete也是有不一样的地方。

下面,咱们来一一讲解⬇

1、重载操作符

既然new和delete都是操作符,咱们可以对new和delete进行重载;当你再使用new和delete操作内存时,编译器就会调用到咱们自己重载的new/delete全局函数了。(如对操作符重载不了解的,请自行补充知识)

void* operator new(size_t size){
    if(size == 0) size = 1;
    void* ptr = malloc(size);
    if(ptr == nullptr){
        std::cout << "ERROR NEW!" << std::endl;
    }
    std::cout << "NEW Memory Size = " << size << " address = " << ptr << std::endl;
    return ptr;
}
void* operator new[](size_t size){
    if(size == 0) size = 1;
    void* ptr = malloc(size);
    if(ptr == nullptr){
        std::cout << "ERROR NEW[]!" << std::endl;
    }
    std::cout << "NEW[] Memory Size = " << size << " address = " << ptr << std::endl;
    return ptr;
}
void operator delete(void* ptr){
    std::cout << "DELETE " << ptr << std::endl;
    if(ptr) free(ptr);
}
void operator delete[](void* ptr){
    std::cout << "DELETE[] " << ptr << std::endl;
    if(ptr) free(ptr);
}

此时,再使用 int* p = new int {1}; 开辟内存,那么,c++编译器会自动链接到我们刚才的操作符重载函数 void* operator new(size_t size) ,至于编译器是怎么将 int* p = new int {1}; 解析成 void* operator new(size_t size) 函数的,咱们不关心,咱们只要知道编译器做了这样一层代码解析转换即可。并且,转换的过程中会将需要开辟的内存大小当作形式参数传递过去!

这里还有一点需要注意:函数的返回值是void*,而咱们new操作接收的返回值为int*,这里其实也是编译在底层做了一层转换!(非常重要,new 的返回值和 operator new 函数的返回并不完全相同,包括类型和地址)

2、new和delete的原理

在开始验证new和delete之前,咱们先使用以下代码:

#include <iostream>
#include <stdlib.h>
#include <memory>
using namespace std;
//此处添加前面一步重载new和delete的代码
class Master
{
public:
    Master()
    {
        printf("constructor ---- %p\n",this);
        c = rand()%10000;
    }
    ~Master()
    {
        printf("destructor ---- %p\n", this);
    }
public:
    int c;
};
int main()
{
    Master * p = new Master{};
    delete p;
    return 0;
}

推荐一个在线调试cpp的网站:Compiler Explorer (godbolt.org)

Master * p = new Master{}; 这一行的汇编信息如下:

这里有3个点要注意,

  • 内存大小在编译时确定
  • 会先调用operator new(unsigned long)函数
  • 最后会调用构造函数

那么,delete操作的汇编信息应该和new刚好相反,这个可以自行测试!

下面将咱们重载的operator方法添加进去,然后执行,output日志信息如下:

NEW Memory Size = 4 address = 0x2154eb0
constructor ---- 0x2154eb0
destructor ---- 0x2154eb0
DELETE 0x2154eb0

通过日志,咱们也能够得出这样的结论:

new: 先malloc内存,然后执行构造函数 delete:先执行析构函数,然后free内存

3、new[]和delete[]的原理

前面咱们说了new/delete与new[]/delete[]有一些不同,那我们先看看日志信息,请添加以下测试代码:

int main()
{
    Master * p = new Master{};
    //测试new[]
    Master * pArray = new Master[5];    
    printf("pArray = %p\n", pArray);
    printf("pArray[0] = %p\n", &pArray[0]);
    printf("pArray[1] = %p\n", &pArray[1]);
    // delete[] pArray;
    // delete p;
    return 0;
}
///
output:---------------------------------------begin
NEW Memory Size = 4 address = 0x565422925eb0
constructor ---- 0x565422925eb0
NEW[] Memory Size = 28 address = 0x5654229262e0
constructor ---- 0x5654229262e8
constructor ---- 0x5654229262ec
constructor ---- 0x5654229262f0
constructor ---- 0x5654229262f4
constructor ---- 0x5654229262f8
pArray = 0x5654229262e8
pArray[0] = 0x5654229262e8
pArray[1] = 0x5654229262ec
output:----------------------------------------end
 

重点来了,malloc返回的内存地址(0x5654229262e0)和new[]返回的地址(0x5654229262e8)并不一致,而且申请的内存大小并不是Master类的大小 4*5=20,而是28!!

通过汇编代码,我们看到这里确实偏移了8个字节!其他的操作与new没有多大差别,都是先调用operator new[],然后再循环调用数组中的每个对象的构造函数!

通过output日志和汇编分析,可以得到我们前面讲到的结论:(非常重要,new 的返回值和 operator new 函数的返回并不完全相同,包括类型和地址)!

我们调用 delete[] pArray; 删除的地址为 0x5654229262e8,而真实触发 operator delete[] 的形参为 0x5654229262e0, 详细日志信息如下:

也就是operator new[] 和 operator delete[] 的操作的内存是真实地址,并且是正确的!为什么会这样?就是因为c++编译器在底层做了一次转换!

这个问题也很好理解,如果编译器没有这样一层逻辑转换,当你 delete[] 一个数组时,编译器要怎么操作呢?他怎么知道数组的长度然后遍历去调用析构函数呢?所以,这样一层逻辑转换有了存在的意义,这也体现在 new[] 数组时为什么大小并不是 = 对象大小*数组长度,而是多了8个字节!

好了嘛,那多出来的8字节是不是就是数组长度呢?咱们可以把这块内存信息输出!添加以下代码:

    unsigned long* pSize = (unsigned long*)pArray - 1;//偏移8字节
    printf("p = %p\n", pSize);
    printf("sizeof pArray = %d\n", *pSize);//输出该地址上的内存数据

日志输出:

偏移8字节之后就是咱们 operator new[] 函数分配的真实的内存地址,然后咱们输出该地址8字节空间的数据信息为5,这就是申请的数据空间的大小!

也就是有了这个多余的8字节(在32位系统上为4字节),new[] 存储数组长度到该内存, delete[] 时就能够从该内存读取数组长度,并遍历数组去调用析构函数啦!

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!

相关文章

  • QT开发应用程序的欢迎界面实例

    QT开发应用程序的欢迎界面实例

    下面小编就为大家带来一篇QT开发应用程序的欢迎界面实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08
  • C语言使用链表实现学生信息管理系统

    C语言使用链表实现学生信息管理系统

    这篇文章主要为大家详细介绍了C语言使用链表实现学生信息管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • C语言新手入门速通手册

    C语言新手入门速通手册

    C 语言是一种通用的、面向过程式的计算机程序设计语言。1972 年,为了移植与开发 UNIX 操作系统,丹尼斯·里奇在贝尔电话实验室设计开发了 C 语言。C 语言是一种广泛使用的计算机语言,它与 Java 编程语言一样普及,二者在现代软件程序员之间都得到广泛使用
    2022-04-04
  • C++浅析内联函数的使用

    C++浅析内联函数的使用

    为了消除函数调用的时空开销,C++ 提供一种提高效率的方法,即在编译时将函数调用处用函数体替换,类似于C语言中的宏展开。这种在函数调用处直接嵌入函数体的函数称为内联函数(Inline Function),又称内嵌函数或者内置函数
    2022-05-05
  • C语言实现通讯录管理系统

    C语言实现通讯录管理系统

    这篇文章主要为大家详细介绍了C语言实现通讯录管理系统,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-01-01
  • C++中的不规则二维数组实现代码

    C++中的不规则二维数组实现代码

    本文介绍了一个在C++中保存不定长二维数组的数据结构,在这个结构中,我们使用了一个含有指针和数组长度的结构体,用这样的一个结构体构造一个结构体数组,用于存储每一个不定长的数组,感兴趣的朋友一起看看吧
    2024-03-03
  • Qt实现QLineEdit输入前提示输入范围并用正则表达式限制输入范围

    Qt实现QLineEdit输入前提示输入范围并用正则表达式限制输入范围

    在日常开发过程中QLineEdit作为输入框,有时要限制输入的内容,这篇文章主要给大家介绍了关于Qt实现QLineEdit输入前提示输入范围并用正则表达式限制输入范围的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下
    2024-05-05
  • C++全面覆盖内存管理知识讲解

    C++全面覆盖内存管理知识讲解

    本章主要介绍C语言与C++的内存管理,以C++的内存分布作为引入,介绍C++不同于C语言的内存管理方式(new delete对比 malloc free),感兴趣的朋友来看看吧
    2022-06-06
  • C++简单又轻松建立链式二叉树流程

    C++简单又轻松建立链式二叉树流程

    二叉树的链式存储结构是指,用链表来表示一棵二叉树,即用链来指示元素的逻辑关系。通常的方法是链表中每个结点由三个域组成,数据域和左右指针域,左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址
    2022-06-06
  • c语言实现http下载器的方法

    c语言实现http下载器的方法

    这篇文章主要介绍了c语言实现http下载器的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-07-07

最新评论