动态内存管理: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_FINALLY
     return;
}
/*
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.mfbz.cn/a/387930.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MyBatisPlus基础操作之增删改查

目录 一、基本使用 1.1 插入数据 1.2 删除操作 1.3 更新操作 二、条件构造器Wrapper 2.1 常用AbstractWrapper方法 2.1.1 示例一 2.2.2 示例二 2.2.3 示例三 2.2 常用QueryWrapper方法 2.2.1 示例一 2.2.2 示例二 2.2.3 示例三&#xff08;常用&#xff09; 2.3 常…

攻防演练后的一点随记

攻防演练 攻防演练算是告一段落了&#xff0c;各位红队和蓝队的兄弟们都辛苦了&#xff0c;写一点随记&#xff0c;供大家参考。 记得第一次参加攻防演练是在2018年&#xff0c;当时被派到北京&#xff0c;在某个政企单位做攻防演练支撑工作&#xff0c;然后2020年又被紧急派到…

【STM32 CubeMX】学STM必会的数据结构——环形缓冲区

文章目录 前言一、环形缓冲区是什么二、实现环形缓冲区实现分析2.1 环形缓冲区初始化2.2 写buf2.3 读buf2.4 测试 三、代码总况总结 前言 在嵌入式系统开发中&#xff0c;经常需要处理数据的缓存和传输&#xff0c;而环形缓冲区是一种常见且有效的数据结构&#xff0c;特别适用…

提前部署游戏业务防护,为何如此重要?

现在做网络游戏的企业都知道服务器的安全对于我们来说很重要&#xff01;互联网上面的DDoS攻击和CC攻击等等无处不在&#xff0c;而游戏服务器对服务器的防御能力和处理能力要求更高&#xff0c;普通的服务器则是比较注重各方面能力的均衡。 随着游戏行业的壮大&#xff0c;网络…

java 宠物在线商城系统Myeclipse开发mysql数据库web结构jsp编程servlet计算机网页项目

一、源码特点 java 宠物在线商城系统是一套完善的java web信息管理系统 servletdaobean mvc模式&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S 模式开发。开发环境为TOMCAT7.0,Myeclipse8.5开发&…

尚硅谷最新Node.js 学习笔记(三)

目录 六、Node.js 模块化 6.1、介绍 什么是模块化与模块&#xff1f; 什么是模块化项目&#xff1f; 模块化好处 6.2、模块暴露数据 模块初体验 暴露数据 6.3、导入&#xff08;引入&#xff09;模块 6.4、导入模块的基本流程 6.5、CommonJS规范 七、包管理工具 7…

站在C/C++的肩膀速通Java面向对象

默认学过C或C&#xff0c;对变量、表达式、选择、循环都会。 运行特征 解释型语言&#xff08;JavaScript、Python等&#xff09; 源文件-(平台专属解释器)->解释器中执行编译型语言&#xff08;C、Go等&#xff09; 源文件-(平台编译器)->平台可执行文件Java 源文件-(…

算法详解:滑动窗口-- 最大连续1的个数 III

题目来源:力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 本期讲解滑动窗口经典例题,我会从三个点开始讲解题目1.题目解析2.算法原理 3.编写代码 1.题目解析 这道题目理解起来还是比较简单的,我们简单分析一下,也就是给定一个数组,数组是由1和0组成…

AtCoder Beginner Contest 335 (Sponsored by Mynavi) --- F - Hop Sugoroku -- 题解

目录 F - Hop Sugoroku 题目大意&#xff1a; 思路解析&#xff1a; 代码实现&#xff1a; F - Hop Sugoroku 题目大意&#xff1a; 思路解析&#xff1a; 容易想到这是一个dp题&#xff0c;然后初始转移方程为&#xff1a; 如果当a[i] 较大时&#xff0c;时间复杂度为 O(N…

【AI视野·今日NLP 自然语言处理论文速览 第七十九期】Thu, 18 Jan 2024

AI视野今日CS.NLP 自然语言处理论文速览 Thu, 18 Jan 2024 Totally 35 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers Deciphering Textual Authenticity: A Generalized Strategy through the Lens of Large Language Semantics …

MySQL数据库-MVCC多版本并发控制

mvcc,多版本并发控制&#xff08;Multi-Version Concurrency Control&#xff09;,是一种用于数据库管理系统中的并发控制方法. 在传统的并发控制方法中,如锁定机制,当一个事务修改数据时,会对相关的数据对象进行锁定,其他事务需要等待该锁释放才能进行操作。这种方法存在着事…

操作系统-408

一、操作系统概述 1、定义 负责协调软件和硬件的计算机资源的工作为上层应用提供简易的服务操作系统是系统软件 2、功能&#xff1a; 操作系统是系统资源的管理者 处理机管理存储器管理文件管理设备管理向上层提供方便易用的服务 命令接口程序接口对硬件机器的扩展 3、特征…

详解tomcat中的jmx监控

目录 1.概述 2.如何开启tomcat的JMX 3.tomcat如何实现JMX的源码分析 1.概述 本文是博主JAVA监控技术系列文章的第二篇&#xff0c;前面一篇文章中我们介绍了JAVA监控技术的基石——jmx&#xff1a; 【JMX】JAVA监控的基石-CSDN博客 本文我们将从使用和源码实现两个方面聊…

springboot743二手交易平台

springboot743二手交易平台 获取源码——》公主号&#xff1a;计算机专业毕设大全

Python面向对象学习小记——面向过程VS面向对象

【面向过程就好比你是一个工人&#xff0c;你得亲自去做一个个任务 面向对象就好比你一个包工头&#xff0c;你可以差遣你下面的工人去做】

日期类运算符重载以及const成员详细解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言进阶 数据结构初阶 Linux C初阶 算法 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂 目录 一.前言 二.运算符重载 2.1概念 2.2比较的符号重载 2.2.1…

linux 安装docker

目录 环境 操作步骤 1 下载脚本 2 执行脚本 3 检查docker版本&#xff0c;证明安装成功 环境 阿里云 ubuntu 22.04 64位 操作步骤 参考linux系统安装docker-腾讯云开发者社区-腾讯云 (tencent.com) 1 下载脚本 curl -fsSL https://get.docker.com -o get-docker.sh …

JavaWeb学习|Filter与ThreadLocal

学习材料声明 所有知识点都来自互联网&#xff0c;进行总结和梳理&#xff0c;侵权必删。 引用来源&#xff1a;尚硅谷最新版JavaWeb全套教程,java web零基础入门完整版 Filter 1、Filter 过滤器它是 JavaWeb 的三大组件之一。三大组件分别是&#xff1a;Servlet 程序、Liste…

RH850从0搭建Autosar开发环境【2X】- Davinci Configurator之XCP模块配置详解(上)

XCP模块配置详解 - 上 一、XCP模块配置项处理1.1 Tx Pdu配置项二、XCP模块其他配置项2.1 参数XcpMainFunctionPeriod2.2 参数XcpOnCanEnabled2.3 容器XcpOnCan总结从本节开始先专注与配置项错误处理以及构建Autosar Rh850的最小系统搭建。 XCP模块在汽车电子各控制器中处于十分…

使用ShardingJDBC实现分库分表

一、测试环境 JDK&#xff1a;1.8SpringBoot&#xff1a;2.7.17MySQL驱动&#xff1a;5.1.49MyBatis&#xff1a;2.3.1shardingJDBC&#xff1a;5.1.0 二、核心依赖 <!-- mysql 驱动 --> <dependency><groupId>mysql</groupId><artifactId>mysq…