C/C++内存管理
文章目录
零.前言
在C语言的学习中,我们了解到了动态开辟内存的两个重要的函数malloc以及free,C++在此基础上对动态内存的开辟以及空间的释放进行了简化,以期望更加满足面向对象编程的特点,本文将介绍C/C++的内存管理,C++中动态开辟内存的两个操作符以及它们底层的实现,并提出了内存池的概念,以及内存泄漏问题。
1.一张图展示内存分布
1.栈:非静态局部变量/函数参数/返回值等等,栈时向下增长的。
2.内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享内存,做进程间通信。
3.堆:用于程序运行的动态内存分配,堆是可以上增长的。
4.代码段:可执行的代码/只读常量。
2.C语言的动态内存开辟
C语言使用malloc,calloc,ralloc进行动态内存的开辟和处理。
使用free函数来进行动态内存的释放。
int* p1=(int*)malloc(sizeof(int));//开辟一个int大小的空间
int* p2=(int*)calloc(4,sizeof(int));//开辟了4个大小为int的空间
int* p3=(int*)realloc(p2,sizeof(int)*10);//对p2增加40个字节大小的空间
free(p1);
free(p3);//释放p1和p3的空间
3.C++动态内存开辟
(1)new与delete
在C++中使用new和delete来进行动态内存的管理,原因有二:
1.自定义类型对象动态申请的时候,解决初始化和清理的问题。(new和delet会调用构造函数和析构函数)
2. new失败后要求抛异常,符合面向对象语言出错处理机制。
下面给出不同的开辟空间的方式:
int* a1 = new int;//开辟一个int大小的空间
int* a2 = new int[10];//开辟一个10个int大小的空间
int* a3 = new int(10);//开辟一个int大小的空间并初始化为10
A* a = new A;//开辟一个A类型的对象
delete a1;//删除a1的空间
delete[] a2;//删除数组的空间
delete a3;//删除a3的空间
delete a;//删除A类型对象的空间
注意:在为数组开辟空间后,进行删除时一定要在delete后加[]。
在使用new构造对象和用delete删除对象的时候会调用构造和析构函数:
总结:
new=申请空间+初始化
delete=调用析构函数清理+释放空间
在使用new申请空间的时候,不需要申请失败的检查,因为new失败会抛异常。
(2)new与delete的底层
当我们使用new时:
class Stack
{
private:
int* a;
int _top;
int _capacity;
public:
Stack()
:a(nullptr)
, _top(0)
, _capacity(0)
{}
~Stack()
{
free(a);
_top = _capacity = 0;
}
};
int main()
{
Stack* p1 = new Stack;
delete p1;
}
对于Stack* p1 = new Stack;这段代码转到反汇编之后可以看到,主要调用了两个函数:
一个是我们即将要说的operator new还有一个就是构造函数:
同理delete也调用了两个函数,一个是析构函数还有一个就是operator delete(注意两者的顺序)
(3)operator new与operator delete
用法
operator new和operator delete的用法与malloc和free是相同的,我们可以直接使用operator new来开辟空间,用operator delete来进行空间的释放。
Stack* p2 = (Stack*)operator new(sizeof(Stack));
operator delete(p2);
注意,operator new与operator delete并不是想给程序员使用的,我们使用new和delete就可以使用到它们
底层源码
operator new的底层源码:
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
operator delete的底层源码:
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 new和operator delete本质上就是对malloc和free的封装。并且实现了当malloc申请内存失败之后的抛异常处理,这更加符合C++的处理机制。
(4)内存池概念
当我们动态开辟多个对象的时候,我们调用的是operator new[],其本质上是对operator new的封装,但是每开辟一段空间我们就需要申请一次。我们可以通过内存池或者为函数定制专属的operator new来解决这一问题。
就好比从山上向山下取水一样,每一次下山都需要消耗大量人力物力,不如一次性取很多水,在山上建立一个池塘。从而提高效率。
C++为了避免出现多次申请空间的现象,提出来了内存池的概念,STL中的内存池叫做空间配置器。
关于内存池的具体做法在以后的文章中会提及。
(5)构造函数的直接调用
我们知道,构造函数在创建变量的时候就已经调用了,是不能够进行直接调用的。
new分为两个部分,一个是调用operator new还有一部分就是调用构造函数。
如果我们已经手动调用了operator new那么如何手动调用构造函数呢?
new(p2)Stack;
C++提供了如上的方式进行构造函数的调用,即对已经开辟出空间的p2进行初始化。当然Stack后面是可以用括号跟参数的。
但是析构函数是可以进行直接调用的。
以下四段代码两两等价:
Stack* p1 = new Stack;
Stack* p2 = (Stack*)operator new(sizeof(Stack));
new(p2)Stack;
delete p1;
p2->~Stack();//直接调用析构函数
operator delete(p2);
(6)总结:malloc/free与new/delete的区别
共通点:都是从堆上申请空间,都需要用户手动释放。
不同点:
1.malloc和free是函数,new和delete是操作符。
2.malloc申请的空间不会初始化,new可以初始化。
3.malloc申请空间时需要手动计算空间大小,并进行传递,new只需要其后跟上空间的类型即可。
4.malloc的返回值为void*,在使用时必须强转,new不需要,但new会捕获异常。
5.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。
4.内存泄漏
(1)原因
动态申请的内存不用了,但没有进行释放。
(2)危害
1.出现内存泄漏的进程,正常结束,这些内存会还给系统不会造成大的危害。
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;
}
比如这段代码就由于抛异常导致p3没有被释放。
(3)如何避免
1.事前预防使用智能指针。
2.事后查错使用内存泄漏检测工具。
5.总结
内存的处理在不同的操作系统中一直以来是一个很重要复杂的问题,小米的创始人雷军大佬就曾经写过处理内存的工具,至今仍被使用着。了解内存的处理不仅可以让我们编程感到更加扎实也可以为计算机减轻负担,文章看完了,点赞你要亮,三连更漂亮我们下篇再见(比心)。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具