C++笔记:动态内存管理

文章目录

  • 语言层面的内存划分
  • C语言动态内存管理的缺陷
  • new 和 delete 的使用了解
    • 语法
    • new 和 delete 操作内置类型
    • new 和 delete 操作自定义类型
  • new 和 delete 的细节探究
  • new 和 delete 的底层探究
    • operator new 和 operator new[]
    • operator delete 和 operator delete[]
  • 显式调用构造函数:定位new
  • malloc/free和new/delete的区别

语言层面的内存划分

想要全面认识动态内存管理就要对 C/C++ 语言层面上的内存划分有一个简单的认识。

第一点:内存整体被划分为了多个区域,每个区域有各自的特点。
在这里插入图片描述

我们常遇到的有四个区域:栈区、堆区、数据段(静态区)、代码段(常量区)。

  1. 【栈区】
    又称 “ 堆栈”,栈内存的分配和释放是由处理器的指令集中的特定指令来执行的,由于硬件支持,所以效率很高,但是分配给栈的内存空间容量有限,并且是在编译时被编译器确定的,使用时超出容量限制就会引发 “ 栈溢出 ” 错误。
    栈区主要存放运行函数而分配的局部变量、函数参数、函数返回值等(主要是临时需求)。

  2. 【堆区】
    堆区内存容量比栈区大得多,而且堆区内存空间的分配和释放由程序员手动控制,如果程序员不手动释放,只会由操作系统在程序结束时释放;堆区内存空间的访问速度相对于栈区是比较慢的,主要用来满足程序员按需使用内存空间的需求(如算法、数据结构),堆区内存的分配与释放又被称为 “ 动态内存管理 ”

  3. 【数据段】
    数据段是用来存储全局变量、静态变量的数据的,该部分内存空间在程序结束后由操作系统释放。

  4. 【代码段】
    该区域是用来存放可执行代码和只读常量的。可执行代码指的不是我们写的 C/C++ 代码(硬盘),而是经过编译器编译后生成的二进制代码;只读常量,也叫 “ 字面值常量 ”,程序中有着相当多的数据,有一部分是被指定好了的,不希望被修改的数据,这部分内存空间就是用来存放这部分数据的。

第二点:内存区域具体如何划分取决于操作系统如何设计。

这里个人用了一段代码来验证上面提出的内存分布模型是否真实。

#include <iostream>
using namespace std;

int globalVal = 1;
int globalStaticVal = 1;

int main()
{
	static int staticVal = 1;

	int* ptr1 = (int*)malloc(sizeof(int) * 5);
	int* ptr2 = (int*)malloc(sizeof(int) * 5);
	int* ptr3 = (int*)malloc(sizeof(int) * 5);
	int* ptr4 = (int*)malloc(sizeof(int) * 5);
	int* ptr5 = (int*)malloc(sizeof(int) * 5);

	const char* pstr = "abcd";

	cout << "栈区:" << endl;
	printf("&ptr1: %p\n", &ptr1);
	printf("&ptr2: %p\n", &ptr2);
	printf("&ptr3: %p\n", &ptr3);
	printf("&ptr4: %p\n", &ptr4);
	printf("&ptr5: %p\n\n", &ptr5);


	cout << "堆区:" << endl;
	printf("ptr5: %p\n", ptr5);
	printf("ptr4: %p\n", ptr4);
	printf("ptr3: %p\n", ptr3);
	printf("ptr2: %p\n", ptr2);
	printf("ptr1: %p\n\n", ptr1);

	cout << "数据段:" << endl;
	printf("&globalVal: %p\n", &globalVal);
	printf("&globalStaticVal: %p\n", &globalStaticVal);
	printf("&staticVal: %p\n\n", &staticVal);
	
	cout << "代码段:" << endl;
	printf("pstr: %p\n", pstr);
	
	return 0;
}

下面是这份代码分别在【Linux】和【Windows】下的测试结果:

