初识C++ · 内存管理

目录

1 C/C++的内存分布

2 C语言的内存管理

3 C++的内存管理

4 operator new 和 operator delete

5 定位new


1 C/C++的内存分布

语言不同,内存分布是相同的,对于局部变量都是放在栈上,全局变量都是放在静态区(数据段),动态开辟的都是从堆中开辟,const修饰的变量也是都放在常量区(代码段)

这里试试手:

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

globalVar在哪里?staticGlobalVar在哪里?staticVar在哪里? localVar在哪里?num1 在哪里?char2在哪里? *char2在哪里?pChar3在哪里?*pChar3在哪里? ptr1在哪里? *ptr1在哪里?

A.栈  B.堆  C.数据段(静态区)  D.代码段(常量区)

globalVar是全局变量,所以在静态区,C

StaticGlobalVar是被static修饰的变量,所以在静态区,C

StaticVar也是被static修饰的,所以在静态区,C

localVar是局部变量,所以在栈,A

num1是地址,在栈上,A;char 2也是地址,在栈上,

A;*char2是常量字符串的第一个字符,所以在常量区,D

pChar3是地址,栈上,A

*pChar3同*char2,在常量区,D

ptr1是地址,栈上,A

*ptr1是动态开辟的第一个空间的元素,堆上,B


2 C语言的内存管理

C语言中的内存是用函数的形式进行管理的,涉及的函数是malloc realloc calloc free,均被包含在头文件stdlib里面,malloc函数和calloc函数的最大区别就是是否初始化的问题,realloc是用来扩容,使用完会自动是空间,free是释放空间。

似乎看起来比较完美了,但是为什么C++有单独的内存管理方式呢?

这是因为:

int main()
{
	int* pa = (int*)malloc(sizeof(int) * 10);
	free(pa);
	return 0;
}

malloc等函数只能用来申请内置类型的空间,对于自定义类型就没有办法,顺便提一句,涉及内存管理的时候,最好注意有没有内存泄露的问题,内存泄露是慢性病,很严重的喔。

那么,C++就引入了不同的方式进行内存管理,其实C语言的内存管理的方式在C++里面也是可以使用的,但是局限性比较大,当我们学会后面的内存管理方式之后,就把malloc函数等放在一边吧


3 C++的内存管理

在C++中内存管理是使用new 和 delete来实现的,这两个都是C++中的标识符,和C语言不同的是C++实现内存管理不是用的函数。

首先,对于内置类型来说:

int* p1 = new int;

这写法等价于:

int* p1 = (int*)malloc(sizeof(int) * 1);

一下就简洁了,与calloc不同的是,这样不能进行初始化,那么要进行初始化可以在后面加括号:

int* p1 = new int(1);

通过解引用打印出来也是1,与calloc不同的是初始化可以任意初始化,calloc只能初始化为0。

那么开辟一个数组使用到的是方括号:

int* pa = new int[10];

这种写法是开辟了一个大小为40字节的数组,初始化的方式是使用花括号:

int* pa = new int[10]{1,2,3,4};

这里和数组是一样,如果不是初始完全的话,那么剩下的部分就会自动初始化为0。

既然是开辟空间,方括号里面的也可以是变量,同malloc函数一样,可以使用变量:

int n = 10;
int* pa = new int[n]{1,2,3};

当然,C++引入了new和delete可不是为了内置类型的,是为的自定义类型:
这里使用类A介绍:

class A
{
public:
	A(int n)
		:_a(n)
		,_b(n - 1)
	{
		cout << "int n" << endl;
	}
	A(int a, int b)
		:_a(a)
		,_b(b)
	{
		cout << "int a, int b" << endl;
	}
	~A()
	{
		_a = _b = -1;
		cout << "~A" << endl;
	}
private:
	int _a;
	int _b;
};

3.1 new

使用:

A* pA = new A;//开辟一个A
A* pB = new A(0);//对pB对象初始化
A* pC = new A[10];//开辟10个对象的空间
A* pD = new A[10]{1,2,3,{1,3}};//初始化对象

