C++语法|深入理解 new 、delete

在开发过程中,非常重要的语法就有我们new和delete,周所周知在C++中最为强大的能力就是对内存的控制,所以我们再怎么强调new和delete都不为过

文章目录

  • 1.new和delete基本语法
    • new和malloc的区别是什么?
      • (1)开辟单个元素的内存差别
      • (2)开辟数组内存语法差别
      • (3)开辟类类型的语法差别
      • (4) 为结构体类型分配内存
    • new有多少种?
  • 2.new和delete的函数重载
    • 重载operator new 和 operator delete
    • 重载new[]和delete[]
    • 重载 new 和delete的典型应用
  • 3.面试问题:new\new[]和delete\delete[]能混用吗(C++为什么区分单个元素和数组的内存分配和释放)?

1.new和delete基本语法

在本节中,我们会通过对比malloc和free来对比讲解new和delete的基本语法。

new和malloc的区别是什么?

我觉得想要完整描述他们两个的区别就是要从以下四个层次来进行描述:
开辟单个内存、开辟数组内存、开辟结构体内存和开辟类类型的内存。

还有个一句话总结,
malloc是一个库函数,而new是运算符,所以调用的方法肯定是不一样的。
malloc按字节开辟内存,只分配原始内存,不会调用构造函数,所以在不涉及到类类型的时候,其实用哪个语法上都差不多。
new不仅会分配内存内存,还会调用析构函数,并且对应的delete也会去调用析构山函数,但是free一个类类型首先需要显式调用析构函数,然后free这块内存。

所以说申请一段内存就调用malloc(更轻便),如果为类对象申请内存,请使用new!

(1)开辟单个元素的内存差别

malloc是按照字节来开辟内存的,返回值是void。需要对返回的类型进行一个强转*。并且对于开辟内存失败的语法就是通过返回值来进行检测的。
并且,我们需要对这块内存尽兴初始化,因为malloc可不帮我们进行初始化,他只负责给我们开辟内存

int *p = (int*)malloc(sizeof(int)); //molloc按照
if (p == nullptr) { //返回值是空说明内存开辟失败return -1; 
}
*p = 20;

然后它直观开辟内存,他不管初始化,我们需要单独用*p的代码来进行一个初始化的操作,释放这个p的内存需要一个free的库函数。它的语法也是把这块内存的起始地址传进来。

free(p)

也就是说new不仅可以做内存开辟,还可以做内存初始化操作,并且new可以直接指定开辟内存的类型,所以也不需要强转; 可以直接用小括号指定初始值

而new开辟内存失败是通过抛出bad_alloc类型的异常来判断。一般是使用try 和 catch

try {int *p1 = new int(20);
} catch (const bad_alloc &e) {}

释放内存直接delete p1即可。

delete p1;

(2)开辟数组内存语法差别

分别使用malloc和new开辟数组内存如下代码:

int *q = (int*)malloc(sizeof(int) * 20);
if (q == nullptr) {return -1;
} 
free(q);//int *q1 = new int[20];这样写的话堆上只负责开辟数组而不进行初始化
int *q1 = new int[20](); //20个int 。并且初始化所有元素为0
delete[]q1;

在开辟数组内存的语法中,malloc仍然遵循——指定要开辟的内存大小即可;
但是new却连语法都一样了,需要用到一个[]中括号来指明需要开辟的数组个数。并且new只支持数组元素初始化为0。

为什么C++里面要把释放单个元素内存和数组内存分开呢?
原因不体现在简单类型上,而是体现在面向对象的类类型上,我们在new的函数重载中会讲到讲到。

(3)开辟类类型的语法差别

我们一定要抓住关键点:
malloc 是 C 库函数,用于在堆上分配指定字节数的内存,但它只分配内存,不调用类的构造函数。因此,如果你使用 malloc 来分配一个类对象的内存,你需要手动调用构造函数(通常通过 placement new)并且手动管理初始化和销毁。

如果使用new来申请一个类类型,那就简单多了:

(4) 为结构体类型分配内存

其实如果在C++中,如果结构体类型没有写构造函数,编译器会自动生成一个默认构造函数。默认构造函数会将所有内置类型的成员初始化为未定义值(对于基本数据类型)。
此时malloc和new的使用没有什么本质上的区别。