在这里插入图片描述

通过对比可以看到,Linux 下是完全符合上面提到的内存分布结构,但是 Windows 下就不太一样了,所以上面那张图片仅供参考,但是能够确定的是,操作系统确实会对内存结构进行分区,但是具体如何分区那就得看操作系统本身是如何设计的了

C语言动态内存管理的缺陷

C语言中提供了以下函数来进行动态内存管理:

// 向堆区申请一块连续的内存空间
void* malloc (size_t size);
// 向堆区申请一块连续的内存空间,并初始化为0
void* calloc (size_t num, size_t size);
// 重新调整申请的内存空间的大小
void* realloc (void* ptr, size_t size);
// 释放申请的连续空间
void free (void* ptr);

C++ 是在C语言的基础上发展来的而且C++也完全兼容C语言的动态内存管理方式,但是随着C++的发展,特别是 “ 类与对象 ” 的引入之后,C语言动态内存管理的方式在有些就显得无能为了,而且使用起来比较麻烦。

以下面这个简单例子来进行一个演示:
在这里插入图片描述
代码中使用 malloc 函数向内存申请了一个块大小为 sizeof(A) 空间,然后用指针变量 p1 来接收空间的地址,然后问题来了,我们该如何初始化这块空间?

首先,从对比来看,mallocfree 是不会调用构造函数和析构函数的;同时,C++语法也说明了构造函数是实例化对象时由编译器自动调用的,无法通过 p1->A() 的方式直接显式调用。

其次,直接访问成员(p1->_a)这一方法也行不通,类的成员为私有属性,无法直接访问,而且类成员设置为私有属性本身就是为了数据安全所考虑的,改成公有就有点本末倒置。

这么看下来,好像就只能额外的设计两个函数分别用来代替构造和析构,但是这么设计的话构造和析构的语法设计就显得没有价值了。因此,可以看到 malloc 和 free 不方便解决C++动态申请的自定义类型对象的初始化问题

为了解决这一缺陷,C++设计了新的动态内存管理的方式,即 newdelete

new 和 delete 的使用了解

语法

new 用于在动态内存中分配一个对象,并返回对象的指针

// 分配单个对象
type* pointer = new type;

// 分配对象数组
type* arrayPointer = new type[size];

注意:
new 会分配一个单独的对象,并返回指向该对象的指针,new[] 会分配一个对象数组,并返回指向第一个对象的指针。

delete用于释放通过new分配的对象或对象数组

// 释放单个对象
delete pointer;

// 释放对象数组
delete[] arrayPointer;

注意:
delete 用于释放 new 申请的空间,delete[] 用于释放 new[] 申请的空间,二者一定要配对使用,不然容易出现错误。

new 和 delete 操作内置类型

newdelete 对于内置类型对象的申请与释放上与 mallocfree 没有任何区别,以整型为例,其他类型操作也是一样的:

#include <iostream>
using namespace std;

int main()
{
	// 动态申请一个int类型的空间
	int* p1 = new int;
	
	// 动态申请一个int类型的空间并初始化为10
	int* p2 = new int(10);
	
	// 动态申请10个int类型的空间
	int* p3 = new int[10];

	// 动态申请10个int类型的空间并初始化前3个元素
	int* p4 = new int[10]{ 1, 2, 3 };
	
	delete p1;
	delete p2;
	delete[] p3;
	delete[] p4;

	return 0;
}

new 和 delete 操作自定义类型

在自定义类型上,new 的作用为:先申请空间 + 后调用构造;delete 的作用为:先调用析构 + 后释放空间。

new 调用构造有三种方式,最方便的是第三种:
在这里插入图片描述

new 和 delete 的细节探究

#include <iostream>
using namespace std;

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = new int[capacity];
		_size = 0;
		_capacity = 4;
	}

	~stack()
	{
		delete[] _a;
		_size = _capacity = 0;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	stack* p1 = new stack;
	delete p1;
	
	stack* p2 = new stack[10];
	delete[] p2;
	return 0;
}

