【C++深入浅出】C/C++内存管理(教你如何new到对象)

news2024/11/20 14:19:55


一. 前言

        前面我们学习了有关C++类和对象的知识,学会了如何构建一个完整的类,这些类都是存储在栈空间上的。在C语言中,我们不仅可以在栈上定义变量,也可以对上的空间进行管理,在接下来的几期中,我们的目标就是学会C++中是如何进行内存管理的

        没有对象的兄弟们都看过来啦,接下来的内容就是教你如何new一个对象出来,学习完本章节内容,保你们人人都有对象,好好看好好学

        话不多说,开整!!!

二. C/C++的内存分布

        在正式学习之前,我们先来看一下如下的示例代码:

#include<stdlib.h>
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);
}

问:你能不能指出以上每个变量在内存中存储的位置?如果这个变量是个指针,那指针又是指向内存中的哪块区域?请画图分析

        我们知道,在C/C++中,内存被分为栈区、堆区、静态区、字符常量区不同的区域,每个区域存储的内容互有差别,具体可以回顾往期:

C语言地址空间icon-default.png?t=N7T8http://t.csdn.cn/P5rkL        由此我们很容易可以看出,像globalVar这些全局变量是放在静态区(数据段)中,像num1这种非静态局部变量是存放在栈区的,像"abcd"这些字符串常量是存放在字符常量区(代码区)的,而像ptr1这些则是指向由malloc函数在堆区上所申请的空间。画图分析如下:

三. C语言内存管理方式

        在C语言中,我们通常使用mallocrealloccalloc来进行动态内存管理。它们的函数原型和基本功能如下所示:

函数原型功能说明
void* malloc(unsigned int num_bytes)

上动态申请一段num_bytes字节的空间,并返回这段空间的首地址,空间内的值不进行初始化,是随机值

void *calloc(size_t n, size_t size)上动态申请n个size字节的空间,和malloc不同的是,空间内的值会被初始化为0
void realloc(void *ptr, size_t new_Size)和上面两个函数不同,realloc主要是对已申请的内存空间进行扩容。ptr为指向原来空间的指针,new_size为扩容后内存空间的大小

        我们来看个小栗子:

void Test()
{
	int* p1 = (int*)malloc(sizeof(int)); //申请一个整形大小的空间,不进行初始化
	free(p1); //释放空间
	
	int* p2 = (int*)calloc(4, sizeof(int)); //申请4个整形大小的空间,初始化为0
	int* p3 = (int*)realloc(p2, sizeof(int) * 10); //对p2指向的空间进行扩容,扩容后的空间大小为10个整形的空间
	// 这里需要free(p2)吗?
	free(p3);
}

上面的p2还需要进行free()释放吗?答案是不用的。因为我们在之后对p2进行了扩容操作,而扩容分为原地扩容异地扩容

假如进行的是原地扩容,那么p2和p3指向的都是同一段空间,这时对p3进行释放就相当于对p2进行释放,如果此时我们又对p2进行了释放,则相当于对同一段空间进行多次释放,程序会崩溃

还有一种可能就是原空间所在分区后面剩余的空间不够了,此时需要进行异地扩容,那么系统就会在新的区域开辟一段空间,然后再将旧空间的数据拷贝下来,接着会自动释放旧空间,最后返回新空间的地址。

由此可见,对于p2,我们无需对其进行free()释放。

四. C++内存管理方式

        C++全面兼容C语言,故在C语言中进行动态内存管理的方式依然可以继续沿用。但在一些特殊场合使用C语言的方式无法达到目的;而且使用起来比较麻烦,需要进行各种函数调用、传参,因此C++提出了新的内存管理方式:通过newdelete操作符进行动态内存管理。

4.1 new/delete内置类型

        使用new和delete操作内置类型,基本上和malloc与free没什么区别,使用方法如下:

注意:new和deletenew[]和delete[]需要匹配进行使用,前者用于申请和释放单个元素空间,后者用于申请和释放连续的空间,不要混用,否则可能引发问题。