对于自定义类型来说,需要注意的是初始化其实使用了隐式类型转换,对于多参数的对象来说,就是花括号里面套一个花括号就可以了。

这里可能有个问题就是A* pB = new A(0)给的是匿名对象吗?并不是,因为匿名对象的生命周期只有这一行,后面就直接析构了,但是这里没有,所以不是匿名对象。

这是开辟空间的用法,你如果认为只有这么简单就错辣!

new比malloc高级在于,new不仅仅包含了malloc,我们逐一介绍,这里不卖关子,直说了就:
new = malloc + 抛异常 + 构造函数,其中malloc + 抛异常 = operator new,大概是可以这样理解的。

首先,new一个自定义类型的时候是会自动调用对应的构造函数的:

new一个的时候如果内存足够,就会调用对应的构造函数,光是能调用对应的构造函数,就可以节省很多事:

struct ListNode
{
	//struct ListNode* _next;
    ListNode* _next;
	int _val;
	ListNode(int val)
		:_next(nullptr)
		,_val(val)
	{}
};
void Func()
{
	ListNode* n1 = new ListNode(1);
	ListNode* n2 = new ListNode(2);
	ListNode* n3 = new ListNode(3);
	n1->_next = n2;
	n2->_next = n3;
	delete n1;
	delete n2;
	delete n3;
}

既然能调用构造函数,手搓链表的时候只能说太爽了,至于struct写不写看文件后缀,C++中里面的那个struct可以不用写。

int main()
{
    A* pa = new A(0);
    return 0;
}

我们再来看看这段代码的反汇编:

F11调试之后,跳转了几次来到了这里,就会发现,new的底层调用的其实是malloc,所以new是对malloc的一个封装,跳转之前我们还可以看到:

有operator new和A::A,先进入了operator new就会发现里面有我们熟悉的malloc,后面再调用了构造函数。

为什么说new用来手搓链表就很爽,因为我们不用没开辟一个就去assert一下,new里面的抛异常会帮我们解决;

因为抛异常是后面的知识了,这里简单介绍,在C语言中报错是通过返回错误码,比较暴力,在C++中有一种温和的报错方式就是抛异常,通过try - catch ,比如我一次性开辟很大的空间,系统分配的内存不够了,那么try-catch就会起作用,程序运行完毕会打印一行字,告诉你有错误,new里面的抛异常就是为了防止开辟空间不够,因为链表每次开辟一个节点就要检查是否为空,new底层中的malloc开辟之后,抛异常直接就帮忙解决了空指针的问题(因为malloc函数开辟失败返回的是空指针)。

总结来说就是:new = malloc + 抛异常 + 构造函数,new是对malloc的封装,对内置类型来说new和malloc没有区别,对自定义类型来说new不仅可以开辟空间,还可以抛异常并且调用对应的构造函数。

3.2 delete

同C语言,开辟了空间,使用完就要还给操作系统,C语言中使用的是free,在C++中使用的是delete,delete = operator delete+ 析构,而operator delete最终是通过free实现的:

这里当我们进入到operator delete的汇编就可以发现:

最后调用的其实就是free,这里是free的宏,即operator delete最终使用free释放。

delete的大致使用如下:

int main()
{
	A* pa = new A(0);
	A* pb = new A[10]{1,2,3,4};
	delete pa;
	delete[] pb;
	return 0;
}

对于pb来说,我们开辟了10个A类型的空间,也就是我们开辟了80个字节,delete就会释放这么多字节,但是!

当我们显式调用了析构函数之后,真的只开辟了80个字节吗?

在2019中,我们进入到operator new中就可以看到:

我们明明只要40个字节,却多开了4个字节,但是当我们注释掉析构函数之后,我们再去调试,就会发现size变成了40,也就是有没有显式调用析构函数会影响实际开辟的空间大小,这里我们再引入一个点:

C语言和C++中的内存管理混用可以吗?

