【C++】动态内存管理:织梦寻优,在代码世界中编织高效内存的诗篇

【C++】动态内存管理:织梦寻优,在代码世界中编织高效内存的诗篇
在这里插入图片描述


文章目录

一、复习C/C++内存分布

在之前C语言的文章中我们详细讲解了C语言的动态内存管理,其中也简单学习了C/C++的内存分布,接下来我们就来通过一些练习来复习一下,C语言动态内存管理文章:【C语言】动态内存管理及相关笔试题
接下来我们先来看看之前学过的内存分布图,然后再来做题:
在这里插入图片描述
上面就是我们C/C++内存分布的图片了,在给出题目之前我还是提一下,其实这里我们所指的内存其实是语言层的理解,它其实是虚拟地址,而非实际的物理地址,是不是比较吃惊,到时候在学习Linux的时候我会给大家慢慢讲解,接下来我们继续复习,给出题目,如下:
//代码int globalVar =1;staticint staticGlobalVar =1;voidTest(){staticint staticVar =1;int localVar =1;int num1[10]={1,2,3,4};char char2[]="abcd";constchar* pChar3 ="abcd";int* ptr1 =(int*)malloc(sizeof(int)*4);free(ptr1);}
//选择题 选项 : A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)1. globalVar在哪里?____ 2. staticGlobalVar在哪里?____ 3. staticVar在哪里?____ 4. localVar在哪里?____ 5. num1在哪里?____ 6. char2在哪里?____ 7.*char2在哪里?___ 8. pChar3在哪里?____ 9.*pChar3在哪里?____ 10. ptr1在哪里?____ 11.*ptr1在哪里?____ 
在上面我们给出了一段代码,里面包含了各种变量,接下来我们就一 一来判断它们属于虚拟内存中的哪个区域:
1. 首先是第1问,问变量 globalVar 在哪个区域,首先我们要看出它是局部变量还是全局变量,很明显它只是一个普通的全局变量,所以它存放在静态区,选C
2. 接下来是第2问,问变量 staticGlobalVar,它是一个静态的全局变量,我们之前讲过,全局变量和静态变量都存放在静态区,这里的staticGlobalVar是他们的结合体,所以自然就是静态区,选C
3. 然后是第3问,问变量 staticVar 存放在哪里,根据上面的代码我们不难看出,staticVar是一个局部的静态变量,它的作用域是局部的,但是声明周期是全局,存放在静态区,还是选C
4. 接下来是第4问,问变量 localVar 在哪里,很明显它是一个局部变量,并且没有什么特别之处,所以它存放在Test函数的栈帧中,自然就在栈区了,选A
5. 接下来是第5问,问 num1 在哪里,和上面localVar不同的是,num1是一个数组,也存放在Test的栈帧中,属于栈区,选A
6. 接下来是第6问,从这里开始慢慢就有难度了,问 char2 在哪里,注意不要被迷惑了,char2是一个局部的数组,跟上面的num1数组没有什么不同,也属于栈区,选A
7. 接下来是第7问,问 * char2在哪里,我们都知道数组名其实就是首元素的地址,那么 *char2 就是数组的首元素了,也就是字符 ’ a ',整个数组都在栈区中。字符 ’ a ’ 是它的一部分,也属于栈区,选A
8. 接下来是第8问,这个题和下一题就有点小坑了,要注意解答, 它问 pChar3 在哪里,千万要记住,pChar3只是Test函数中的一个局部指针变量,应该属于栈区,选A,很多人把它误以为成常量区
9. 接下来是第9问,这个题问 *pChar3 在哪里,pChar3是一个局部指针变量,存放在栈区,而它指向的内容是一个常量字符串,所以对 pChar3 解引用拿到的是常量区的内容,属于常量区,选D
10. 接下来是第10问,这个题问 ptr1 在哪里,跟上面两个题很类似,这里的ptr1只是一个局部的指针变量。所以属于栈区,选A
11. 最后是第11问,这个题问 *ptr1 在哪里,虽然ptr1只是一个局部的指针变量,属于栈区,但是它指向的内容属于堆区,解引用后拿到的就是堆区的数据,所以选B
那么上面就是对前面11道题的文字分析了,你答对了几道题呢?接下来我们再结合下面的图片复习一下:
在这里插入图片描述
相信大家已经复习好了C/C++的内存分布了,接下来我们来简单复习一下C语言的动态内存管理

二、简单复习C语言动态内存管理