new有多少种?

  • 抛出异常版本的new
int *p1 = new int(20);
  • 不抛出异常版本的new
int *p2 = new (nothrow) int; 
  • 常量new:在开辟常量new的时候,我们一定要用常量指针来指向这块内存,不然就会抛出错误。
//抛出错误,在堆上抛出了一个常量
//int *p3 = new const int(40);
//既然是使用常量我们就不能把它当一个普通内存,我们必须使用常量指针去指向这块内存
const int *p3 = new const int(40); 
  • 定位new:常常用在类类型的构造和销毁
int data = 0;
int *p4 = new (&data) int (50);

首先我们知道这里的data是在栈内存上的。当我们使用定位new,此时new就不会再在堆上为我们分配内存了,而是在已经存在的内存地址(即&data)上构造对象。
也就是说new (&data) int(50) data 的地址上构造一个 int,并初始化为 50。并且data仍然在栈上。

八股背诵总结:
malloc和new的区别
1.malloc按字节开辟内存,new开辟内存时要指定类型,所以malloc开辟内存返回的都是void* ,new的返回值自动转换成我们指定的指针类型operator new -> int*
2.malloc只负责开辟空间,new不仅仅有malloc的功能,还可以进行数据的初始化
3.malloc开辟内存失败返回nullptr指针;new抛出的是bad_alloc类型的异常
free和delete区别:
delete:调用析构函数,free不会调用

2.new和delete的函数重载

从汇编代码上可以看出,new的调用本质上是对operator new函数的调用。delete的调用本质上是对operator delete函数的调用。所以我们其实可以自己进行该函数的重载。

重载operator new 和 operator delete

//先调用oeprator new开辟内存空间、然后调用对象的构造函数(初始化)
void* operator new(size_t size) {void *p = malloc(size);if (p == nullptr)throw bad_alloc;cout << "operator new addr: " << p << endl;return p;
}// delete p;调用p指向对象的析构函数、再调用operator delete释放内存空间
void operator delete (void *ptr) {cout << "operator delete addr: " << ptr << endl;free(ptr);
}int main () {int *p = new int;delete p;return 0;
}

现在编译器链接的就是我们自己实现的operator new 和 operator delete,而不会链接C++库的这两个函数
如果想判断是否开辟内存失败:

try {int *p = new int;delete p;
} catch (const bad_alloc &err) {cerr << err.what() << endl;
}

重载new[]和delete[]

void* operator new[] (size_t size) {void *p = malloc(size);if (p == nullptr)throw bad_alloc;cout << "operator new[] addr: " << p << endl;return p;
}void operator delete[] (void *ptr) {cout << "operator delete[] addr: " << ptr << endl;free(ptr);
}

我们new一个数组进行一下测试:

try {int *p = new int;delete p;int *q = new int[10];delete[] q;
} catch (const bad_alloc &err) {cerr << err.what() << endl;
}

可以看出结果非常完美

什么时候要重载operator new 和 operator delete呢?
我们的应用或者整个的系统整个内存管理有我们自己的实现方案,比如说实现一个内存池,否则我们就规规矩矩使用库里面的这些函数即可。
除非我保证当前应用我自己解决内存管理的方案比new和delete都好,否则就用库里面的new和delete即可。
(内存池的使用场景:对于实时性要求比较强的,对于小块内存频繁进行分配和释放的应用最好是实现内存池)

总而言之,如果我们重写operator new和operator delete我们就可以接管整个程序的内存管理。

重载 new 和delete的典型应用

如何设计一个程序可以让其检查C++程序内存泄漏的问题
有内存泄漏那就说明new操作肯定没有对应的delete,我们可以在全局去重写 operator new 和 operator delete函数,在new操作里面我们用映射表记录一下我们都有哪些内存被开辟过了,然后释放的时候把相应的内存资源删除掉。如果我们映射表中表示还有一些内存没有被释放,那就肯定存在内存泄漏的问题。

