动态内存管理:new和delete的底层探索

之前我们在C语言上是学过malloc和calloc还要realloc等函数来在堆上获取相应的内存,但是这些函数是存在缺陷的,今天引入对new和delete的学习,来了解new和delete的底层实现。

首先就是在C++中我们为什么要对内存进行区域的分块? 

答案是为了对内存进行更好的管理

那这些区域中对我们程序员来说最重要的区域就是堆,因为堆上的空间需要我们自行的进行申请和释放。

C语言内存题目

int globalVar = 1 ;
static int staticGlobalVar = 1 ;
void Test ()
{
static int staticVar = 1 ;
int localVar = 1 ;
int num1 [ 10 ] = { 1 , 2 , 3 , 4 };
char char2 [] = "abcd" ;
const char* pChar3 = "abcd" ;
int* ptr1 = ( int* ) malloc ( sizeof ( int ) * 4 );
int* ptr2 = ( int* ) calloc ( 4 , sizeof ( int ));
int* ptr3 = ( int* ) realloc ( ptr2 , sizeof ( int ) * 4 );
free ( ptr1 );
free ( ptr3 );
}
1. 选择题:
选项 : A .   B .   C . 数据段 ( 静态区 )   D . 代码段 ( 常量区 )
globalVar 在哪里? ____   staticGlobalVar 在哪里? ____
staticVar 在哪里? ____   localVar 在哪里? ____
num1 在哪里? ____
char2 在哪里? ____   * char2 在哪里? ___
pChar3 在哪里? ____       * pChar3 在哪里? ____
ptr1 在哪里? ____         * ptr1 在哪里? ____
2. 填空题:
sizeof ( num1 ) = ____ ;  
sizeof ( char2 ) = ____ ;       strlen ( char2 ) = ____ ;
sizeof ( pChar3 ) = ____ ;     strlen ( pChar3 ) = ____ ;
sizeof ( ptr1 ) = ____ ;

栈主要存放的是一些局部变量,函数参数,栈的最大特点就是向下增长,而堆是向上增长的,函数执行之后栈的的空间会自动的进行释放,函数栈帧的销毁其实就是将内存空间返回给我们的操作系统这个过程。栈还有一个特点就是它的效率高且容量空间有限

堆是给程序员进行管理的一块内存,堆是向上进行增长的,程序员进行使用之后需要进行释放,如果不进行释放就会存在很大的问题,内存泄漏是最主要的一个问题。分配的方式可能不是连续的,类似链表这样随机化。

代码段

存放常量和一些可读的代码

静态区
存放的是一些全局常量和静态数据 

那有了上面的基础我们就来完成上面的题目。

int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd";
const char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
1. 选择题:选项: A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)globalVar在哪里?_C___   staticGlobalVar在哪里?__C__staticVar在哪里?__C__   localVar在哪里?_A___num1 在哪里?__A__char2在哪里?_A___   *char2在哪里?_A__pChar3在哪里?____ A     *pChar3在哪里?___D_ptr1在哪里?____   A     *ptr1在哪里?_B___
2. 填空题:sizeof(num1) = 40____;  sizeof(char2) = _5___;      strlen(char2) = ___4_;sizeof(pChar3) = __4_/ 8_;     strlen(pChar3) = __4__;sizeof(ptr1) = _4_/8__;

sizeof(指针)是指针大小就是4字节或者8字节,然后就要注意的是字符串后面还是有一个\0这个需要注意一下。

C++内存管理方式

C 语言内存管理方式在 C++ 中可以继续使用,但有些地方就无能为力,而且使用起来比较麻烦,因 此C++ 又提出了自己的内存管理方式: 通过 new delete 操作符进行动态内存管理
我们先来看看new和delete是怎么使用的。
#include<iostream>
int main()
{int* p1 = new int;int* p2 = new int[10];delete p1;delete[] p2;return 0;
}

优势一

竟然学了new和delete,抛弃原来的malloc和realloc还有delete,那总有我们的优势,但是我们现在可以看到的优势好像就是除了简短,没有其他的优势了,它也不会进行初始化,没有将我们的内置类型进行初始化,但是这里我们只要知道它的优势就是代码变的更加简短了。这个是用法上的

 优势二