4.2 new/delete自定义类型

        new/delete操作符和malloc/free函数最大的区别体现在申请自定义类型中。使用new申请自定义类型的对象会自动调用构造函数,同理,使用delete释放自定义类型对象则会自动调用析构函数。下面我们来验证一下

class A
{
public:
	A(int x)
		:num(x)
	{
		cout << "A(int x)" << endl;
	}
	~A()
	{
		cout << "~A()" << endl;
	}
private:
	int num;
};
int main()
{
	cout << "测试new/delete----->" << endl;
	A* ptr1 = new A(3);
	delete ptr1;
	cout << "测试malloc/free---->" << endl;
	A* ptr2 = (A*)malloc(sizeof(A));
	free(ptr2);
	return 0;
}

五. operator new与operator delete函数

        上面我们介绍的new和delete是两个用户进行动态管理内存的操作符。而在系统中,C++还给我们提供了两个全局函数operator newoperator delete,注意,这两个函数不是运算符重载!!!new操作符在底层是调用operator new全局函数来申请空间,delete操作符在底层是通过
operator delete全局函数来释放空间。

        operator new函数在底层实际上也是通过malloc来申请空间的,当空间申请成功时直接返回,如果空间申请失败并且用户没有指定应对措施,就会抛出异常。而operator delete函数底层则是通过free来释放空间。

由此可见,new操作符在空间申请失败时默认会抛出异常,而malloc函数则是返回空指针 。

六. new和delete的实现原理

6.1 对于内置类型

        针对内置类型来说,new和delete实际和malloc和free基本类似。二者只有以下两处不同:

  1. 我们是通过new/delete申请/释放单个元素,通过new[]/delete[]申请/释放连续空间
  2. 使用new和new[]申请空间时,如果申请失败则会抛出异常,而malloc则是返回空指针

6.2 对于自定义类型

        而针对自定义类型来说,new和delete会自动调用构造和析构函数,它们的实现方式如下:

  • new 的原理

        1、底层调用operator new函数申请空间

        2、在申请的空间上执行构造函数,完成对象的构造

  • delete 的原理

         1、先在已申请的空间上执行析构函数,完成对象中资源的清理工作

         2、然后调用operator delete函数释放该对象的空间

  • new T[] 的原理

        1. 底层调用operator new[]函数,在operator new[]中调用operator new函数完成N个对
象空间的申请
        2. 在申请的空间上执行N次构造函数

  • delete[] 的原理

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


七. 定位new表达式

        在C++中是不允许通过对象显示调用构造函数的,只允许我们显式地调用析构函数,如下

int main()
{
	A* ptr = (A*)operator new(sizeof(A));  //使用operator new函数开辟空间,注意operator new不会自动调用构造函数
	ptr->A(10); //通过对象显式调用构造函数,不允许
	ptr->~A(); //通过对象显式调用析构函数,允许
	return 0;
}

         可是有时候我们是需要对已分配的内存空间调用构造函数进行初始化的,例如使用内存池中的空间时。由于内存池分配出的内存并没有进行初始化,故如果是自定义类型的对象,则需要显式调用构造函数对内存池的空间进行初始化,此时就需要用到我们的定位new表达式了。

        定位new的使用格式如下:

        new  (place_address)  type (initializer-list)

        其中place_address必须是个指针,而initializer-list就是传递给构造函数的初始化列表,具体使用方法如下:

// 定位new的使用
int main()
{
	//p1现在指向的只是与A对象相同大小的一段空间,并没有调用构造函数初始化对象
	A* p1 = (A*)malloc(sizeof(A));
	new(p1)A(10); //使用定位new显式调用构造函数
	p1->~A(); //显式调用析构函数
	free(p1);

	//operator new底层也是用malloc实现的,只是开辟了空间,并没有调用构造函数
	A* p2 = (A*)operator new(sizeof(A)); 
	new(p2)A(10); //使用定位new显式调用构造函数
	p2->~A();
	operator delete(p2);
	return 0;
}

我们可以发现,operator new再加上我们的定位new之后就相当于new操作符。operator new函数先申请空间,然后再使用定位new调用构造函数进行初始化。