#include <iostream>
#include <unordered_map>
#include <cstdlib>std::unordered_map<void*, size_t> allocations;void* operator new(size_t size) {void* ptr = std::malloc(size);if (ptr) {allocations[ptr] = size;}return ptr;
}void operator delete(void* ptr) noexcept {auto it = allocations.find(ptr);if (it != allocations.end()) {allocations.erase(it);}std::free(ptr);
}int main() {int* p = new int[10];// 故意不释放内存以测试泄漏检测// delete[] p;// 检查内存泄漏if (!allocations.empty()) {std::cout << "Memory leaks detected:" << std::endl;for (const auto& alloc : allocations) {std::cout << "Leaked " << alloc.second << " bytes at " << alloc.first << std::endl;}} else {std::cout << "No memory leaks detected." << std::endl;}return 0;
}

3.面试问题:new\new[]和delete\delete[]能混用吗(C++为什么区分单个元素和数组的内存分配和释放)?

如果是开辟单个的元素和数组的话,new和delete混用是没有任何问题的,因为此时他们只剩malloc 和 free 的功能

但是如果设计到类类型,那就不行了。我们还是跟之前一样实现operator new 和 operator delete
比如说我们设计以下类:

void* operator new(size_t size) {void *p = malloc(size);if (p == nullptr)throw bad_alloc;cout << "operator new addr: " << p << endl;return p;
};
void operator delete (void *ptr) {cout << "operator delete addr: " << ptr << endl;free(ptr);
};
class Test {
public:Test(int data = 10) { cout << "Test()" << endl}~Test() { cout << "~Test()" << endl;}
private:int ma_;
}

首先我们来测试operator new和operator delete[]的混用。

int main () {Test *p1 = new Test();//delete[] p1; 混用会报错,无法正常析构delete p1;
}

如果使用delete[] p1,会不停去析构Test而无法正常去释放内存。这是为什么呢?因为我们在开辟内存类对象和数组对象的时候,系统会为我们在返回给我们的指针的前面开辟一块内存(可能4字节、可能16字节)来存储我们开辟对象的个数,但是我们使用的是new而不是new[],所以前面根本就没有存储对象个数的内存,然而我们却调用delete[]来释放内存,系统就会去找存储对象内存个数的那块内存,所以酷酷报错。


现在我们来测试operator new[]和operator delete

int main () {Test *p2 = new Test[5];//delete p2;delete[] p2;
}

我们构造了5个Test类并且为它分配内存,但是如果使用delete p2,只能析构最后构造出来的Test类。(有的编译器会直接报错,进程直接崩溃)
结果如下:

operator new addr: 0x12f606de0
Test()
Test()
Test()
Test()
Test()
~Test()
operator delete addr: 0x12f606df0

我们从0x12f606de0地址但是却从0x12f606df0释放,有16个字节的内存泄漏!

总结:

  • 对于不同的编译器内置类型,new和delete[]混用 或者是 new[]和delete混用是完全没问题的,因为内置类型没有构造析构可言,只涉及内存开辟释放,而内存开辟释放的底层就是malloc和free。
  • 如果我们自己定义了类类型,就必须搭配使用而不能混用。因为其实我们调用new[]时,系统不仅仅会为我们开辟对应个数的内存空间,而且还会在开辟地址的首地址记录存储对象的个数(该区域一般占4个字节,我的电脑上占16个字节)。
    如果我们用new[]开辟,但是用delete来释放内存,系统会以为我们只开辟了一个类对象的内存,所以只会释放一个类对象的内存,就让我们上文中的输出operator new addr: 0x12f606de0 operator delete addr: 0x12f606df0
  • 自定义的类类型,有析构函数,为了调用正确的析构函数,那么开辟对象数组的时候,回多开辟4个字节记录对象的个数。所以我们不能随意混用

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DGC-GNN 配置运行

算法 DGC-GNN&#xff0c;这是一种全局到局部的图神经网络&#xff0c;用于提高图像中2D关键点与场景的稀疏3D点云的匹配精度。与依赖视觉描述符的方法相比&#xff0c;这种方法具有较低的内存需求&#xff0c;更好的隐私保护&#xff0c;并减少了对昂贵3D模型维护的需求。DGC-…

卡片笔记写作法 精读笔记 01