new/delete 单个对象的详细过程:
在这里插入图片描述

new/delete 对象数组的详细过程:
在这里插入图片描述

从上面的过程分析中也说明了,为什么 new-deletenew[]-delete[] 一定要配对使用,否则可以会因为找不到空间正确的起始地址而引发错误。

new 和 delete 的底层探究

new 和 delete 是用户进行动态内存申请和释放的操作符

  • new 在底层调用 operator new 全局函数来申请空间,new[] 在底层调用 operator new[] 全局函数来申请空间;
  • delete 在底层通过 operator delete 全局函数来释放空间,delete[] 在底层通过 operator delete[] 全局函数来释放空间;

operator new 和 operator new[]

这是一个C++标准库中 operator new 的示例实现:

void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
	// 尝试分配 size 字节的内存
	void* p;
	while ((p = malloc(size)) == 0)
	// 如果分配失败,尝试调用 new_handler 函数处理,即_callnewh
	if (_callnewh(size) == 0)
	{
		// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
		static const std::bad_alloc nomem;
		_RAISE(nomem);
	}
	return (p);
}

需要注意的是,这段代码中包含了一些平台特定的宏和定义,例如__CRTDECL_THROW1_RAISE等。这些宏和定义可以在特定的编译环境中提供一些额外的功能或兼容性支持。

虽然这个函数实现的细节个人目前也没有全部搞懂(异常处理还未学习),但是有一点可以肯定的就是 operator new 复用了 malloc,也就是说,operator new 实际上是通过调用 malloc 来向内存申请空间的,如果空间申请失败,就进行异常处理。

operator new[] 则是对 operator new 的再封装:

void* operator new[](size_t size) THROW1(std::bad_alloc)
{
    // 调用 operator new 来分配内存
    return operator new(size);
}

operator delete 和 operator delete[]

如果说 operator new 封装了 malloc 那么 operator delete 就是封装了 free

// free的实现
#define free(p) _free_dbg(p, _NORMAL_BLOCK)

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

在C语言的底层,free 本质上是一个宏函数,而 operator delete 最终是通过 free 来释放空间的(虽然除了调用 free 外还进行了很多操作)。

operator new[] 类似,operator delete[] 也是对 operator delete 的再封装。

以上的代码示例仅供参考,函数的具体实现依据依据平台而定。

显式调用构造函数:定位new

关于构造函数和析构函数,其实有个很特别的现象:

stack* p = (stack*)malloc(sizeof(stack));
// error: p->stack();
p->~stack();

指针变量p无法直接显式调用构造函数却能够显式调用析构函数,是不是很神奇?

根据这个现象来猜测,C++中应该有某种方式来显式调用构造函数,这个设计就是 “ 定位new ”。

【定位new表达式的作用】

在已分配的原始内存空间中调用构造函数初始化一个对象。

【定位new表达式的语法格式】

// 1
new (place_address) type
// 2
new (place_address) type(initializer-list)

place_address必须是一个指针,initializer-list是类型的初始化列表

【定位new表达式的使用场景】

定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。

关于内存池,目前了解不多,所以就不写在笔记里了。

【new 和 delete 的再认识】

#include <iostream>
using namespace std;

class stack
{
public:
	stack(int capacity = 4)
	{
		_a = new int[capacity];
		_size = 0;
		_capacity = 4;
	}

	~stack()
	{
		delete[] _a;
		_a = nullptr;
		_size = _capacity = 0;
	}

private:
	int* _a;
	int _size;
	int _capacity;
};

int main()
{
	// operator new + 定位new -> 承担 new 的作用
	stack* p = (stack*)operator new(sizeof(stack));
	new(p)stack(10);	// 如果构造有参数需要传参
	
	// 析构 + operator delete -> 承担 delete 的作用
	p->~stack();
	operator delete(p);

	return 0;
}

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

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

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