八. malloc/free和new/delete的区别

        讲了这么多,我们也知道了如何在C++中new到一个对象了 最后我们就来总结一下C语言和C++动态申请的区别叭,作为找到对象之后的祝福叭

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

以上,就是本期的全部内容啦🌸

制作不易,能否点个赞再走呢🙏

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

idea + Docker-Compose 实现自动化打包部署(仅限测试环境)

一、修改docker.service文件&#xff0c;添加监听端口 vi /usr/lib/systemd/system/docker.service ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock重启docker服务 systemctl daemo…

一个进程最多可以创建多少个线程基本分析

前言 ​话不多说&#xff0c;先来张脑图~ linux 虚拟内存知识回顾 虚拟内存空间长啥样 在 Linux 操作系统中&#xff0c;虚拟地址空间的内部又被分为内核空间和用户空间两部分&#xff0c;不同位数的系统&#xff0c;地址空间的范围也不同。比如最常见的 32 位和 64 位系统&…

四大特性模块(module)

module的动机 C20中新增了四大特性之一的模块(module)&#xff0c;用以解决传统的头文件在编译时间及程序组织上的问题。 modules 试图解决的痛点 能最大的痛点就是编译慢, 头文件的重复替换, 比如你有多个翻译单元, 每一个都调用了 iostream, 就得都处理一遍. 预处理完的源…

muduo异步日志库

文章目录 一、日志库模型参考 一、日志库模型 muduo日志库是异步高性能日志库&#xff0c;其性能开销大约是前端每写一条日志消息耗时1.0us~1.6us。 采用双缓冲区&#xff08;double buffering&#xff09;交互技术。基本思想是准备2部分buffer&#xff1a;A和B&#xff0c;前…

英语——分享篇——每日200词——2601-2800

2601——resistant——[rɪzɪstənt]——adj.抵抗的——resistant——resi热死(拼音)st石头(拼音)ant蚂蚁(熟词)——热死了石头上的蚂蚁还在抵抗——The body may be less resistant if it is cold. ——天冷时&#xff0c;身体的抵抗力会下降。 2602——prospect——[prɒspe…

ubuntu安装后配置

基础配置 查看当前Ubuntu 的版本号 cat /etc/issue 更新软件源&#xff1a; 可以不用再复制黏贴内容了&#xff0c;直接找到Software Updates&#xff0c;Download from 选择china下的阿里云&#xff0c;然后reload就可以了。 更新软件 sudo apt-get update sudo apt-get u…

算法__中缀表达式转后缀表达式

文章目录 概念算法中缀转后缀案例讲解 后缀算值案例讲解 概念 中缀表达式就是日常生活中遇到的运算表达式&#xff0c;例如a*(b-c)&#xff1b; 后缀表达式则是另一种运算表达式&#xff0c;其特点在于运算符在对象后&#xff0c;且表达式中没有括号&#xff0c;例如abc-* 算…

DOM4J解析.XML文件

<?xml version"1.0" encoding"utf-8" ?> <books><book id"SN123123413241"><name>java编程思想</name><author>华仔</author><price>9.9</price></book><book id"SN1234…

发票识别神器推荐,告别繁琐手动录入,轻松管理财务

随着数字化时代的到来&#xff0c;越来越多的企业和个人开始寻求自动化处理各种繁琐任务的方式。其中&#xff0c;发票识别就是一个常见且具有挑战性的任务。发票识别是指将纸质或电子形式的发票转化为结构化的数据&#xff0c;以便进一步处理和分析。在过去&#xff0c;人工进…

【Qt QML】Qt Linguist使用方法

1. 简介 应用开发过程中&#xff0c;有时候需要翻译成多种语言&#xff0c;例如&#xff1a;将界面上所有中文翻译成英文&#xff0c;以适应国外市场。 针对多语言切换需求&#xff0c;Qt有一个Qt Linguist解决方案。 在所有需要翻译的字符串处使用qsTr()函数&#xff0c;Qt…

负采样:如何高效训练词向量