我们可以进行手动的初始化

 看下面一段代码我们可以看到它的优势更加明显了

场景:

如果我们要实现一个链表的话,C语言是要写一个创造节点的函数,然后再来进行实现的,我们先来看看创造节点的函数是怎么写的,然后再来看看C++中如果要进行创造一个链表的话是怎么实现才是最快的。

struct ListNode
{ListNode* _next;int _val;
};ListNode* CreateNode(int x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail\n");exit(-1);}newnode->_next = NULL;newnode->_val = x;return newnode;
}

我们可以看到的是需要我们自行的创造节点,然后进行链接,可以写一个for循环,但是每次都要去调用这个函数,很不方便,这个时候就要引出new的第三个优势。

优势三

对于自定义类型的时候new的过程是先开一段空间,然后调用它的构造函数进行初始化。我们来快速的实现一个链表的连接,看看C++里是怎么写的。

#include<iostream>using namespace std;
struct ListNode
{ListNode* _next;int _val;ListNode(int val):_next(nullptr),_val(val){}
};ListNode* CreateList(int n)//n表示的是长度
{ListNode head(-1);int val = 0;ListNode* tail = &head;for (int i = 0; i < n; i++){cin >> val;tail->_next = new ListNode(val);tail = tail->_next;}return head._next;
}
ListNode* CreateNode(int x)
{ListNode* newnode = (ListNode*)malloc(sizeof(ListNode));if (newnode == NULL){perror("malloc fail\n");exit(-1);}newnode->_next = NULL;newnode->_val = x;return newnode;
}
int main()
{ListNode* list = CreateList(5);return 0;
}

 看到C++里写的链表就会发现实现一个链表其实是很快的。

优势四

new失败之后是直接抛异常的,malloc失败之后是会进行检查的,所以new不需要进行失败检查。

operator newoperator delete函数

new delete 是用户进行 动态内存申请和释放的操作符 operator new operator delete
系统提供的 全局函数 new 在底层调用 operator new 全局函数来申请空间, delete 在底层通过 operator delete 全局函数来释放空间。

operator new

它是可以直接调用的,但是我们一般不直接的调用,new失败之后是直接抛异常的,抛异常的整个过程是在operator new上,我们调用new的时候其实是先调用operator new 然后再去调用相应的构造函数,但是operator new的底层其实还是malloc,所以operator new其实就是对malloc的封装,失败之后就抛异常,实现new。

operator是一个对malloc进行封装的函数,但是new是一个操作符,之前说过操作符其实都是在编译的时候就转化为相应的指令了,和函数不同,函数实在我们运行的时候进行的调用,所以两者之前是存在偏差的,new转化为指令是会去调用operator new 函数 然后再去调用构造函数,这两个操作都是new的底层。

operator new[]

其实这里也是一样的道理

我们new [] 之后是回去调用operator new []  然后operator new[] 再去调用operator new 和n次构造函数。

operator delete

这里需要思考的一个问题其实就是delete之后先去调用析构函数还是先去调用operator delete的问题,我们可以来看看下面的这段代码,这是一个stack的析构和构造函数。

class Stack
{
public:Stack():_top(0), _capacity(0), _a(new int[4]){}~Stack(){delete[] _a;_capacity = _top = 0;}
private:int* _a;size_t _top;size_t _capacity;
};

如果先去释放内存的化,这里的一个问题就是内存泄漏,所以我们要做的先去调用析构函数,然后再去调用我们的operator delete

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空               间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)if (_callnewh(size) == 0){// report no memory// 如果申请内存失败了,这里会抛出bad_alloc 类型异常static const std::bad_alloc nomem;_RAISE(nomem);}
return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData)
{_CrtMemBlockHeader * pHead;RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));if (pUserData == NULL)return;_mlock(_HEAP_LOCK);  /* block other threads */__TRY/* get a pointer to memory block header */pHead = pHdr(pUserData);/* verify block type */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));_free_dbg( pUserData, pHead->nBlockUse );__FINALLY_munlock(_HEAP_LOCK);  /* release other threads */__END_TRY_FINALLYreturn;
}
/*
free的实现
*/
#define   free(p)               _free_dbg(p, _NORMAL_BLOCK)

