动态内存管理: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.hqwc.cn/news/469955.html

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

相关文章

【STM32 CubeMX】串口编程DMA

文章目录 前言一、DMA方式1.1 DMA是什么1.2 CubeMX配置DMA1.3 DMA方式函数使用DMA的发送接收函数 总结 前言 在嵌入式系统中&#xff0c;串口通信是一项至关重要的功能&#xff0c;它允许单片机与外部设备进行数据交换&#xff0c;如传感器、显示器或其他设备。然而&#xff0…

深度学习疆界:探索基本原理与算法,揭秘应用力量,展望未来发展与智能交互的新纪元

目录 什么是深度学习 深度学习的基本原理和算法 深度学习的应用实例 深度学习的挑战和未来发展方向 挑战 未来发展方向 深度学习与机器学习的关系 深度学习与人类的智能交互 什么是深度学习 深度学习是一种基于神经网络的机器学习方法&#xff0c;旨在模仿人类大脑分析…

WEB APIs(1)

变量声明const&#xff08;修饰常量&#xff09; const优先&#xff0c;如react&#xff0c;基本const&#xff0c; 对于引用数据类型&#xff0c;可用const声明&#xff0c;因为储存的是地址 何为APIs 可以使用js操作HTML和浏览器 分类&#xff1a;DOM&#xff08;文档对象…

Rust - 切片Slice

Slice类型 Slice数据类型没有所有权&#xff0c;slice允许我们引用集合中一段连续的元素序列而不用引用整个集合。字符串slice(string slice) 是String中 一部分值的引用。如下述代码示例&#xff0c;不是对整个String的引用而是对部分String的引用&#xff1a; fn main() {l…

代码随想录算法训练营Day27|回溯算法·组合总和、组合总和II、分割回文串

组合总和 class Solution{ private:vector<vector<int>>result;vector<int>path;void backtracking(vector<int>& candidates,int target,int sum,int startIndex){if(sum > target){return;}if(sum target){result.push_back(path);return;}…

Python之:如何使用双重for循环输出九九乘法表?

文章目录 前言源代码 前言 如何用for双重循环输出九九乘法表&#xff1f;教程来咯&#xff01; 源代码 代码如下&#xff1a; for i in range(1, 10):for j in range(1, i1):print(f{j}{i}{i*j}\t, end)print()你学会了吗&#xff1f;效果如下&#xff1a; 想看详解&#…

【贪心算法】代码随想录算法训练营第三十二天 |122.买卖股票的最佳时机II,55.跳跃游戏,45.跳跃游戏II(待补充)

122.买卖股票的最佳时机II&#xff08;未观看&#xff0c;动态待补充&#xff09; 1、题目链接&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 2、文章讲解&#xff1a;代码随想录 3、题目&#xff1a; 给定一个数组&#xff0c;它的…

猫头虎分享已解决Bug || DNS解析问题(DNS Resolution Issue):DNSLookupFailure, DNSResolveError

博主猫头虎的技术世界 &#x1f31f; 欢迎来到猫头虎的博客 — 探索技术的无限可能&#xff01; 专栏链接&#xff1a; &#x1f517; 精选专栏&#xff1a; 《面试题大全》 — 面试准备的宝典&#xff01;《IDEA开发秘籍》 — 提升你的IDEA技能&#xff01;《100天精通鸿蒙》 …

谈谈Lombok的坑

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

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

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

.target勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复

导言&#xff1a; 网络安全威胁如勒索病毒已经成为企业和个人数据安全的重大挑战之一。.target勒索病毒作为其中的一种&#xff0c;以其高度复杂的加密算法和迅速变化的攻击手法备受关注。本文将深入介绍.target勒索病毒的特点&#xff0c;探讨如何有效地恢复被加密的数据文件…

【学网攻】 第(28)节 -- OSPF虚链路

系列文章目录 目录 系列文章目录 文章目录 前言 一、什么是OSPF虚链路&#xff1f; 二、实验 1.引入 实验目标 实验背景 技术原理 实验步骤 实验设备 实验拓扑图 实验配置 扩展 实验拓扑图 实验配置 实验验证 文章目录 【学网攻】 第(1)节 -- 认识网络【学网攻…