C语言的动态内存管理通常是通过几个函数来实现,我们这里将它们简单介绍一下就好了:
1. malloc:它的参数是我们要在堆上开辟的空间的大小,单位是字节,它返回一个void*的指针,需要我们接收时强转为对应的指针类型,如果开辟失败会返回空指针
2. calloc:它的参数与malloc不同,第一个参数是要开辟多少个变量,第二个参数是一个变量的大小,单位是字节,其次它会把所有变量都初始化为0,其它与malloc没什么区别
3. realloc:当我们使用malloc或者calloc开辟空间后,发现开辟的空间不够用了,于是我们可以使用realloc进行扩容,有可能是在之前空间的后面扩容,但是如果之前空间的后面不够用,realloc就会另外开辟一块空间,将之前的数据拷贝过来,然后释放掉之前的空间,它的第一个参数是要扩容的指针,第二个参数是扩容后的大小,单位是字节,如果扩容失败会返回空指针,否则返回扩容的空间的首地址,要注意的是如果第一个参数是空指针,那么此时它的作用和malloc一致
4. free:使用上面三个函数动态开辟的内存不会自动释放,需要我们手动进行释放,否则就会造成内存泄漏,也就是当前程序不使用这段空间了,但是又没有释放,就导致了内存的浪费,称为内存泄漏,解决办法就是使用free函数对空间进行释放
上面就是对C语言中动态内存管理的简单复习,接下来我们才进入今天的重点:C++中的动态内存管理是如何使用的

三、C++动态内存管理

C++中的动态内存管理仍然可以使用C语言的那几个函数,但是在某些场景有局限性,我们可以使用C++自己的内存管理方式:通过new和delete操作符进行动态内存管理

new与new[]