元数据 卡片笔记写作法&#xff1a;如何实现从阅读到写作 书名&#xff1a; 卡片笔记写作法&#xff1a;如何实现从阅读到写作作者&#xff1a; 申克阿伦斯简介&#xff1a; 卢曼的“盒中笔记”通常很简短&#xff0c;因为这些只是他庞大繁杂研究中的索引&#xff0c;等需要时&…

工作太闲怎么办?有没有什么副业推荐?

如果您的工作太闲&#xff0c;可以考虑参加一些副业&#xff0c;利用您的空余时间进行一些有意义的活动。以下是一些副业建议 1. 在线兼职 可以通过一些在线平台寻找兼职工作&#xff0c;如做在线调查、参与评估、进行数据输入等。 2.做任务 还可以做下百度的致米宝库&#…

2024第16届四川教育后勤装备展6月1日举办 欢迎参观

2024第16届四川教育后勤装备展6月1日举办 欢迎参观 邀请函 主办单位&#xff1a; 中国西部教体融合博览会组委会 承办单位&#xff1a;重庆港华展览有限公司 博览会主题&#xff1a;责任教育 科教兴邦 组委会&#xff1a;交易会159交易会2351交易会9466 展会背景 成都…

009.Rx(Reactive Extenstions)的关系

响应式扩展库在组成响应式系统的应用程序中发挥作用&#xff0c;它与消息驱动的概念相关。Rx不是在应用程序或服务器之间移动消息的机制&#xff0c;而是在消息到达时负责处理消息并将其沿着应用程序内部的执行链传递的机制。需要说明的是&#xff0c;即使您没有开发包含许多组…

暗区突围TWITCH掉宝领测试资格后,steam激活显示是无效激活码

自《暗区突围》测试启动以来&#xff0c;吸引了大量玩家关注&#xff0c;特别是通过在Twitch平台上观看直播即可获得测试资格的活动&#xff0c;更是掀起了热潮。然而&#xff0c;部分玩家在成功获得激活码后&#xff0c;在Steam平台激活时遭遇了“无效激活码”的问题。本文将提…

力扣HOT100 - 763. 划分字母区间

解题思路&#xff1a; class Solution {public List<Integer> partitionLabels(String s) {int[] last new int[26];int len s.length();for (int i 0; i < len; i) {last[s.charAt(i) - a] i;//记录字母最远的下标}List<Integer> partition new ArrayList…

tarjan学习

1.割点&#xff08;必须经过&#xff09;&#xff1a;当时&#xff0c;y是一个割点&#xff0c;x是y的一个子节点&#xff0c;当没有点x时&#xff0c;y无法访问其他点 2.割边&#xff08;必须经过&#xff09;&#xff1a;当时&#xff0c;y不经过这条边无法到达x&#xff0c…

Electron+Vue+pyinstaller服务打包

electron环境安装略 1. electron的入口文件配置test.js, 需要在package.json 配置文件中指定main: src/test.js const { app, BrowserWindow } require(electron)const createWindow () > {const win new BrowserWindow({width: 800,height: 600})// win.loadFile(inde…

手撸XXL-JOB(一)——定时任务的执行

SpringBoot执行定时任务 对于定时任务的执行&#xff0c;SpringBoot提供了三种创建方式&#xff1a; 1&#xff09;基于注解(Scheduled) 2&#xff09;基于接口&#xff08;SchedulingConfigurer&#xff09; 3&#xff09;基于注解设定多线程定时任务 基于Scheduled注解 首…

Language2Pose: Natural Language Grounded Pose Forecasting # 论文阅读

URL https://arxiv.org/pdf/1907.01108 TD;DR 19 年 7 月 cmu 的文章&#xff0c;提出一种基于 natural language 生成 3D 动作序列的方法。通过一个简单的 CNN 模型应该就可以实现 Model & Method 首先定义一下任务&#xff1a; 输入&#xff1a;用户的自然语言&…

探索 Canva 的功能以及如何有效使用 Canva

『创意瞬间变现&#xff01;Canva AI Drawing 让你的文字描绘成艺术』 在数字设计和创意领域&#xff0c;Canva 是创新和用户友好性的灯塔。这个平台不仅简化了图形设计&#xff0c;还引入了 AI Drawing 等强大工具&#xff0c;使其成为专业人士和初学者的首选解决方案。让我们…