可以看到其实free也是一个宏。

通过上述两个全局函数的实现知道, operator new 实际也是通过 malloc 来申请空间 ,如果
malloc 申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施
就继续申请,否则就抛异常。 operator delete 最终是通过 free 来释放空间的

newdelete的实现原理

这里再和大家分享一个new对于内置类型和自定义类型的处理,和有没有析构函数的处理是不是一样的,首先先写一个类。然后对不同的情况进行处理。

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
int main()
{int* ptr = new int[10];delete ptr;return 0;
}

先来看看这种情况,我们没有配对的使用,但是最后的运行的情况是合法的,也没有进行报错,是因为这是一个内置类型,来看看汇编代码。

 可以看到的是我们也是new40个字节大小出来,再来看看自定义的的类型是个怎么样子的。

class A
{
public:A(){cout << "A()" << endl;}~A(){cout << "~A()" << endl;}
private:int _a;
};
int main()
{int* ptr = new int[10];delete ptr;A* ptr1 = new A[10];delete ptr1;return 0;
}

再来看看这个编译是没有问题,但是如果我们进行运行的时候就是会报错。

有人会说这里只是调用了一次析构函数,其实不是的,是因为我们释放内存的时候,没有从最开始的指向开始,而是从中间一段地方开始的,这就和我们银行的分期付款是差不多的,我们可以看啊看汇编代码。

 可以看到是44的字节大小,但是我们自定义类型的A其实只要四个字节的存储大小,所以里面的原因就是我们再前面多开一个字节的大小来进行存储。

所以才会这样,但是还有一个奇怪的现象就是如果我们把析构函数去掉的化就会变成不会报错。

class A
{
public:A(){cout << "A()" << endl;}/*~A(){cout << "~A()" << endl;}*/
private:int _a;
};
int main()
{int* ptr = new int[10];delete ptr;A* ptr1 = new A[10];delete ptr1;return 0;
}

 这样写也不对,但是也不会报错,所以得出一个结论。

new和delete要配对使用,要不然结果是不确定的

如果申请的是内置类型的空间, new malloc delete free 基本类似,不同的地方是:
new/delete 申请和释放的是单个元素的空间, new[] delete[] 申请的是连续空间,而且 new 在申
请空间失败时会抛异常, malloc 会返回 NULL

 

 定位new

我不算再这个部分来写,让大家来可以简单的了解一下定位new后面会学一些池化技术,这个时候就是需要用的时候,定位new其实是手动的去调用自定义类型的构造函数,构造函数是不能调用的,因为再定义的时候自动调用,但是定位new可以,所以定位new我们现在就可以理解为它是对一块已有的空间调用构造函数。 

可以来看看它是怎么使用的,这块大家先了解一下就OK了

class A
{
public:A(int n ):_a(n){}/*~A(){cout << "~A()" << endl;}*/
private:int _a;
};int main()
{A* pa = (A*)malloc(sizeof(A));new(pa)A(1);return 0;
}

malloc/freenew/delete的区别

malloc/free new/delete 的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地
方是:
1. malloc free 是函数, new delete 是操作符
2. malloc 申请的空间不会初始化, new 可以初始化
3. malloc 申请空间时,需要手动计算空间大小并传递, new 只需在其后跟上空间的类型即可,
如果是多个对象, [] 中指定对象个数即可
4. malloc 的返回值为 void*, 在使用时必须强转, new 不需要,因为 new 后跟的是空间的类型
5. malloc 申请空间失败时,返回的是 NULL ,因此使用时必须判空, new 不需要,但是 new
要捕获异常
6. 申请自定义类型对象时, malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new
在申请空间后会调用构造函数完成对象的初始化, delete 在释放空间前会调用析构函数完成
空间中资源进行清理。

内存泄漏