我们先来讲解new怎么使用,我们将内置类型和自定义类型进行分类讨论,首先是内置类型,如下:
//不初始化int* p1 =newint;//初始化int* p2 =newint(1);
我们可以看到new用起来非常方便,不用计算要开辟的空间的大小,也不用我们强转类型,只需要写出对应的类型,并且还能够进行随意的初始化,非常方便,当然,如果我们想开辟一个数组,我们就可以这样用,如下:
//不初始化int* arr1 =newint[3];//初始化(C++11支持)int* arr2 =newint[3]{1,2,3};
那么上面就是使用new开辟内置类型空间的使用方法,如果是单个变量就使用new,如果是数组就使用new[],接下来我们开始介绍new如何给自定义类型的对象开辟空间,如下:
classDate{public:Date(int year =2025,int month =1,int day =1):_year(year),_month(month),_day(day){}voidPrint(){ cout << _year <<"年"<< _month <<"月"<< _day <<"日"<< endl;}private:int _year;int _month;int _day;};intmain(){//不初始化(会调用默认构造,没有默认构造会报错) Date* d1 =new Date;//利用构造进行初始化 Date* d2 =newDate(2025,2,20); d1->Print(); d2->Print();return0;}
可以看到使用new来为自定义类型开辟空间非常好用,如果我们不传参初始化也会给我们调用默认构造,如果我们想传参初始化同样也可以调用构造,非常重要,而C语言的内存管理函数就做不到这一点,接下来我们看看程序的运行结果:
在这里插入图片描述
可以看到确实如我们预期所料,没有问题,至于我们想申请自定义类型数组,还是使用new[],也会自动调用默认构造,或者使用大括号显示传参,如下:
intmain(){//不初始化(自动调用默认构造,没有默认构造就报错) Date* arr =new Date[2];//利用构造初始化 Date* arr =new Date[2]{{2025,1,2},{2024,2,3}};return0;}

delete与delete[]

当我们使用完动态开辟的内存后,我们需要将它释放掉,C++提供了操作符delete让我们释放空间,首先是释放单个内置类型,如下:
//开辟int* p1 =newint(1);//释放delete p1;
使用方法如上,非常简单,接着要注意的是,上面的方法仅适用于开辟了一个对象,如果是数组需要使用delete[]进行释放,如下:
//开辟int* arr =newint[10];//释放delete[] arr;
上面就是使用delete和delete[]释放内置类型的教程,接下来我们学习使用delete和delete[]释放自定义类型,其实使用方法是一模一样的,只是要特殊说明的是,delete和delete[]释放自定义类型的空间时会调用这个自定义类型的析构,然后再释放空间,我们写个程序验证一下:
classA{public:A(int a =10):_a(a){ cout <<"调用了构造函数A()"<< endl;}~A(){ cout <<"调用了析构函数~A()"<< endl;}private:int _a;};intmain(){ A* arr =new A[10]; cout << endl;delete[] arr;return0;}
上面我们开辟了一个数组,数组的每个元素都是一个A类的对象,按照我们之前讲的,这里为了初始化这些对象会调用10次构造函数,然后我们最后释放arr的时候,会调用10次析构,我们来运行一下程序看看是不是这样的,如下:
在这里插入图片描述
可以看到确实如我们预期所料,这里调用了10次构造和析构,但是有的同学可能就有疑问了,delete就已经释放空间了,为什么还要继续调用析构呢?在上面的日期类和A类中可能还看不出来,接下来我们举个更加复杂一点的例子解析,如下:
classstack{public:stack(int n =10):_size(0),_capacity(10){ _arr =newint[10];}~stack(){if(_arr)delete[] _arr; _size = _capacity =0;}private:int* _arr;int _size;int _capacity;};intmain(){ stack* pst =new stack;delete pst;return0;}
在上面我们简单写了一个栈的构造与析构,并且使用new去申请了一个对象,随后直接将它释放掉,我们主要关注的是为什么delete和delete[]释放空间前要调用析构函数,我们画一个示意图,如下:
在这里插入图片描述
当我们开辟好空间后,pst指向了一个堆上的stack对象,然后stack中的_arr成员变量就指向了一个堆上的整型数组,我们来看看delete不自动调用stack的析构而直接释放stack对象会怎么样,如图:
在这里插入图片描述
可以看到这个时候程序就会出问题,因为我们只释放了stack对象的空间,_arr指向堆上的整型数组,这个数组还没有被释放,会引发内存泄漏,所以delete和delete[]释放对象时,必须先调用它的析构,然后再释放这个对象,如下图:
在这里插入图片描述
这下我们就真正理解到为什么释放自定义类型的对象时,要先调用它的析构,然后再释放这个对象了,因为这个对象的成员可能指向堆上的空间,所以要先调用析构释放这个成员指向的堆的空间,再释放掉整个对象

四、operator new与operator delete(重点)

new和delete是用户进行动态内存申请和释放的操作符,operator new 和operator delete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间

operator new

new在底层调用了函数operator new来申请空间,那么operator new是如何实现的呢?我们来看看源码中是如何写的,如下:
/* operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间 失败,尝试执行空间不足应对措施,如果改应对措施用户设置了,则继续申请,否则抛异常 */void* __CRTDECL operatornew(size_t size)_THROW1(_STD bad_alloc){// try to allocate size bytesvoid* p;while((p =malloc(size))==0)if(_callnewh(size)==0){// report no memory// 如果申请内存失败了,这里会抛出bad_alloc 类型异常staticconst std::bad_alloc nomem;_RAISE(nomem);}return(p);}
在这里插入图片描述
我们可以看到operator new在底层封装了malloc,那么这是为什么呢?直接用malloc就好了,为什么还要搞出来一个operator new,这是因为malloc没有成功申请到空间返回空指针,不太符合面向对象语言的方式,所以重新写一个函数operator new
这个函数封装了原本的malloc,封装的目的是为了更好地以面向对象的方式解决问题,operator new会判断malloc的返回值,如果不为空就直接返回了,如果为空进入判断就抛出异常,异常我们在后面的部分会讲到,是C++解决错误的方式,而不是依靠返回值,由于涉及到继承等其它知识,这里就简单说一下就好了
总之只需要知道为什么我们不直接使用malloc,而是将malloc封装成为operator new,目的就是为了让申请空间失败后抛出异常,从而让我们能够捕获

operator delete

那么我们知道了operator new的底层是malloc,那么有没有可能operator delete的底层是free呢?我们来看看源码,如下:
/* operator delete: 该函数最终是通过free来释放空间的 */voidoperatordelete(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;}
在这里插入图片描述
可以看到operator delete确实封装了free,delete就用它来释放空间,通过上述两个全局函数的实现知道,operator new 实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete 最终是通过free来释放空间的
operator new和operator delete这两个小点还是很重要的,涉及到了new和delete的底层,有时还会出现在面试中,当然,有了这些前置知识,我们就可以更好的学习后面的new和delete的原理了

五、new与delete原理

内置类型

如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new[]和delete[]申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回空指针

自定义类型

new的原理

1. 调用operator new函数申请空间(operator new的底层封装了malloc,根据malloc的返回值来决定是否抛出异常)
2. 在申请的空间上执行构造函数,如果没有传参那么调用默认构造,如果传参了就按照参数完成构造,最终完成对象的构造

delete的原理

  1. 在空间上执行析构函数,完成对象中资源的清理工作
  2. 调用operator delete函数释放对象的空间(operaotr delete底层封装了free)

new T[N]的原理

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

delete[]的原理

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

六、C++与C语言动态管理区别总结

1. malloc、calloc、realloc以及free是函数,new和delete是操作符
2. malloc需要计算开辟空间的大小,并且需要强转类型,new无需计算空间的大小,只需要写出对应的类型
3. new底层封装了operator new,operator new底层又封装了malloc,delete底层封装了operator delete,operator delete又封装了free
4. new会调用operator new来开辟空间,对于内置类型可以初始化,对于自定义类型可以调用对应的构造函数来构造对象,而malloc没有这些功能
5. delete在释放自定义类型的对象时,会调用对象的析构函数,再调用operator delete来释放空间
6. C语言判断是否出错依赖malloc的返回值,而new则是抛异常
那么今天C++的动态内存管理部分就到这里啦,希望大家有所收获,同时下一篇文章我们就会了解到泛型编程了,也就是模板,敬请期待吧!
bye~

Read more

libmd 实现详解:仓颉语言中的哈希算法库开发实践

libmd 实现详解:仓颉语言中的哈希算法库开发实践

libmd 实现详解:仓颉语言中的哈希算法库开发实践 前言 密码学哈希函数是现代信息安全的基石,广泛应用于数据完整性验证、数字签名、用户认证和数据安全存储等领域。在仓颉语言生态中,libmd库提供了完整的密码哈希算法实现,支持多种主流哈希算法,包括经典的MD2、MD4、MD5,以及SHA系列(SHA-1、SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/256)和RIPEMD-160等算法。同时,该库还提供了HMAC功能,支持消息认证码的生成,为数据提供了额外的安全保障。 本文将从库的设计思路、核心实现、技术挑战、性能优化等多个维度,深入解析libmd库的开发过程,为仓颉语言开发者提供库开发的实践参考。 一、库概述 1.1 项目背景 在软件开发的众多领域,数据完整性验证和安全性保障是至关重要的需求。哈希算法因其单向性、抗碰撞性和雪崩效应等特性,成为解决这些问题的理想工具。从文件校验到用户认证,从区块链技术到数字签名,哈希算法的应用无处不在。 libmd库旨在为仓颉语言提供一套完整、高效、易用的哈希算法解决方案,支持多种主流哈希算法,

By Ne0inhk
算法基础篇:(二十一)数据结构之单调栈:从原理到实战,玩转高效解题

算法基础篇:(二十一)数据结构之单调栈:从原理到实战,玩转高效解题

目录 前言 一、什么是单调栈?先打破 “栈” 的常规认知 1.1 单调栈的核心特性 1.2 如何实现一个单调栈? 实现单调递增栈 实现单调递减栈 1.3 核心操作解析:为什么要 “弹出元素”? 二、单调栈能解决什么问题?四大核心场景全覆盖 2.1 场景 1:找左侧最近的 “更大元素” 问题描述 解题思路 代码实现 测试用例验证 2.2 场景 2:找左侧最近的 “更小元素” 问题描述 解题思路 代码实现 测试用例验证 2.3 场景 3:找右侧最近的 “更大元素” 问题描述

By Ne0inhk
直流无刷电机FOC控制算法

直流无刷电机FOC控制算法

文章目录 * 1、FOC概述 * 1.1 FOC控制算法介绍 * 2、无刷电机 * 2.1 无刷电机介绍 * 2.2 无刷电机和永磁同步电机的区别 * 2.3 无刷电机的控制原理 * 2.3.1 无刷电机工作原理 * 2.3.2 直流无刷电机驱动原理 * 2.3.2.1 有感直流无刷电机六步换相驱动原理 * 2.3.2.2 直流无刷电机FOC控制原理 * 3、无刷电机FOC控制算法 * 3.1 FOC控制算法整体流程 * 3.2 FOC算法Clarke变换 * 3.2.1 Clarke变换公式推导 * 3.2.2

By Ne0inhk
Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战

Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 async_extension 的鸿蒙化适配指南 - 实现具备高级异步编排算法与流操作扩展的并发工具集、支持端侧复杂业务流的函数式处理实战 前言 在进行 Flutter for OpenHarmony 的大规模异步业务系统(如实时行情刷新、多源数据聚合)开发时,如何更优雅地处理 Future 的超时竞争、Stream 的防抖(Debounce)或复杂的并发队列控制?虽然 Dart async 包提供了基础功能,但 async_extension 进一步扩展了异步编程的边界,提供了更符合函数式范式的工具。本文将探讨如何在鸿蒙端构建极致、高效的异步处理链路。 一、原直观解析 / 概念介绍 1.1 基础原理 该库通过对 Dart 核心异步类的非侵入式扩展(Extensions)

By Ne0inhk