用malloc函数开辟的我用delete,用new的我用free,你说可行吗?实际上对于内置类型来说是没有问题的,因为不会涉及多开空间的问题,也就不会涉及越界的问题,那么对于自定义类型来说:

当我们显式调用了析构函数,使用free就会出问题,其实不管是free还是delete都会出问题:

其中的缘由就是多开的4个空间是用来干嘛的,当显式调用析构函数之后,多开辟的空间就是用来记录有多少个空间需要被析构,是一个数字用来记录,那么就多开了一个整型,所以会多4字节,如果没有显式调用析构就不会,显式调用就相当于告诉编译器我有这么多空间需要销毁,你记得数数。

那么为什么会出现越界的问题?

因为开辟了多少空间,delete[] 就知道要销毁这么多空间,在delete[]对应的汇编中有一条语句是回退4个字节,所以delete[]操作的时候就不会出现越界的问题,对于free delete来说没有回退的指令就会出现越界的问题,这也就是为什么不要混用的原因,挺容易出现幺蛾子的。

delete是operator delete + 析构,那么有没有想过,析构的是哪里?operator delete的free是free的哪里呢?

这个问题是比较简单的,不卖管子了就,free是free掉指针指向的区域,析构是完成资源清理工作(如果有资源需要清理的话),有点像两个栈实现队列的那种说法。


4 operator new 和 operator delete

其实在上面new和delete的介绍已经把这两个介绍的差不多了,这里补充一些点即可。

operator new 和 operator delete是全局函数,不是C++中的标识符,我们通过它们的定义就可以知道 operator new 是调用的malloc实现的开辟空间,operator delete是调用的free来释放的空间,

既然是全局函数,意味我们可以显式的调用:

显式调用的情况,operator new和malloc函数的用法是一样的,都不能进行初始化,operator delete使用和free是一样的:

int main()
{
	int* p1 = (int*)operator new(sizeof(int) * 10);
	A* pa = (A*)operator new (sizeof(A) * 10);
	operator delete(p1);
	operator delete(pa);
	return 0;
}

它们主要的内容还是前面讲的。


5 定位new

定位new这里了解一下就行了:

使用格式: new (place_address) type或者new (place_address) type(initializer-list)

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

int main()
{
	A* p2 = (A*)operator new(sizeof(A));
	new(p2)A;
	A* p3 = (A*)operator new(sizeof(A));
	new(p3)A(1);
	return 0;
}

我们平常显式调用析构函数是没有问题,但是如果想显式调用构造函数就有问题了,构造函数是初始化用的,也就是一旦对象实例化了,就会自动的调用对应的构造函数,像上面的情景,指针指向一块空间,我们相对它进行初始化不能直接显式的调用构造函数,这里就需要用定位new了,格式如上,如果是多个对象,调用构造可以这样:

int main()
{
	A* p4 = (A*)operator new(sizeof(A) * 10);
	for (int i = 0; i < 10; i++)
	{
		new(p4 + i)A{ i };
	}
	A* p5 = (A*)operator new(sizeof(A) * 10);
	new(p5)A[5]{ 1,2,3,4,5 };
	return 0;
}

有两种初始化方法,比较推荐的是for循环,因为好控制,一旦空间大了,比如1000个,一个一个写显然是不可能的,所以一般用for循环。

定位new一般是用于内存池的情况,因为从内存池分配出来的空间都是没有初始化的,所以需要用到定位new,但是更多的细节放在后面介绍,是计算机网络的知识了。


感谢阅读!

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

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

相关文章

jvm重要参数可视化和线上问题排查

jvm重要参数可视化和线上问题排查 目标jvm参数分类(了解)运行时数据区相关的&#xff08;jdk1.8&#xff09;处理 OOM 相关的垃圾回收器相关的GC 日志记录相关的意义,默认值,调优原则&#xff08;重要&#xff0c; 待拆分&#xff09; 排查 OOM 流程 和 常见原因参考文章 目标 …