Negative Sampling 1.何为负采样 负采样是一种用于训练词嵌入模型的采样方法&#xff0c;特别适用于处理大规模词汇表的情况。负采样的目标是降低计算成本并改善模型的性能&#xff0c;同时有效地训练词向量。 2.为什么需要负采样 在传统的词嵌入模型中&#xff0c;如Word…

CleanMyMac X2024讲解及如何下载?

您是否曾经为Mac电脑的性能下降、存储空间不足而烦恼&#xff1f;是否希望有一个简单而高效的解决方案来优化您的Mac系统&#xff1f;那么&#xff0c;我向您介绍一款非常出色的工具&#xff1a;CleanMyMac X。它能够轻松处理这些问题&#xff0c;并让您的Mac恢复到最佳状态。 …

C++前缀和算法的应用:向下取整数对和 原理源码测试用例

本文涉及的基础知识点 C算法&#xff1a;前缀和、前缀乘积、前缀异或的原理、源码及测试用例 包括课程视频 题目 向下取整数对和 给你一个整数数组 nums &#xff0c;请你返回所有下标对 0 < i, j < nums.length 的 floor(nums[i] / nums[j]) 结果之和。由于答案可能会…

2023-10-17 LeetCode每日一题(倍数求和)

2023-10-17每日一题 一、题目编号 2652. 倍数求和二、题目链接 点击跳转到题目位置 三、题目描述 给你一个正整数 n &#xff0c;请你计算在 [1&#xff0c;n] 范围内能被 3、5、7 整除的所有整数之和。 返回一个整数&#xff0c;用于表示给定范围内所有满足约束条件的数…

深度学习推荐系统架构、Sparrow RecSys项目及深度学习基础知识

文章目录 &#x1f31f; 技术架构&#xff1a;深度学习推荐系统的经典技术架构长啥样&#xff1f;&#x1f34a; 一、深度学习推荐系统的技术架构&#x1f34a; 二、基于用户行为的推荐&#x1f34a; 三、基于多模态数据的推荐&#x1f34a; 四、基于知识图谱的推荐 &#x1f3…

CEC2023:基于自适应启动策略的混合交叉动态约束多目标优化算法(MC-DCMOEA)求解CEC2023(提供MATLAB代码及参考文献)

一、动态多目标优化问题 1.1问题定义 1.2 动态支配关系定义 二、 基于自适应启动策略的混合交叉动态多目标优化算法 基于自适应启动策略的混合交叉动态多目标优化算法&#xff08;Mixture Crossover Dynamic Constrained Multi-objective Evolutionary Algorithm Based on Se…

【Overload游戏引擎细节分析】Lambert材质Shader分析

一、经典光照模型&#xff1a;Phong模型 现实世界的光照是极其复杂的&#xff0c;而且会受到诸多因素的影响&#xff0c;这是以目前我们所拥有的处理能力无法模拟的。经典光照模型冯氏光照模型(Phong Lighting Model)通过单独计算光源成分得到综合光照效果&#xff0c;然后添加…

Modelsim无法生成LICENSE的问题

按照网上的破解教程&#xff0c;将mgls.dll和mgls64.dll属性都是去掉只读后&#xff0c;点击patch64_dll.bat文件生成LICENSE&#xff0c;发现在弹出的对话框中一直提示找不到其文件&#xff0c;无法正常生成LICENSE。 解决方法&#xff1a; 1.按winR键或者在电脑搜索界面中输…

C/C++ const相关 常量指针 常指针 常指针常量 顶层底层const

文章目录 前言const限定符初始化const引用指针和const顶层和底层const总结 前言 在看const相关内容的时候&#xff0c;对const的一些概念还存在部分疑惑&#xff0c;容易搞混&#xff0c;尤其是在变量声明这种情况下。 这篇博客就主要写一下const的相关。 const限定符 const主…

npm 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。

一、报错&#xff1a; npm : 无法将“npm”项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写&#xff0c;如果包括路径&#xff0c;请确保路径正确&#xff0c; 然后再试一次。 所在位置 行:1 字符: 1npm init -y~~~ CategoryInfo : ObjectNotFo…