C语言内存管理及经典算法.docx
- 文档编号:9948111
- 上传时间:2023-02-07
- 格式:DOCX
- 页数:97
- 大小:225.75KB
C语言内存管理及经典算法.docx
《C语言内存管理及经典算法.docx》由会员分享,可在线阅读,更多相关《C语言内存管理及经典算法.docx(97页珍藏版)》请在冰豆网上搜索。
C语言内存管理及经典算法
C语言内存管理
对于一个c/c++程序员来说,内存泄漏是一个常见的也是令人头疼的问题,为了应对这个问题,有许多技术被研究出来来解决这个问题,例如SmartPointer,GarbageCollection等。
一般我们常说的内存泄漏是指堆内存的泄漏。
那么为什么会导致内存泄漏呢?
通过学习内存管理,相信你一定能解决好这个问题。
1-1C语言内存管理方式
在进入本专题前,我们先看一下下面的程序,来简单分析以下C语言的内存管理:
#include
#include
//全局变量定义
intiGlobalInt1=0;
intiGlobalInt2=0;
intiGlobalInt3=0;
//全局常量定义
constintiGlobalConstInt1=1;
constintiGlobalConstInt2=5;
constintiGlobalConstInt3=6;
//全局静态变量定义
staticintiGlobalStaticInt1=0;
staticintiGlobalStaticInt2=0;
staticintiGlobalStaticInt3=0;
//函数定义
voidfuncParamTest(intiFuncParam1,intiFuncParam2,intiFuncParam3)
{
//函数私有变量定义
intiLocalInt1=iFuncParam1;
intiLocalInt2=iFuncParam2;
intiLocalInt3=iFuncParam3;
printf("函数参数变量内存地址\n");
printf("iFuncParam1=0x%08x\n",&iFuncParam1);
printf("iFuncParam2=0x%08x\n",&iFuncParam2);
printf("iFuncParam3=0x%08x\n\n",&iFuncParam3);
printf("函数本地变量的内存地址\n");
printf("iLocalInt1=0x%08x\n",&iLocalInt1);
printf("iLocalInt2=0x%08x\n",&iLocalInt2);
printf("iLocalInt3=0x%08x\n\n",&iLocalInt3);
return;
}
//入口函数
intmain(intargc,char*argv[])
{
//局部静态变量
staticintiStaticInt1=0;
staticintiStaticInt2=0;
staticintiStaticInt3=0;
//局部静态常量定义
conststaticintiConstStaticInt1=0;
conststaticintiConstStaticInt2=0;
conststaticintiConstStaticInt3=0;
//局部常量
constintiConstInt1=1;
constintiConstInt2=5;
constintiConstInt3=6;
//局部变量
intiLocalInt1=0;
intiLocalInt2=0;
intiLocalInt3=0;
char*pMalloc1,*pMalloc2,*pMalloc3;
char*pNew1,*pNew2,*pNew3;
printf("全局常量的内存地址\n");
printf("iGlobalConstInt1=0x%08x\n",&iGlobalConstInt1);
printf("iGlobalConstInt2=0x%08x\n",&iGlobalConstInt2);
printf("iGlobalConstInt3=0x%08x\n\n",&iGlobalConstInt3);
printf("iConstStaticInt1=0x%08x\n",&iConstStaticInt1);
printf("iConstStaticInt2=0x%08x\n",&iConstStaticInt2);
printf("iConstStaticInt3=0x%08x\n\n",&iConstStaticInt3);
printf("全局变量的内存地址\n");
printf("iGlobalInt1=0x%08x\n",&iGlobalInt1);
printf("iGlobalInt2=0x%08x\n",&iGlobalInt2);
printf("iGlobalInt3=0x%08x\n\n",&iGlobalInt3);
printf("静态变量的内存地址\n");
printf("iGlobalStaticInt1=0x%08x\n",&iGlobalStaticInt1);
printf("iGlobalStaticInt2=0x%08x\n",&iGlobalStaticInt2);
printf("iGlobalStaticInt3=0x%08x\n\n",&iGlobalStaticInt3);
printf("iStaticInt1=0x%08x\n",&iStaticInt1);
printf("iStaticInt2=0x%08x\n",&iStaticInt2);
printf("iStaticInt3=0x%08x\n\n",&iStaticInt3);
printf("本地变量的内存地址\n");
printf("iConstInt1=0x%08x\n",&iConstInt1);
printf("iConstInt2=0x%08x\n",&iConstInt2);
printf("iConstInt3=0x%08x\n\n",&iConstInt3);
printf("iLocalInt1=0x%08x\n",&iLocalInt1);
printf("iLocalInt2=0x%08x\n",&iLocalInt2);
printf("iLocalInt3=0x%08x\n\n",&iLocalInt3);
funcParamTest(iLocalInt1,iLocalInt2,iLocalInt3);
//在堆上分配内存,使用new
pNew1=newchar[16];
pNew2=newchar[16];
pNew3=newchar[16];
//在堆上分配内存,使用malloc
pMalloc1=(char*)malloc(16);
pMalloc2=(char*)malloc(16);
pMalloc3=(char*)malloc(16);
printf("在堆上分配内存内存地址\n");
printf("pMalloc1=0x%08x\n",pMalloc1);
printf("pMalloc2=0x%08x\n",pMalloc2);
printf("pMalloc3=0x%08x\n\n",pMalloc3);
//释放new分配的内存空间
delete[]pNew1;
delete[]pNew2;
delete[]pNew3;
pNew1=NULL;
pNew2=NULL;
pNew3=NULL;
//释放malloc分配的内存空间
free(pMalloc1);
free(pMalloc2);
free(pMalloc3);
pMalloc1=NULL;
pMalloc2=NULL;
pMalloc3=NULL;
return0;
}
本程序在WindowsXP下,VC6编译后的执行结果是:
注意,上面我们输出的完全是内存地址,也就是说,是程序在进程中内存地址(注意是虚拟内存地址而不是物理内存地址)。
我们认真观察程序输出,发现每种类型的内存地址都是连续的,而不同类型之间内存地址有的是连续的,有的差别极大(注意:
不同编译器可能输出的结果不一样,但这并不影响我们分析问题)。
基本上,我们可以把这些地址范围分为如下几个部分:
堆、栈、全局/静态存储区和常量存储区。
栈,就是那些由编译器在需要的时候分配,在不需要的时候自动释放的存储区。
里面的变量通常是局部变量、函数参数等。
在栈上分配内存,通常是指在执行函数时,函数内局部变量在栈上创建,函数执行结束时这被自动释放。
栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
堆,就是那些由new或使用malloc分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new/malloc就要对应一个delete/free。
如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
动态内存的生存期由我们决定,使用非常灵活,但问题最多,也是我们本章讨论的重点。
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。
静态存储区在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改(通过特殊手段当然是可以修改的,例如Windows下可直接修改PE文件)。
通过分析上面的程序,我们大抵可以绘出程序内存分配情况
通过上面分析,我们知道,全局常量(例如iGlobalConstInt1)和局部静态常量(例如iConstStaticInt1)位于常量存储区;全局变量(例如iGlobalInt1)和局部静态变量(iStaticInt1)位于静态数据区;本地变量(例如iLocalInt1)和函数参数变量(例如iFuncParam1)位于栈区,它是动态存储区的一部分;使用malloc/new(例如pMalloc1,pNew1)分配的内存位于堆区,它也是动态存储区的一部分。
由于常量存储区和静态数据区都是在程序编译的时候就分配好空间了,而堆栈是在程序运行过程中自动分配好的空间。
使用堆分配内存是显式分配的,我们在稍后详细介绍。
下面简单介绍一下使用栈分配内存空间的原理。
我们来看看我们DEMO程序对于函数参数那一部分,我们发现,栈分配如下所示
我们发现函数参数地址和变量地址分布如上,其中多了四个字节,正好就是RET指令。
首先,三个参数以从又到左的次序压入堆栈,先压“iFuncParam3”,再压“iFuncParam2”,最后压入“iFuncParam1”;然后压入函数的返回地址(RET),接着跳转到函数地址接着执行。
第三步,将栈顶(ESP)减去一个数,为本地变量分配内存空间,接着就初始化本地变量的内存空间。
感兴趣的读者可以使用工具反汇编上面的代码,然后就可以看到C语言是如何编译的了。
从上面我们可以看出,对于栈分配内存而言,是由编译器自动管理,无需我们手工控制。
然而,对于堆而言,内存分配与释放由程序员控制了,方法非常灵活,但也最容易出现问题。
1-2C语言内存管理
上一节中我们简单分析了C语言内存管理方式,其中对于堆的管理,我们仅进行了简单描述。
在本节中我们要详细描述C语言内存管理。
在介绍之前,我们需要先熟悉C语言堆内存管理涉及的函数。
alloca
void*alloca(size_tsize);
alloca的作用分配一块大小为size个字节的可用内存块,并返回首地址。
不能分配的时候返回NULL。
alloca是从栈中分配内存空间,使用alloca分配内存后不必使用free来释放内存。
alloca是分配一块未经初始化的内存,这和malloc一样,如果需要初始化可以调用memset函数。
在上节中,我们已经对栈分配做了详细的描述,栈的释放是由编译器自动管理的,所以不需要我们手动去释放它。
示范代码如下:
#include
#include
intmain()
{
intsize=1000;
interrcode=0;
void*pData=NULL;
if(size>0&&size<1024)
{
pData=alloca(size);
if(pData!
=NULL)
{
printf_s("Allocated%dbytesofstackat0x%p",size,pData);
}else{
printf_s("Allocated%dbytesofstackfailed",size);
}
}else{
printf_s("Triedtoallocatetoomanybytes.\n");
}
}
malloc与free
void*malloc(size_tsize);
voidfree(void*ptr);
malloc的作用分配一块大小为size个字节的可用内存块,并返回首地址。
不能分配的时候返回NULL。
malloc是从堆中分配内存空间,使用malloc分配内存后必须使用free释放内存。
free清除ptr所指向的地址,它只作清除的工作,并告诉系统,这块地址已经被释放和清除,可以重新被分配。
使用malloc分配的内存没有进行初始化,也就是说,该内存区中可能存在先前内容,而calloc则将内存初始化为0。
如果需要对malloc分配的内存初始化,可以使用memset函数。
示范代码如下:
#include
#include
#include
voidmain(void)
{
char*string;
/*Allocatespaceforapathname*/
string=malloc(_MAX_PATH);
//InaC++file,explicitlycastmalloc'sreturn.Forexample,
//string=(char*)malloc(_MAX_PATH);
if(string==NULL)
printf("Insufficientmemoryavailable\n");
else
{
printf("Memoryspaceallocatedforpathname\n");
free(string);
printf("Memoryfreed\n");
}
}
calloc
void*calloc(size_tnmemb,size_tsize);
calloc的作用是分配并初始化内存块,返回一个指向nmemb块数组的指针,每块大小为size个字节。
它和malloc的主要不同之处是会初始化(清零)分配到的内存。
示范代码如下:
#include
#include
voidmain(void)
{
long*buffer;
buffer=(long*)calloc(40,sizeof(long));
if(buffer!
=NULL)
printf("Allocated40longintegers\n");
else
printf("Can'tallocatememory\n");
free(buffer);
}
Realloc
void*realloc(void*ptr,size_tsize);
realloc以ptr所指地址为首址,分配size个字节的内存,并返回ptr所指地址。
realloc不会初始化分配到的内存块,如果ptr为NULL则相当于malloc,如果size为NULL则相当于free(ptr)。
不能分配返回NULL。
示范代码如下:
#include
#include
#include
voidmain(void)
{
long*buffer;
size_tsize;
if((buffer=(long*)malloc(1000*sizeof(long)))==NULL)
exit
(1);
size=_msize(buffer);
printf("Sizeofblockaftermallocof1000longs:
%u\n",size);
/*Reallocateandshownewsize:
*/
if((buffer=realloc(buffer,size+(1000*sizeof(long))))
==NULL)
exit
(1);
size=_msize(buffer);
printf("Sizeofblockafterreallocof1000morelongs:
%u\n",
size);
free(buffer);
exit(0);
}
通过上面的学习,我们知道:
alloca、calloc、malloc、realloc负责分配内存,free负责释放内存。
其中alloca是在栈中分配内存,而calloc、malloc、realloc是在堆中分配内存,也就是说alloca的内存分配,是有作用域的,不需要释放,而calloc、malloc、realloc内存是没有作用域的,需要调用free主动释放分配内存区域。
alloca,malloc,realloc只负责分配内存,并不初始化分配内存空间,而calloc不仅分配内存,还负责初始化分配内存为0。
realloc是以传入指针为基址,分配指定大小的内存区域。
当读者阅读到此时的时候,可能觉得内存管理其实很简单,无非是分配内存释放内存而已。
大家不妨看看如下一个程序:
voidMyGetMemory(intiSize)
{
char*szTemp=(char*)malloc(iSize);
if(!
GetString(szTemp,iSize))
{
printf("getstringfailed!
\n");
return;
}
...
free(szTemp);
}
相信大家能很快发现上面在GetString函数返回失败的情况下,内存没有释放,将产生内存泄露。
如果我们再更改一下,可能这个错误稍微隐蔽一点。
char*MyGetMemory(intiSize)
{
char*szTemp=(char*)malloc(iSize);
if(!
GetString(szTemp,iSize))
{
printf("getstringfailed!
\n");
returnNULL;
}
returnszTemp;
}
voidTest()
{
char*szMalloc=MyGetMemory(23);
if(szMalloc)
{
printf("out:
%s\n",szMalloc);
free(szMalloc);
szMalloc=NULL;
}
}
这个程序的内存泄露同样是在GetString失败的时候产生,我们单存分析Test函数是发现不了内存泄露的。
在实际项目中,由于较为复杂,可能忘记释放内容了,也有可能释放内容后再次释放内容等等,这些错误要么是程序运行时间越久,所耗内存越大,要么直接出现异常。
如果分配了内存忘记释放,那样就产生了内存泄漏。
为了防止内存泄漏,一些项目甚至要求对分配、释放内存进行跟踪,以避免内存泄漏。
最简单的方法就是封装内存分配和释放函数,实际分配中并不直接调用alloca、calloc、malloc、realloc来分配内存,也不直接调用函数free来释放内存。
另外,在服务器上,由于程序需要长期执行,频繁的分配内存资源会导致内存碎片的增多,这样可以使用内存池来解决这些问题。
既然内存管理错误这么频繁,后果这么严重,那么作为一个新手程序应该如何来避免这些问题呢?
在下一节我们将详细介绍。
1-3C语言内存使用要点及常见错误
在介绍内存使用要点之前,我们先看看使用C语言内存管理中经常出现的错误,尤其是新手。
1、内存分配后没有校验,使得内存未成功,却正常使用。
2、内存分配成功后,没有初始化就使用。
3、内存分配成功,也进行了初始化,可是使用内存时出现了越界(缓冲区溢出)。
这种错误如果被黑客成功利用,最严重的后果就是整个操作系统被黑客完全控制。
4、内存分配成功后,忘记释放内存,产生内存泄漏。
5、内存分配成功后,也正常释放内存,可是在释放内存后又继续使用。
6、混淆指针和数组。
上面的这些问题,不仅仅是新手容易犯,一个工作多年的老程序员依然可能犯这样的错误。
如果有一天,您发现您的程序在debug下可以成功运行,可是在release下却不能成功运行,一种可能就是您的数据没有被初始化。
如果有一天,您的程序出现一个访问一个非常内存地址的错误,那么你应该检查一下是否产生了越界错误等等。
总而言之,上面的任何一种错误出现了,就极有可能不好定位错误,尤其是访问越界、释放后继续使用的错误。
内存分配后不校验直接使用主要是新手犯这种错误,由于没有意识到内存分配会不成功。
这种问题的解决办法很简单,判断指针是否为空就可以了,在上节中的例子比比皆是,就不列举出来了。
另外一种情况是函数的入参为空,可以使用assert(p!
=NULL)来简单检查,也可以使用if(p!
=NULL)来判断。
这类错误只要养成好习惯,是完全可以避免的。
内存分配后没有初始化,这种情况也是属于粗心引起,也通常是新手犯的错误。
从上节中我们知道,alloca,malloc,realloc是只负责分配内存而不负责初始化内存的,完全可以想象,不初始化直接使用会导致不可预知的错误。
其实,内存没有初始化是出现所有内存分配的情况下,可能是全局的,也有可能出现在栈上,当然更多是出现在堆分配上。
比如如下的例子就是一个堆分配后没有初始化出现的错误,其实在实际项目中,肯定没有这么明显,中途隔了很多代码,所以往往不容易发现。
intiTimes;
for(iTimes=0;iTimes<20;iTimes++)
{
}
While(iTimes>0)
{
…
iTimes--;
}
后来由于程序需要我们注释掉了那段for循环,结果变成了
intiTimes;
/*
for(iTimes=0;iTimes<20;iTimes++)
{
}
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 语言 内存 管理 经典 算法
![提示](https://static.bdocx.com/images/bang_tan.gif)