相关文章

体验官分享 | 用户眼中的OK3588-C开发板究竟有多优秀?

编者荐语&#xff1a;飞凌嵌入式今年共发起了5期【产品体验官】活动&#xff0c;让更多热爱嵌入式的朋友免费体验到了自己感兴趣的产品&#xff0c;飞凌嵌入式也收获了很多宝贵的建议。活动期间体验官们创作了许多优质的体验报告&#xff0c;今天小编就与大家分享一篇来自体验官…

Django讲课笔记01:初探Django框架

文章目录 一、学习目标二、课程导入&#xff08;一&#xff09;课程简介&#xff08;二&#xff09;课程目标&#xff08;三&#xff09;适用人群&#xff08;四&#xff09;教学方式&#xff08;五&#xff09;评估方式&#xff08;六&#xff09;参考教材 三、新课讲授&#…

SAP UI5 walkthrough step2 Bootstrap

我的理解&#xff0c;这就是一个引导指令 1.我们右键打开命令行--执行 ui5 use OpenUI5 2.执行命令&#xff1a;ui5 add sap.ui.core sap.m themelib_sap_horizon 执行完之后&#xff0c;会更新 yaml 文件 3.修改index.html <!DOCTYPE html> <html> <head&…

Java安全之Commons Collections6分析

CC6分析 import org.apache.commons.collections.*; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; impo…

[⑧ADRV902x]: Digital Pre-Distortion (DPD)学习笔记

前言 DPD 数字预失真技术&#xff0c;是一种用于抑制功率放大器非线性失真的方法。 它通过在信号输入功率放大器&#xff08;PA&#xff09;之前插入一个预失真模块&#xff0c;对输入信号进行适当的调制&#xff0c;以抵消功率放大器引起的非线性失真&#xff0c;使功率放大器…

线上盲盒扭蛋机,开启潮玩新玩法

盲盒近几年非常火爆&#xff0c;因其不确定性、随机性吸引着盲盒爱好者&#xff0c;引起了潮玩文化风潮。扭蛋机是盲盒的一种新抽取模式&#xff0c;线上扭蛋机小程序在具有盲盒的优势外&#xff0c;还具有较大吸引力&#xff0c;用户参与率较高&#xff0c;这也使得扭蛋机成为…

C++_命名空间(namespace)

目录 1、namespace的重要性 2、 namespace的定义及作用 2.1 作用域限定符 3、命名空间域与全局域的关系 4、命名空间的嵌套 5、展开命名空间的方法 5.1 特定展开 5.1 部分展开 5.2 全部展开 结语&#xff1a; 前言&#xff1a; C作为c语言的“升级版”&#xff0c;其在…

西南科技大学C++程序设计实验十一(泛型程序设计与C++标准模板库)

一、实验目的 1. 掌握泛型程序设计概念; 2. 掌握vector、deque、list容器使用方法; 3.了解set、map容器使用方法。 二、实验任务 1.分析完善以下程序,理解vector容器使用方法: #include <iostream> __#include <vector>_______ //补充vector模板头文件 …

(C语言实现)高精度除法 (洛谷 P2005 A/B Problem II)

前言 本期我们分享用C语言实现高精度除法&#xff0c;可通过该题测试点我点我&#xff0c;洛谷 p2005。 那么话不多说我们开始吧。 讲解 大家还记不记得小学的时候我们是怎么做除法的&#xff1f;我们以1115为例。 我们的高精度除法也将采用这个思路进行&#xff0c;分别用两…

selenium 解决 id定位、class定位中,属性值带空格的解决办法

一、前置说明 selenium遇到下面这种元素&#xff1a; <th id"demo id" class"value1 value2 value3 ">1、虽然id一般不会有空格&#xff0c;但是前端错误的这种写法(如下图)&#xff0c;会造成使用id定位不到元素&#xff0c;如&#xff1a; find…