基于C语言中的类型转换,C++标准创造出了更加可视化的类型转换

目录 前言 一、 C语言中的类型转换 二、为什么C需要四种类型转换 三、C中新增的四种强制类型转换操作符以及它们的应用场景 1.static_cast 2.reinterpret_cast 3.const_cast 4.dynamic_cast 前言 在C语言中&#xff0c;如果赋值运算符左右两侧的类型不同&#xff0c;或者…

短视频矩阵系统贴牌---saas源头开发

一、短视频矩阵运营注意事项&#xff1a; 如&#xff1a;房产行业 短视频矩阵运营是一个系统化的项目&#xff0c;涉及多个平台和账号的管理&#xff0c;以及内容的创作、发布和优化等多个方面。 以下是短视频矩阵运营的注意事项文档的概要以及结果运营数据 一周持续运营量 二…

uni-app 多列picker切换列显示对应内容

html部分&#xff1a; <view class"uni-list"><view class"uni-list-cell"><view class"uni-list-cell-left">选择用户</view><view class"uni-list-cell-db"><picker mode"multiSelector"…

【JavaWeb】网上蛋糕商城后台-类目管理,退出

概念 本文讲解和实现类目管理和管理员的退出功能。 类目列表信息 点击类目管理&#xff0c;向服务器发送请求/admin/type_list 在servlet包中创建AdminTypeListServlet类&#xff0c;获得所有商品分类 package servlet;import model.Type; import service.TypeService;impo…

网站localhost和127.0.0.1可以访问,本地ip不可访问解决方案

部署了一个网站, 使用localhost和127.0.0.1加端口号可以访问, 但是使用本机的ip地址加端口号却不行. 原因可能有多种. 可能的原因: 1 首先要确认是否localhost对应的端口是通的(直接网址访问), 以及你无法访问的那个本机ip是否正确(使用ping测试)&#xff1b; 2 检查本机的防火…

堆的基本操作(c语言实现)

1.堆的基本操作 1.1定义堆 typedef int HPDataType;//堆中存储数据的类型typedef struct Heap {HPDataType* a;//用于存储数据的数组int size;//记录堆中已有元素个数int capacity;//记录堆的容量 }HP;1.2初始化堆 然后我们需要一个初始化函数&#xff0c;对刚创建的堆进行初…

软件测试开发之 职业发展必备 能力模型解析

为什么要了解能力模型 王阳明曾在《传习录》中提到过一个思想&#xff1a;以终为始。所谓“以终为始”&#xff0c;意味着在行动的开始阶段就要考虑到最终的目标和结果&#xff0c;以此来指导自己的行动和选择。那么如果我们想在自己的行业内获取好的职业发展&#xff0c;第一…

Meta更低的训练成本取得更好的性能: 多token预测(Multi-Token Prediction)

Meta提出了一种透过多token预测(Multi-token Prediction)来训练更好、更快的大型语言模型的方法。这篇论文的重点如下: 训练语言模型同时预测多个未来的token,可以提高样本效率(sample efficiency)。 在推论阶段,使用多token预测可以达到最高3倍的加速。 论文的主要贡献包括: …

2024年Delphi自学培训网络资源

概述 Delphi 是一种基于 Object Pascal 的面向对象编程语言。最初&#xff0c;Delphi 是作为构建 Windows 应用程序的工具而创建的&#xff0c;并于 1995 年发布。从那时起&#xff0c;这些技术向前迈出了一大步&#xff0c;Delphi也不例外。尽管第一个用 Delphi 编写的应用程…

Windows 10 中使用 Montreal-Forced-Aligner (MFA) 实现音频和文本强制对齐

文章目录 一、实现目标二、安装 Montreal-Forced-Aligner1、使用 Anaconda 虚拟环境2、修改默认下载路径3、安装 montreal-forced-aligner 及相关第三方包4、验证是否安装成功 三、下载声学模型和发音词典1、命令行方式下载2、手动方式下载 四、强制对齐1、准备音频及对应文本2…