什么是内存泄漏:内存泄漏指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。
内存泄漏的危害:长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。
void MemoryLeaks()
{// 1.内存申请了忘记释放int* p1 = (int*)malloc(sizeof(int));int* p2 = new int;// 2.异常安全问题int* p3 = new int[10];Func(); // 这里Func函数抛异常导致 delete[] p3未执行,p3没被释放.delete[] p3;
}

内存泄漏其实就是对一块已经不再使用的空间没有进行释放。

内存泄漏是要程序员进行控制的,内存泄漏是不会报错的。

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

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

相关文章

ChatGPT高效提问—prompt实践(漏洞风险分析-重构建议-识别内存泄漏)

ChatGPT高效提问—prompt实践&#xff08;漏洞风险分析-重构建议-识别内存泄漏&#xff09; 1.1 漏洞和风险分析 ChatGPT还可以帮助开发人员预测代码的潜在风险&#xff0c;识别其中的安全漏洞&#xff0c;而不必先运行它&#xff0c;这可以让开发人员及早发现错误&#xff0…

【vscode】在vscode中如何导入自定义包

只需要额外添加这两条语句即可&#xff1a; import os,sys sys.path.append("../..") 需要注意的是&#xff0c;ipynb 文件打开的工作目录是文件本身的路径&#xff0c;而 py 文件打开的工作路径是 vscode 打开的路径。 相比较而言 pycharm 中创建好项目之后并不…

FT2232调试记录(2)

FT2232调试记录 &#xff08;1&#xff09;获取当前连接的FTDI设备通道个数:&#xff08;2&#xff09;获取当前连接的设备通道的信息:&#xff08;3&#xff09;配置SPI的通道:&#xff08;4&#xff09;如何设置GPIO:&#xff08;5&#xff09;DEMO测试&#xff1a; FT2232调…

【阅读笔记】空域保边降噪《Side Window Filtering》

1、保边滤波背景 保边滤波器的代表包括双边滤波、引导滤波&#xff0c;但是这类滤波器有一个问题&#xff0c;它们均将待处理的像素点放在了方形滤波窗口的中心。但如果待处理的像素位于图像纹理或者边缘&#xff0c;方形滤波核卷积的处理结果会导致这个边缘变模糊。 基于这个…

揭秘 2024 春晚刘谦魔术——代码还原

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、魔术大概流程 二、代码实现各个步骤 2.1 partition&#xff08;对半撕牌&#xff09; 2.2 bottom&#xff08;将 n 张牌置底…

基于微信小程序的智能社区服务小程序,附源码

博主介绍&#xff1a;✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;…

谈谈Lombok的坑

Lombok 是一个 Java 库&#xff0c;通过注解的方式在编译时自动为类生成 getter、setter、equals、hashCode 等方法&#xff0c;以简化代码和提高开发效率。本文主要谈谈代码简化背后的代价。 引入Lombok之前是怎么做的 IDE中添加getter/setter, toString等代码&#xff1a; …

单链表的介绍

一.单链表的概念及结构 概念&#xff1a;链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表 中的指针链接次序实现的 。 结构&#xff1a;根据个人理解&#xff0c;链表的结构就像火车厢一样&#xff0c;一节一节连在一起的&#x…

实现安全性

实现安全性 问题陈述 Chris希望阅读位于服务器上的电子邮件消息。他将自己的登录信息发送到服务器已进行验证。因此,Chris决定用基于表单的验证来验证他的登录信息。但是,他首先决定只用基于表单的验证测试登录页面 。 解决方案 要解决上述问题,Chris需要执行以下任务: 用…

从零开始做题:逆向 ret2shellcode jarvisoj level1

1.题目信息 BUUCTF在线评测 2.原理 篡改栈帧上的返回地址为攻击者手动传入的shellcode所在缓冲区地址&#xff0c;并且该区域有执行权限。 rootpwn_test1604:/ctf/work/9# gdb ./level1 GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.5) 7.11.1 Copyright (C) 2016 Free Software Fou…

【C++航海王:追寻罗杰的编程之路】关于模板,你知道哪些?

