一、C语言中动态申请内存(函数)
【堆上】
(1)malloc函数:
函数原型: void* malloc(size_t size); void* malloc(count *sizeof(*pointer));
函数功能:动态申请size个字节大小的内存空间,返回该段空间的首地址,该段空间里面的内容是随机值。
函数参数:malloc()函数有一个参数,即要分配的内存空间的大小。
返回值:始终是void* ;申请成功,返回空间首地址,否则,返回NULL;使用这个函数一定要对返回值进行判断。malloc返回的内存是“没有”初始
一个错误的例子:
int *p=malloc(10*4);
首先没有对返回值进行强制类型转换,编译时会出现警告。其次,参数给了4,只是32位机器下的一个整型的长度,可移植性差。最后,没有进行参数检测,万一分配内存 失败,后面对这个指针的操作可能会导致崩溃!
建议像下面这样使用malloc:
int *p = (int*)malloc(n*sizeof(int));
if (NULL == p)
{
//do something or exit
}
(2)calloc函数:
函数原型:void*calloc(size_t nmemb,size_t size);
函数功能:动态申请num个元素数组的内存块,申请的空间会用0来初始化。
函数参数:calooc函数有两个参数,分别为元素的数目num和每个元素的大小size,这两个参数的乘机就是要分配的内存空间的大小。
返回值:申请成功,返回空间的首地址,否则返回NULL。使用时一定要注意检测是否开辟成功。
(3)、realloc函数:
函数原型:void* realloc(void* mem_address,unsigned int newsize);
参数说明:ptr:需要改变的指针; size:要改变的字节数byte,可比原内存空间大或者小。
函数功能:先判断当前指针指向的内存块后面有没有足够大的连续内存空间,如果有扩大,直接返回原地址。如果指向的内存块之后没有足够大的内存空间,则重新开辟size大小的空间,将原有的数据从头到尾拷贝到新的内存空间中去,然后将原空间释放,最后返回新分配空间的首地址。
注意:
1、realloc失败的时候返回NULL;
2、realloc失败的时候,原来的内存空间不会改变,不会释放也不会移动。
3、如果size为0,效果等同于free,只对指针所指内存进行释放,对于二级指针** realloc时,只会释放一维,注意使用时谨防内存泄漏。
4、传递给realloc的指针,必须是先经过malooc/calooc/realooc申请的。
5、当ptr为NULL时,等同于malloc。
void TestMemory()
{
// Malloc
int * pTest = (int*)malloc(10 * sizeof(int));
DoSomething()
if (pTest != NULL)
{
free(pTest);
pTest = NULL;
}
// calloc 该函数会将申请的内存空间初始化为0
int * pTest1 = (int*)calloc(10, sizeof(int));
DoSomething();
if (pTest != NULL)
{
free(pTest);
pTest = NULL;
}
// rellock,改变原有内存空间大小,若不能改变,则将会开辟一段新的内存,
//将原有内存的内容拷贝过去,
// 但不会对新开辟的空间进行初始化
int * pTest2 = (int*)malloc(10 * sizeof(int));
realloc(pTest2, 100 * sizeof(int));
free(pTest2);
}
以上3个空间使用完后,一定要记得释放掉那块空间,否则会引起内存泄漏,这在大的项目中是一件非常重要的事情。
(4)、free函数:
指针被free以后其地址仍然不变(非NULL),只是该地址对应的内存是垃圾,p就成了“野指针”。如果此时不把p设置为NULL,会让人误以为p是个合法的指针。我们有时记不住p所指的内存是否已经被释放了,在释放p之前,先用语句if(p!=NULL)对p进行防错处理。
【常见的内存泄漏举例】
void MemoryLeaks()
{
// 1、内存申请了忘记释放
int *pTest = (int *)malloc(10 * sizeof(int));
assert(NULL != pTest);
DoSomething();
// 2、程序逻辑不清,以为释放了,实际内存泄露
int *pTest1 = (int *)malloc(10 * sizeof(int));
int *pTest2 = (int *)malloc(10 * sizeof(int)); DoSomething();
pTest1 = pTest2;
free(pTest1);
free(pTest2);
// 3、程序误操作,将堆破坏
char *pTest3 = (char *)malloc(5);
strcpy(pTest3, "Memory Leaks!");
free(pTest3);
// 4、释放时传入的地址和申请时的地方不相同
int *pTest4 = (int *)malloc(10 * sizeof(int));
assert(NULL != pTest4);
pTest4[0] = 0;
pTest4++;
DoSomething();
free(pTest4);
}
【栈上】
使用_alloc(VS编译器)或者alloc(gcc编译器)在栈上开辟内存,栈上开辟的内存由便一起自动维护,不需要显示的释放。用法同malloc。
以上的几个函数是C语言中的函数,在C++中也可以使用。
二、C++中动态内存管理(运算符)
(1)、【new/delete运算符】
1、new分配空间看起来比C中动态内存开辟空间简单,而且无需自己计算所需内存的大小,返回值也无需强制类型转换。
2、new不只是分配内存,而且会调用类的构造函数,同理delete会调用类的析构函数。
3、new和delete要匹配使用;delete和free在释放了内存之后,立即将指针设置为NULL,防止产生“野指针”。
void Test
{
int *p4 = new int();// 动态分配4个字节(1个 int)的空间
int *p5 = new int(3);// 动态分配4个字节(1个 int)的空间并初始化为3
int *p6 = new int[3];// 动态分配12个字节(3个 int)的空间
delete p4;
delete p5;
delete[] p6;
}
new和delete、new[]和delete[]一定要匹配使用,否则可能出现内存泄漏甚至崩溃的问题。
(2)、new/delete和new[]/delete[]:
1、new/delete:
其实我们用new来申请一段空间时,编译器会先调用operator new函数,然后再调用构造函数(如果有的话)进行初始化。其中,在operator new函数中,又调用了malloc函数,即operator new是malloc的封装;用delete来释放空间,编译器会先调用一次析构函数,然后才调用free释放那段空间。
2、new[]/delete[]:
再来看看new [] 和 delete [],以test *p2 = new test[10];来举例说明。
如上,编译器总共开辟了44个字节,其中后40个字节用来存放对象,new[]返回的1指针就指向这40个字节的首地址。前4个字节存放了对象个数,它的作用先不管,看一看new[]是怎么做的:
其实new []是对operator new[] 的一个封装,用new[]来分配空间时,调用了operator new[],然后在operator new[]内,将所申请的内存空间大小计算出来,例如上边例子中就是10*sizeof(test),然后在这个基础上加4,变成了44(字节),而operator new[]又封装了operator new,接着又调用了operator new。所以现在清楚了吧!真正申请的空间大小多了4字节。调用完operator new[]后,又将返回的指针向后偏移4个字节,并依次调用10次构造函数,完成对对象的初始化,然后才将这个偏移4字节后的指针返回去。也就是用户看到的指针了。
再看delete[],它其实封装了operator delete[] 和free,用delete[]释放空间时,先根据传进来指针所指向空间的前4个字节的内容(即对象的个数,假设为n),调用n次析构函数(反着来的,先构造的后析构),然后将这个地址传递给operator delete[],operator delete[]封装了operator delete,它先计算出这段空间的真正首地址,即将传进来的指针向前偏移4个字节,然后调用operator delete。
(3)、总结一下:
【new作用】
调用operator new分配空间。
调用构造函数初始化对象。
【delete作用】
调用析构函数清理对象
调用operator delete释放空间
【new[]作用】
调用operator new分配空间。
调用N次构造函数分别初始化每个对象。
【delete[]作用】
调用N次析构函数清理对象。
调用operator delete释放空间。
(4)、定位new表达式:
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
注意:C++中的构造函数是不能显示调用的,这里只是变相的调用了构造函数。
test *p = new test[10];
new(p)test;
new(p + 1) test;
//...
new(p + 9) test;
像上面这样,就完成了对所申请空间的初始化,借用定位new表达式,malloc和free,再加上显示的调用析构函数,可以模拟出new/delete和new[]/delete[]的行为。
三、最后的总结:【malloc/free和new/delete的区别和联系】
- 它们都是动态管理内存的入口
- malloc/free是C/C++标准库的函数,new/delete是C++操作符
- malloc/free只是动态分配内存空间/释放空间。而new/delete除了分配空间还会调用构造函数和析构函数进行初始化与清理(清理成员)
- malloc/free需要手动计算类型大小且返回值会void*,new/delete可自己计算类型的大小,返回对应类型的指针
- 对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free
- 它们都需要各自配对使用
四:补充内存分配的方式:
内存分配方式:
(1)、静态存储区域分配
内存在程序编译的时候就已经分配好,这块内存在程序的整个运行周期都存在。例如全局变量,static变量。
(2)、栈上创建
在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动给被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限
(3)、堆上分配,亦称动态内存分配
程序在运行的时候用malloc或new申请任意的内存,程序员自己负责在任何时候用free或delete释放内存。动态内存的生存期由我们决定,使用灵活,但问题也最多。
(4)、注意
如果函数的参数是一个指针,一般不用指针去开辟空间,非要用指针去开辟空间的时候,就要用指向指针的指针。c/c++语言没有办法知道指针所指的内存容量,除非在申请的时候记住它。内存申请失败的处理方式:return;语句或者exit(1);语句,再或者是c++中的异常处理方式。