Windows本地如何添加域名映射?(修改hosts文件)

1. DNS(域名系统) Domain Name System(域名系统)&#xff1a;为了加快定位IP地址的速度, 将域名映射进行层层缓存的系统. 目的&#xff1a;互联网通过IP&#xff08;10.223.146.45&#xff09;定位浏览器建立连接&#xff0c;但是我们不易区别IP&#xff0c;为了方便用户辨识I…

SAP 后继物料简介

后继物料(Discontinued Part)是SAP系统提供的一项用于物料继承与物料永久性替换的功能。在企业的日常生产业务中,经常会出于技术原因或成本原因进行大批量的物料替换或物料升级。比如说,企业可以用一种可靠性更高的组件替换先前使用的组件,或者出于节省成本的目的,使用一…

Google Gemini Pro 测试

谷歌新发布了Gemini模型&#xff0c;说是吊打GPT4&#xff0c;上手使用了下&#xff0c;感觉一般&#xff0c;没有感觉到预期的吊打。下面是与Bard的对话。

条码生成器与Zint使用

文章目录 目的条形码zint支持条形码种类下载编译qt pro配置code保存条形码目的 1: 了解条形码数据理论知识 2: 了解zint第三方库相关, 如何编译引用到项目中 条形码 条形码(Barcode)一维码 和二维码(QR code)都是用于存储信息的图形化表示方式,通常应用于商品标识、库…

led护眼灯真的能护眼吗?热门口碑好的护眼台灯推荐

2023年我国青少年近视率为53.6%&#xff0c;其中6岁以下儿童近视率为14.5%&#xff0c;小学生近视率为36%&#xff0c;初中生近视率为71.6%&#xff0c;高中生近视率为81%&#xff0c;大学生近视率为90%。 也就是孩子上了大学&#xff0c;十个里面有九个是近视&#xff0c;不戴…

【C语言】vfprintf函数

vfprintf 是 C 语言中的一个函数&#xff0c;它是 fprintf 函数的变体&#xff0c;用于格式化输出到文件中。vfprintf 函数接受一个格式化字符串和一个指向可变参数列表的指针&#xff0c;这个列表通常是通过 va_list 类型来传递的。vfprintf 函数的主要用途是在需要处理不定数…

【文件上传系列】No.1 大文件分片、进度图展示(原生前端 + Node 后端 Koa)

分片&#xff08;500MB&#xff09;进度效果展示 效果展示&#xff0c;一个分片是 500MB 的 分片&#xff08;10MB&#xff09;进度效果展示 大文件分片上传效果展示 前端 思路 前端的思路&#xff1a;将大文件切分成多个小文件&#xff0c;然后并发给后端。 页面构建 先在页…

机器学习实验二:决策树模型

系列文章目录 机器学习实验一&#xff1a;线性回归机器学习实验二&#xff1a;决策树模型机器学习实验三&#xff1a;支持向量机模型机器学习实验四&#xff1a;贝叶斯分类器机器学习实验五&#xff1a;集成学习机器学习实验六&#xff1a;聚类 文章目录 系列文章目录一、实验…

公众号提高数量

一般可以申请多少个公众号&#xff1f;目前公众号申请数量的规定是从2018年底开始实施的&#xff0c;至今没有变化。规定如下&#xff1a;1、个人可以申请1个个人主体的公众号&#xff1b;2、企业&#xff08;有限公司&#xff09;可以申请2个公众号&#xff1b;3、个体户可以申…

数组指针与函数指针

欢迎关注博主 Mindtechnist 或加入【智能科技社区】一起学习和分享Linux、C、C、Python、Matlab&#xff0c;机器人运动控制、多机器人协作&#xff0c;智能优化算法&#xff0c;滤波估计、多传感器信息融合&#xff0c;机器学习&#xff0c;人工智能等相关领域的知识和技术。关…