【C++】内存管理深入解析

目录

  • 1. 内存的五大区域
    • 1.1 栈区(Stack)
    • 1.2 堆区(Heap)
    • 1.3 全局/静态存储区
    • 1.4 常量存储区
    • 1.5 代码区
  • 2. 回顾c语言的动态内存管理
    • 2.1 malloc/calloc/realloc
    • 2.2 free
  • 3. C++中的新旧对话
    • 3.1 new
    • 3.2 delete
  • 4. new/delete的实现原理
    • 4.1 new的工作流程
    • 4.2 delete的工作流程
    • 4.3 new T[N]和delete[ ] 的原理的原理
  • 5.定位new(Placement new)
  • 6. 常见面试题
    • 6.1 malloc/free和new/delete的区别
    • 6.2 内存泄露
  • 7.总结:

在C++的世界里,内存管理是一个至关重要的话题。与C语言相比,C++引入了对象的概念,使得内存管理变得更加复杂但也更加灵活。本文将深入探讨C++中的内存管理机制,包括内存的分布、动态内存的申请与释放,以及new和delete的使用。

1. 内存的五大区域

1.1 栈区(Stack)

栈区主要用于存储函数的局部变量、函数参数等。这部分内存的分配和释放是自动进行的,遵循先进后出的原则。

代码示例

void function() {
    int localVar = 10; // 局部变量存储在栈区
}

1.2 堆区(Heap)

堆区用于程序运行时的动态内存分配,由程序员通过new、delete(C++)或malloc、free(C)手动管理。

代码示例

int* dynamicArray = new int[10]; // 动态分配数组存储在堆区
delete[] dynamicArray; // 释放堆区内存

1.3 全局/静态存储区

全局变量和静态变量(包括静态全局变量和静态局部变量)存储在此区域。这部分内存在程序启动时分配,在程序结束时释放。

代码示例

int globalVar = 20; // 全局变量
static int staticGlobalVar = 30; // 静态全局变量

void function() {
    static int staticLocalVar = 40; // 静态局部变量
}

1.4 常量存储区

常量字符串和其他常量数据存储在此区域。这部分内存通常是只读的。

代码示例

const char* constString = "Hello, World!"; // 常量字符串存储在常量存储区

1.5 代码区

存储程序的二进制代码,即编译后的机器语言指令。这部分内存也是只读的,确保程序代码不会被程序本身或其他程序意外修改。

代码区直接对应于程序的可执行代码

补充说明:

  • 内存映射段:用于映射外设或文件进内存的特殊区域,也可以用于进程间通信。
  • 内核空间:操作系统保留的内存区域,不可直接访问。

2. 回顾c语言的动态内存管理

2.1 malloc/calloc/realloc

malloc函数: 通常用于动态创建数组或者结构体等数据结构。

int *ptr;

ptr = (int*)malloc(5 * sizeof(int));

上述示例代码演示了使用malloc函数动态分配了一个包含5个整数元素的数组,并将其地址赋给指针ptr。

可以将malloc比喻成一个灵活调度仓库管理员,根据不同货物大小和数量进行合理调度并返回货物位置信息.

其中: malloc申请的空间都是未初始化的,即被编译器置为随机值


calloc函数: 在内存中分配指定数量的连续空间,并将每个字节都初始化为零

与malloc函数不同,calloc函数需要两个参数:所需元素的个数每个元素的大小。这使得calloc函数在申请数组等数据结构时更加方便。

int *ptr;

ptr = (int*)calloc(5, sizeof(int));

上述示例代码演示了使用calloc函数动态分配了一个包含5个整数元素的数组,并将其地址赋给指针ptr。与malloc不同,calloc会将分配的内存空间全部初始化为0。

calloc函数用途

  • 用于动态分配数组或结构体等数据结构。

  • 在需要清零初始化内存内容时使用