目录 1 -> 泛型编程 2 -> 函数模板 2.1 -> 函数模板概念 2.2 -> 函数模板格式 2.3 -> 函数模板的原理 2.4 -> 函数模板的实例化 2.5 -> 函数参数的匹配原则 3 -> 类模板 3.1 -> 类模板的定义格式 3.2 -> 类模板的实例化 1 -> 泛型编…

分布式文件系统 SpringBoot+FastDFS+Vue.js【一】

分布式文件系统 SpringBootFastDFSVue.js【一】 一、分布式文件系统1.1.文件系统1.2.什么是分布式文件系统1.3.分布式文件系统的出现1.3.主流的分布式文件系统1.4.分布式文件服务提供商1.4.1.阿里OSS1.4.2.七牛云存储1.4.3.百度云存储 二、fastDFS2.1.fastDSF介绍2.2.为什么要使…

tuple的使用例题(三元组)

题目大意&#xff1a;给一定关系&#xff0c;判断后面给的跟前面的有无矛盾 一开始还在想一些构造的操作&#xff0c;后面实在想不出来看题解&#xff0c;就是暴力啊...... 但是这种数据结构tuple&#xff08;元组&#xff09;确实是没见过&#xff0c;于是写篇总结 见这篇ht…

【MySQL】外键约束的删除和更新总结

&#x1f308;个人主页: Aileen_0v0 &#x1f525;热门专栏: 华为鸿蒙系统学习|计算机网络|数据结构与算法 ​&#x1f4ab;个人格言:“没有罗马,那就自己创造罗马~” #mermaid-svg-7niJLSFaPo0wso60 {font-family:"trebuchet ms",verdana,arial,sans-serif;font-siz…

六、Redis之数据持久化及高频面试题

6.1 数据持久化 官网文档地址&#xff1a;https://redis.io/docs/manual/persistence/ Redis提供了主要提供了 2 种不同形式的持久化方式&#xff1a; RDB&#xff08;Redis数据库&#xff09;&#xff1a;RDB 持久性以指定的时间间隔执行数据集的时间点快照。AOF&#xff0…

浅谈Linux环境

冯诺依曼体系结构&#xff1a; 绝大多数的计算机都遵守冯诺依曼体系结构 在冯诺依曼体系结构下各个硬件相互配合处理数据并反馈结果给用户 其中控制器和运算器统称为中央处理器&#xff08;CPU&#xff09;&#xff0c;是计算机硬件中最核心的部分&#xff0c;像人类的大脑操控…

汽车零部件制造业MES系统解决方案

一、​汽车零部件行业现状 随着全球汽车产业不断升级&#xff0c;汽车零部件市场竞争日趋激烈&#xff0c;从上游的钢铁、塑料、橡胶等生产到下游的主机厂配套制造&#xff0c;均已成为全球各国汽车制造大佬战略目标调整的焦点&#xff0c;其意欲在汽车零部件行业快速开疆扩土&…

Days 32 ElfBoard GDT工作原理

GDT&#xff08;Gas Discharge Tubes&#xff09;&#xff0c;即陶瓷气体放电管。GDT是内部由一个或一个以上放电间隙内充有惰性气体构成的密闭器件。GDT电气性能取决于气体种类、气体压力、内部电极结构、制作工艺等因素。GDT可以承受高达数十甚至数百千 安培的浪涌电流冲击&a…

自定义Spring Boot Starter

引言 在Spring Boot的世界中&#xff0c;Starter 能够简化我们的开发过程&#xff0c;通过封装常见的依赖和自动配置。本篇旨在为有志于进一步简化Spring Boot应用配置的开发者提供指导&#xff0c;让我们一起创建一个自定义的Spring Boot Starter。 一、什么是 Spring Boot …

springboot190基于springboot框架的工作流程管理系统的设计与实现

简介 【毕设源码推荐 javaweb 项目】基于springbootvue 的 适用于计算机类毕业设计&#xff0c;课程设计参考与学习用途。仅供学习参考&#xff0c; 不得用于商业或者非法用途&#xff0c;否则&#xff0c;一切后果请用户自负。 看运行截图看 第五章 第四章 获取资料方式 **项…