C/C++内存管理详解以及内存泄露的检测


前言

这篇博客主要介绍一些C/C++的内存管理机制,包括C/C++内存分布、申请释放内存方式及底层实现的简单解释、内存泄漏、内存泄漏的检测、如何防止内存泄漏,若有错误之处还请指正。

一、C/C++内存分布?

1.内存布局示意图:

在这里插入图片描述

2.内存存放的数据类型

栈:非静态局部变量/函数参数/返回值等等,栈是向下增长的。
内存映射段:是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可以使用接口创建共享内存(shm系列接口),用于进程间的通信。
堆:程序运行时动态内存分配的空间,即new/delete/malloc/free,堆是向上增长的。
数据段:全局数据、静态数据。
代码段:可执行代码(二进制),只读常量。

二、C/C++内存管理

1.C内存管理

申请内存:包括malloc、realloc、calloc

//须包括头文件stdlib.h
void * malloc(int n);
//参数解释
n:内存大小,单位字节。
返回值:返回一个void*类型的指针,申请成功返回对应内存地址,申请失败返回NULL;实际使用,需要将返回值强转为我们需要的类型。
//如下:
//申请10个int大小的空间
int * temp=(int*)malloc(sizeof(int)*10);

//realloc、calloc使用机会较少
//realloc :给一个已经分配了地址的指针重新分配空间,参数ptr为原有的空间地址,newsize是重新申请的地址长度.
void* realloc(void* ptr, unsigned newsize);
// calloc:与malloc相似,参数sizeOfElement为申请地址的单位元素长度,numElements为元素个数,即在内存中申请numElements*sizeOfElement字节大小的连续地址空间.  只不过calloc会初始化申请的空间 
void* calloc(size_t numElements, size_t sizeOfElement);

释放内存:

void free(void* ap)
//ap:既要释放的内存的指针
//无返回值
//释放后最好赋为NULL,防止野指针
free(temp);
temp=NULL;

2.C++内存管理

申请内存:new/new []

//注意:new/delete 为操作符,malloc/realloc/calloc/free为函数。

//申请和释放单个元素的空间,使用new和delete操作符,申请和释放连续的空间,使用new[]和
delete[]
void Test()
{
 // 动态申请一个int类型的空间
 int* ptr1 = new int;
 
 // 动态申请一个int类型的空间并初始化为10
 int* ptr2 = new int(10);
 
 // 动态申请10个int类型的空间
 int* ptr3 = new int[3];
 }

释放内存:delete/delete []

//释放上面申请的空间
 delete ptr1;
 delete ptr2;
 delete[] ptr3;

申请释放自定义类型的空间:

class Test
{
public:
 Test()
 : _data(0)
 {
 cout<<"Test():"<<this<<endl;
 }
 ~Test()
 {
 cout<<"~Test():"<<this<<endl;
 }
 
private:
 int _data;
};

void Test2()
{
//注意:在申请自定义类型的空间时,new会调用构造函数,
//delete会调用析构函数,而malloc与free不会,所以C++中申请自定义类型尽量使用new/delete防止内存泄漏。

 // 申请单个Test类型的对象
 Test* p1 = new Test;
 delete p1;
 
 // 申请10个Test类型的对象
 Test* p2 = new Test[10];
 delete[] p2; }

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

operator new

/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试
执行空 间不足应对措施,如果该应对措施用户设置了,则继续申请,否则抛异常。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) {
 // try to allocate size bytes
 void *p;
 //循环尝试申请,若申请失败,没有内存抛bad_alloc异常,若有内存则一直申请直到成功。
 //从这里看出申请内存顺序为  new操作符->operator new->malloc
 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

/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void *pUserData) {
 _CrtMemBlockHeader * pHead;
 //回调_RTC_Free_hook释放内存
 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 释放
 _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,提高效率。

三、内存泄漏

1.概念

内存泄漏也称作"存储渗漏",用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元。直到程序结束。(其实说白了就是该内存空间使用完毕之后未回收)即所谓内存泄漏。
若一个系统时常运行着存在内存泄露的进程,则随着时间的推移,内存逐渐减少,最终很有可能导致系统奔溃。
内存泄漏包括 系统资源泄漏:打开的系统资源没有关闭,如文件描述符。
堆内存泄漏:即申请的堆内存没有释放。

2.Windows平台下检测内存泄漏

检测是否存在内存泄漏
包含C的以下头文件和宏

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

包含<crtdbg.h>头文件后,它会把 malloc 和 free 函数映射到它们的调试版本,即 _malloc_dbg 和 _free_dbg,_malloc_dbg 和 _free_dbg将跟踪内存的申请和释放,最后在程序退出之前包含以下语句来转储内存泄漏信息。

_CrtDumpMemoryLeaks();

当加上以上所有操作后,调试时_CrtDumpMemoryLeaks()会将内存泄漏信息展示到输出窗口(调试输出窗口)中。

如下,因为new底层也是调用的malloc ,所以也可检测new。

#define _CRTDBG_MAP_ALLOC
#include<iostream>
#include<crtdbg.h>
using namespace std;
int main() {
	int* a = new int[10];

	_CrtDumpMemoryLeaks();
	return 0;
}

输出窗口信息如下:

Detected memory leaks!
Dumping objects ->
{156} normal block at 0x00FA5AA0, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

程序有可能退出的地方有很多,这时我们可以在程序开始运行的地方加上如下语句,它会在程序退出时自动调用_CrtDumpMemoryLeaks()来转储内存泄漏信息。

_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );

那么问题来了, 如何定位具体内存泄漏的位置了?

要定位到是哪里出现了内存泄漏,必须要有内存的快照。crtdbg头文件为我们定义了结构体_CrtMemState,表示内存的实时状态信息。
通过_CrtMemCheckpoint( _CrtMemState* )可以帮助我们对内存信息进行拍照,并保存到_CrtMemState结构体当中,如下。

//定义结构体
_CrtMemState  cs1;
//拍照
_CrtMemCheckpoint( &cs1);

我们已经知道了内存的快照信息,接下来只需要对比不同地方的内存的快照差异,即可定位内存泄漏位置。对比内存快照的差异,通过如下接口。

//如果不同返回true,否则false
int _CrtMemDifference(
   _CrtMemState *stateDiff,   //保存oldState和newState的差异,入参
   const _CrtMemState *oldState,
   const _CrtMemState *newState
);

得到快照差异_CrtMemState结构体后,通过如下接口可以随时转储内存泄漏信息。

void _CrtMemDumpStatistics(
   const _CrtMemState *state
);

最后,在程序将要结束之前加上_CrtDumpMemoryLeaks()即可

综合示例:

#define _CRTDBG_MAP_ALLOC
#include<iostream>
#include<crtdbg.h>
using namespace std;
int main() {
	//定义内存状态结构体
	_CrtMemState cs1, cs2,cs3;
	_CrtMemCheckpoint(&cs1);
	int* a = new int[10];
	delete[] a;
	int* b = new int[10];
	_CrtMemCheckpoint(&cs2);
	//比较  CS1和CS2的差异,并将其转储到CS3
	if (_CrtMemDifference(&cs3, &cs1,&cs2))
		_CrtMemDumpStatistics(&cs3);
	_CrtDumpMemoryLeaks();
	return 0;
}

通过调试,可以观察到如下输出窗口信息,可知在cs1和cs2之间存在内存泄漏。若无内存泄漏则不会有输出。

0 bytes in 0 Free Blocks.
40 bytes in 1 Normal Blocks.
0 bytes in 0 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 0 bytes.
Total allocations: 80 bytes.
Detected memory leaks!
Dumping objects ->
{157} normal block at 0x00AF5AA0, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

注意crtdbg.h只适用于(DEBUG)调试版本的程序

3.Linux平台下检测内存泄漏

Linux下也有类似的检测内存泄漏库:mcheck.h
使用方法很简单,如下:
test.cpp

      #include<iostream>
      #include<mcheck.h>
      #include <stdlib.h>
      using namespace std;
      int main(){
        //使用前要先设置环境变量MALLOC_TRACE
        setenv("MALLOC_TRACE","./mtrace_output",1);
        //通过mtrace()和muntrace() 定位内存泄漏,
        //内存泄漏信息被保存到setenv第二个参数所指定的文件中,
        //无需我们自己创建,会自动生成,类型为txt。
        mtrace();
        int* a=new int[10];
        muntrace();                                                                                                                                                                         
        return 0;
      }

注意: 生成可执行程序时一定要加上调试选项 -g
g++ -g test.cpp -o test
若存在内存泄漏就会被显示在我们指定的文件中,信息如下;
若不存在start和end之间就无信息。

= Start
@ /home/test/.VimForCpp/vim/bundle/YCM.so/el7.x86_64/libstdc++.so.6:(_Znwm+0x18)[0x7fec16ab0dc8] + 0x188d1c0 0x28
= End

四、C++如何防止内存泄漏

在C中我们只有合理使用malloc和free才能有效防止内存泄露。
但对于C++来说我们可以采用RAII(利用对象的生命周期来管理资源)的思想来防止资源泄露。如:守卫锁、智能指针等。
这里主要介绍下智能指针:
https://blog.csdn.net/weixin_45295598/article/details/109259858
文章审核中


  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值