docker学习笔记(三)搭建NFS服务实验

目录 什么是NFS 简单架构​编辑 一.搭建nfs服务器 二.新建共享目录和网页文件 三.设置共享目录 四&#xff1a;创建使用nfs共享目录的卷 五&#xff1a;创建容器使用nfs-web-1卷 六&#xff1a;测试访问 七&#xff1a;是否同步测试 什么是NFS NFS 服务器&#xff1a;ne…

人工智能将改变科研?从胰腺癌早筛到新药研发

去年底英国《自然》杂志刊文预测的2024年十大科学进展中&#xff0c;人工智能的进步和ChatGPT人工智能占据前两位。那么&#xff0c;人工智能对于科学而言&#xff0c;它的哪些成果将带来有益的发展&#xff1f;今天我们请知名科普作者张田勘来聊聊这个话题。 &#xff08;1&am…

万兆以太网MAC设计(13)主机与FPGA之间进行PING

文章目录 前言&#xff1a;一、ICMP校验和计算二、上板效果1、终端命令行1、wireshark捕捉 前言&#xff1a; 在上板尝试进行PING操作的时候&#xff0c;发现一直是请求超时的情况&#xff0c;结果排查发现是首部校验和没有计算的问题。在UDP层&#xff0c;我们不进行校验和是…

ReentrantReadWriteLock源码分析

ReentrantReadWriteLock是基于AQS实现的读写锁&#xff0c;读锁与读锁不互斥、读锁与写锁互斥、写锁与写锁互斥。 类的继承关系 AQS提供了共享和排它两种模式&#xff0c;acquire/release、acquireShared/releaseShared 是AQS里面的两对模板方法。写锁是排它模式基于acquire/…

Yii2 自动生成php代码

文档地址&#xff1a;入门&#xff08;Getting Started&#xff09;: 用 Gii 生成代码&#xff08;Generating Code with Gii&#xff09; - Yii 2.0 权威指南 - 文档 - Yii Framework 中文网 找到配置文件&#xff0c;以我的项目为例&#xff1a; 因为的是开启了路由美化所以访…

在线扭蛋机小程序:商家稳占市场的新突破口

近几年&#xff0c;扭蛋机进入了爆发期&#xff0c;动漫、游戏的发展更是推动了市场的发展&#xff0c;我国扭蛋机正在蓬勃发展中。 不过&#xff0c;在市场规模扩大下&#xff0c;扭蛋机行业的竞争力也在同时加大&#xff0c;企业商家需要在市场竞争中寻求发展新思路&#xf…

开源推荐榜【FunClip是一款完全开源、本地部署的自动化视频剪辑工具】

FunClip是一款完全开源、本地部署的自动化视频剪辑工具&#xff0c;通过调用阿里巴巴通义实验室开源的FunASR Paraformer系列模型进行视频的语音识别&#xff0c;随后用户可以自由选择识别结果中的文本片段或说话人&#xff0c;点击裁剪按钮即可获取对应片段的视频&#xff08;…

基于EBAZ4205矿板的图像处理:12图像二值化(阈值可调)

基于EBAZ4205矿板的图像处理&#xff1a;12图像二值化(阈值可调) 我的项目是基于EBAZ4205矿板的阈值可调的图像阈值二值化处理&#xff0c;可以通过按键调整二值化的阈值&#xff0c;key1为阈值加1&#xff0c;key4为阈值减1&#xff0c;key2为阈值加10&#xff0c;key5为阈值…

【AI+音视频总结】如何在几分钟内用智能工具摘取音视频精华?揭秘下一代学习和内容创作神器!

今天无意发现一个网站&#xff0c;可以一步到位完成AI音视频总结。 我之前对于音视频总结的步骤还是借助 工具下载 剪映来完成的。详情可以参考之前写的一篇文章 【AI应用】模仿爆款视频二次创作短视频操作步骤 。 这里介绍的网站是 BibiGPT 。 BibiGPT AI 音视频助理 - 它是…