realloc函数: 用于重新调整动态内存分配大小的函数。它可以改变之前分配区域的大小,同时保留原有内容。

注意:如果新申请的空间比原来的大,则新空间中新增部分未初始化;如果新申请空间比原来小,则原有内容可能会被截断。

int *ptr;

// 假设ptr已经通过malloc或者calloc进行了动态内存分配
ptr = (int*)realloc(ptr, 10 * sizeof(int));

上述示例代码演示了使用realloc函数重新调整了之前已经动态分配过的内存空间,将其扩展为包含10个整数元素的数组。

realloc函数用途

  • 用于调整之前动态分配内存空间的大小。

  • 在需要扩大或缩小已分配内存空间时使用


2.2 free

free函数是用于释放动态分配内存空间的函数。它的作用是将之前通过malloc、calloc或realloc等函数动态分配的内存空间进行释放,以便系统可以重新利用这些空间。

int *ptr;

// 假设ptr已经通过malloc、calloc或者realloc进行了动态内存分配
free(ptr);

注意同一块空间不能释放两次。free 后通常会把指针置空。

3. C++中的新旧对话

C语言中管理函数只能对内置类型使用,而 C++ 中存在很多自定义类型,malloc等函数无能为力。

于是C++引入了new和delete操作符,与C语言的malloc和free相比,它们提供了类型安全和对象构造/析构的自动化

3.1 new

new操作符是C++中用于动态内存分配的关键操作符之一,它与malloc函数类似,但提供了更加便捷和安全的内存管理方式。
使用new操作符可以动态地在堆区分配所需大小的内存空间,并返回该空间的首地址。

与malloc不同

  • new操作符会自动计算所需空间的大小,并且在分配失败时抛出异常,而不是返回NULL指针,如下:
int* p2 = (int*)malloc(sizeof(int));
if (p2 == nullptr)
{
	perror("malloc fail");
}

int *ptr = new int; // 动态创建一个整数对象

malloc后还需要进行判断,而new在分配失败时抛出异常,因为不需要判断。

  • new能在分配后进行初始化
int* p1 = new int(10);	//申请一个int,初始化10
int* p2 = new int[10] {1, 2, 3, 4};.//申请10个int,并初始化
  • new可以用于分配自定义类型
//假设存在A类
A* ptr = new A[5];	//申请五个A类,这些类都在堆上
  • new动态开辟时,会调用自定义类型的构造函数

3.2 delete

使用delete操作符可以释放之前通过new操作符动态分配的内存空间,同时也会调用该内存空间对应对象的析构函数来进行资源清理.

int *ptr = new int; // 动态创建一个整数对象

int *arr = new int[5]; // 动态创建包含5个整数元素的数组

// 在不再需要这些动态分配内存时使用delete进行释放

delete ptr;

delete[] arr;
  • delete调用销毁时,会先调用自定义类型的析构函数

注意:切记,申请与释放要配套使用。malloc和free配套使用,new和delete配套使用


4. new/delete的实现原理

在C++中,new和delete不仅是关键字,它们背后的实现涉及到一系列复杂的操作,包括内存分配、对象构造、内存释放、和对象析构。更深入地,new和delete实际上是通过调用全局函数operator new和operator delete来完成其工作的。

4.1 new的工作流程

当你使用new操作符时,C++会执行以下步骤:

  • 内存分配:首先,new通过调用operator new函数分配足够的内存来存储指定类型的对象。这个函数的默认实现使用malloc来分配内存,但可以被重载。
  • 对象构造:分配内存后,new在分配的内存上调用对象的构造函数来初始化对象。

来看看 operator new 的代码实现:

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void* __CRTDECLoperatornew(size_tsize)_THROW1(_STDbad_alloc)
{
	// try to allocate size bytes
	void* p;
	while ((p = malloc(size)) == 0)
		if (_callnewh(size) == 0)
		{
			// report no memory
			//如果申请内存失败了,这里会抛出bad_alloc类型异常
			staticconststd::bad_allocnomem;
			_RAISE(nomem);
		}
	return(p);
}

其实 operator new 就是通过对 malloc 的封装实现的,不过进行了改进,当对象为自定义类型时,会去调用它的构造函数,并且当开辟失败时,会抛出异常。

4.2 delete的工作流程

与new相对应,当你使用delete操作符时,C++会执行以下步骤:

  • 对象析构:首先,delete调用对象的析构函数来执行任何必要的清理工作。
  • 内存释放:析构函数调用后,delete通过调用operator delete函数释放对象占用的内存。这个函数的默认实现使用free来释放内存,但同样可以被重载。

看看 operator delete 的代码实现

/*
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)

operator delete 也是对 free 进行了封装,改进的就是:当释放对象为自定义类型时,会调用它的析构函数。

4.3 new T[N]和delete[ ] 的原理的原理

new T[N]

  • 1、调用operator new[ ]函数,在operator new[ ]函数中实际调用operator new函数完成N个对象空间的申请。
  • 2、在申请的空间上执行N次构造函数。

delete[ ]

  • 1、在空间上执行N次析构函数,完成N个对象中资源的清理。
  • 2、调用operator delete[ ]函数,在operator delete[ ]函数中实际调用operator delete函数完成N个对象空间的释放。

5.定位new(Placement new)

除了标准的new和delete,C++还提供了定位new(Placement new)的概念,允许在已分配的内存上构造对象。这在需要在特定位置构造对象,或者使用内存池时非常有用。

char buffer[sizeof(MyClass)];
MyClass* myObject = new (buffer) MyClass(); // 在buffer指定的内存上构造对象

// 使用定位new时,不应使用普通的delete来释放内存,因为内存不是通过new分配的
// 相反,应直接调用析构函数
myObject->~MyClass();

在实际的C++编程中,定位new操作符通常用于以下场景:

  • 需要在已分配内存空间的特定位置上构造对象。

  • 实现自定义的内存管理策略,如内存池等

内存池中会预先存放许多从堆中申请过来的空间,这些空间是已经分配好的,需要的时候可以直接用,因此定位new可以在此一展身手。


6. 常见面试题

6.1 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在释放空间前会调用析构函数完成
    空间中资源的清理

6.2 内存泄露

  • 内存泄漏:

内存泄漏是指因为疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。


  • 内存泄漏的危害:

长期运行的程序出现内存泄漏,影响很大,如操作系统、后台服务等等,出现内存泄漏会导致响应越来越慢,最终卡死。

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;
}


  • 内存泄漏分类
    在C/C++中我们一般关心两种方面的内存泄漏:

1、堆内存泄漏(Heap Leak)

堆内存指的是程序执行中通过malloc、calloc、realloc、new等从堆中分配的一块内存,用完后必须通过调用相应的free或者delete释放。假设程序的设计错误导致这部分内容没有被释放,那么以后这部分空间将无法再被使用,就会产生Heap
Leak。

2、系统资源泄漏

指程序使用系统分配的资源,比方套接字、文件描述符、管道等没有使用对应的函数释放掉,导致系统资源的浪费,严重可导致系统效能减少,系统执行不稳定。


  • 如何避免内存泄漏?
     1、工程前期良好的设计规范,养成良好的编码规范,申请的内存空间记住匹配的去释放。
     2、采用RAII思想或者智能指针来管理资源。
     3、有些公司内部规范使用内部实现的私有内存管理库,该库自带内存泄漏检测的功能选项。
     4、出问题了使用内存泄漏工具检测。

  • 内存泄漏非常常见,解决方案分为两种:
     1、事前预防型。如智能指针等。
     2、事后查错型。如泄漏检测工具。

7.总结:

C++的动态内存管理是一个强大的特性,允许开发者在运行时分配和释放内存。然而,这也带来了额外的责任,如内存泄漏、野指针和重复释放内存等问题。通过遵循最佳实践和利用C++提供的工具,如智能指针和标准库容器,开发者可以有效地管理内存,写出更稳定、更高效的代码。

在这里插入图片描述

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

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

相关文章

ES6 ~ ES11 学习笔记

课程地址 ES6 let let 不能重复声明变量(var 可以) let a; let b, c, d; let e 100; let f 521, g "atguigu", h [];let 具有块级作用域,内层变量外层无法访问 let 不存在变量提升(运行前收集变量和函数&#…

ZigBee学习——在官方例程上实现串口通信

Z-Stack版本为3.0.2 IAR版本为10.10.1 文章目录 一、添加头文件二、定义接收缓冲区三、编写Uart初始化函数四、编写串口回调函数五、函数声明六、函数调用七、可能遇到的问题(function “halUartInit“ has no prototype) 以下所有操作都是在APP层进行,也就是这个文…

[Python 安装]

进入Python的官方下载页面 http://www.python.org/download/ 然后进行软件的下载 下载好之后点击exe会出现安装界面,接着进行安装,选择安装路径。 运行Python 安装成功后,打开命令提示符窗口(winR,在输入cmd回车&#xf…

C程序训练:二分查找法的应用之2

本文来自:C程序训练:二分查找法的应用之2 在《C程序训练:二分查找法的应用》一文中介绍了利用二分查找计算某个区间中数的个数,本文介绍利用二分查找法计算数列中出现单个数字的位置。题目描述如下。 题目描述:一维整…

Python进阶--下载想要的格言(基于格言网的Python爬虫程序)

注:由于上篇帖子(Python进阶--爬取下载人生格言(基于格言网的Python3爬虫)-CSDN博客)篇幅长度的限制,此篇帖子对上篇做一个拓展延伸。 目录 一、爬取格言网中想要内容的url 1、找到想要的内容 2、抓包分析,找到想…

Netty源码系列 之 bind绑定流程 源码

Netty框架总览 Netty是一个基于NIO异步通信框架 Netty框架是由许多组件,优化的数据结构所构建成。 正是通过灵活的组件构建,优化后的数据结构,进而才能保证Netty框架面对高并发场景具有一定的能力 Netty相关组件 Netty重要的组件有&…

代码随想录算法训练营DAY14 | 二叉树 (1)

一、二叉树理论基础 1.存储方式 链式存储: 顺序存储: 2.二叉树标准定义(Java) public class TreeNode {int val;TreeNode left;TreeNode right;TreeNode() {}TreeNode(int val) { this.val val; }TreeNode(int val, TreeNode left, TreeNode right) {…

spring cloud stream

背景 主要解决不同消息中间件切换问题。实现不同中间件的代码解耦。 链接: 支持的中间件 后文使用kafka测试。 引入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-stream</artifactId></depende…

微服务介绍、使用 Nacos 实现远程调用以及 OpenFeign 的使用

1 微服务的概念 区别于单体项目 单体项目拆分成微服务项目的目标&#xff1a;高内聚、低耦合 拆分思路 纵向拆分&#xff1a;根据功能模块 横向拆分&#xff1a;抽取可复用模块 2 微服务拆分——远程调用 背景&#xff1a;微服务单一职责&#xff0c;每个服务只有自己的功能…

电脑没有声音是怎么回事?几招快速解决

当电脑突然失去声音&#xff0c;这可能成为一种令人烦恼的体验&#xff0c;尤其是在你期望享受音乐、观看视频或进行在线会议的时候。幸运的是&#xff0c;大多数时候&#xff0c;电脑没有声音的问题是可以迅速解决的。电脑没有声音是怎么回事&#xff1f;本文将为你介绍一些常…

老是抓不准现货白银实时报价怎么办?

现货白银的实时报价是不断变动的&#xff0c;投资者要了解当下的现货白银实时走势&#xff0c;并且依靠对实时报价的分析预判未来的趋势&#xff0c;这是不容易的&#xff0c;但是不是不能做到呢&#xff1f;也不是。因为市场不是横盘就是趋势&#xff0c;只要有趋势&#xff0…

零代码3D可视化快速开发平台

老子云平台 老子云3D可视化快速开发平台&#xff0c;集云压缩、云烘焙、云存储云展示于一体&#xff0c;使3D模型资源自动输出至移动端PC端、Web端&#xff0c;能在多设备、全平台进行展示和交互&#xff0c;是全球领先、自主可控的自动化3D云引擎。此技术已经在全球申请了专利…

.NET Avalonia开源、免费的桌面UI库 - SukiUI

前言 今天分享一款.NET Avalonia基于MIT License协议开源、免费的桌面UI库&#xff1a;SukiUI。 Avalonia介绍 Avalonia是一个强大的框架&#xff0c;使开发人员能够使用.NET创建跨平台应用程序。它使用自己的渲染引擎绘制UI控件&#xff0c;确保在Windows、macOS、Linux、An…

用云手机打造tiktok账号需要注意些什么?

随着tiktok平台的火热&#xff0c;越来越多的商家开始尝试更高效的tiktok运营方法。其中&#xff0c;tiktok云手机作为一种新科技引起了很多人的注意&#xff0c;那么用云手机运营tiktok需要注意些什么&#xff1f;下文将对此进行详细解析。 1. 不是所有的云手机都适合做tiktok…

跳过mysql密码并重置密码 shell脚本

脚本 目前只是验证了5.7 版本是可以的&#xff0c;8.多的还需要验证 以下是一个简单的Shell脚本&#xff0c;用于跳过MySQL密码设置并重置密码&#xff1a; #!/bin/bash yum install psmisc -y# 停止MySQL服务 sudo service mysqld stop# 跳过密码验证 sudo mysqld --skip-g…

通过 docker-compose 部署 Flink

概要 通过 docker-compose 以 Session Mode 部署 flink 前置依赖 Docker、docker-composeflink 客户端docker-compose.yml version: "2.2" services:jobmanager:image: flink:1.17.2ports:- "8081:8081"command: jobmanagervolumes:- ${PWD}/checkpoin…

分析伦敦银报价总失败?你试试这样

做伦敦银交易的投资者要先对伦敦银报价进行分析&#xff0c;但是有些投资者反映自己分析伦敦银报价总是失败&#xff0c;抓不住市场价格的变化趋势&#xff0c;为什么会这样呢&#xff1f;我们可以从以下这两个方面来考虑。 转变分析工具。为什么分析伦敦银报价总失败&#xff…

提升你的PHP开发效率:探索JetBrains PhpStorm 2022的全新特性

在当今快速发展的软件开发领域&#xff0c;选择一个强大且高效的开发工具对于提升开发效率、保证代码质量至关重要。对于PHP开发者来说&#xff0c;JetBrains PhpStorm一直是市场上最受欢迎的IDE之一。随着JetBrains PhpStorm 2022的发布&#xff0c;这款工具带来了一系列创新功…

蓝桥杯-求阶乘-python

问题描述 满足N!的末尾恰好有K个0的最小的N是多少&#xff1f; 如果这样的N不存在输出一1。 思路解析 末尾的0是由10产生的&#xff0c;而10是由质数2和5产生的 在求阶乘的过程中&#xff0c;只要是偶数就会有2&#xff0c;而5相对2更少&#xff0c;所以对于10的数量我们可以…

机器学习-梯度下降法

不是一个机器学习算法是一种基于搜索的最优化方法作用&#xff1a;最小化一个损失函数梯度上升法&#xff1a;最大化一个效用函数 并不是所有函数都有唯一的极值点 解决方法&#xff1a; 多次运行&#xff0c;随机化初始点梯度下降法的初始点也是一个超参数 代码演示 impor…