使用C/C++编程时,会经常动态分配内存,以便合理使用内存,本文主要讲述动态内存分配的几种方法及一些原理,理解不深刻之处欢迎指教。
引言
为什么要进行动态内存分配?以数组为例,数组元素在内存中存储的地址是连续的。声明一个数组后,该数组需要的内存大小在程序编译时就被分配,但是该数组实际所需内存大小在程序运行时才知道,若运行时发现数组所需内存大于编译时的分配好的数组内存,就会报错,示例程序如下:
#include <stdio.h>
int main(void)
{
int arr[3];
for (int i = 0; i < 5; i++)
scanf("%d", &arr[i]);
for (int i = 0; i < 5; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
首先声明了有三个元素的数组arr,但是在实际输入时输入了5个元素,也就是说编译器在编译时给arr分配了3个int型大小的内存块,但是在运行时需要5个int型大小的内存块,就会报如下错误。
因此人们采取了一个简单的方法,就是声明一个足够大的数组,例如int arr[1000]。但是该方法有以下缺点:
(1)若程序所需的数组元素个数大于1000,也会造成上述错误;
(2)若程序所需的数组元素个数只有5,会浪费很大的内存空间;
(3)若程序所需的数据元素个数大于1000,为了提高程序的鲁棒性,应该做出合理的响应,而不是简单报错。
为了解决上述问题,就需要动态分配内存,所谓动态分配内存,通俗理解是需要多大的内存,就分配多大的内存。在C语言中,与动态内存分配有关的函数为malloc、calloc、realloc、free,这些函数在库文件<stdlib.h>中声明;C++中,与动态内存分配有关的运算符为new、delete。
malloc
malloc是C语言提供的执行内存分配的函数。malloc函数原型为"void *malloc(size_t size);",当程序调用malloc函数时,malloc就从内存池中提取相应大小的连续内存块,并返回指向该内存块起始位置的指针。示例程序如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
int count;
scanf("%d", &count); //输入元素个数
p = (int *)malloc(count*sizeof(int));//分配内存空间,将首地址赋给p
if (p == NULL) //若分配失败,报错
exit(-2);
for (int i = 0; i < count; i++)
scanf("%d", p + i);
for (int i = 0; i < count; i++)
printf("%d ", *(p + i));
printf("\n");
free(p);
return 0;
}
关于malloc函数,注意以下几点:
(1)malloc申请的内存是连续的,由于数组的内存也是连续的,因此二者有相通之处,*(p+i)可以表示为数组的第(i+1)个元素;
(2)如果内存池是空的,malloc会向操作系统请求得到更多的内存,然后进行分配;如果操作系统无法向malloc提供更多的内存,malloc就会返回NULL指针,NULL其实就是一个宏定义,值为0,因此对p的值检查是十分必要的,如果p为NULL,表示没分配到内存,自然就不能进行接下来的步骤了。
(3)观察函数原型"void *malloc(size_t size);"可知,malloc返回的是void *类型的指针,这是因为malloc无法得知程序用分配好的内存块存储什么类型的数据(例如int、char、double等),因此返回void *类型,而void *类型可以转为其他任何类型的指针,上述程序就将void *类型强制转换为int *类型,以便该内存块存储int型的数据。
(4)从指针角度看,因为指针存储的是变量的地址,是一个地址值,因此“p = (int )malloc(countsizeof(int))”这个操作可以理解为对指针p初始化。
calloc
calloc也用于内存分配,函数原型为"void *calloc(size_t num_elements,size_t element_size);",示例程序如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
int count;
scanf("%d", &count);
p = (int *)calloc(count,sizeof(int));
if (p == NULL)
exit(-2);
for (int i = 0; i < count; i++)
printf("%d ", *(p + i));
printf("\n");
free(p);
return 0;
}
程序运行结果如下:
该函数和malloc有以下区别:
(1)申请完内存块后,calloc将该内存块存储的值初始化为0,然后才返回指向该内存块起始位置的指针;
(2)两个函数请求内存大小的方式不同,可见参数列表。
realloc
realloc主要用于修改已经分配的内存的大小,函数原型为“void *realloc(void *ptr,size_t new_size);”,ptr代表已经分配内存的首地址,new_size代表修改后的大小。若用于扩大一个内存块,该内存块原先的内容保留,新增加的内存添加到原先的内存块的后面,并且新增加的部分没有初始化;若用于缩小一个内存块,该内存块的尾部的部分内存被裁减掉,前面的部分内存的内容依然保留。示例程序如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p,*newbase;
int count;
scanf("%d", &count);
p = (int *)malloc(count*sizeof(int));
if (p == NULL)
exit(-2);
for (int i = 0; i < count; i++)
scanf("%d", p + i);
for (int i = 0; i < count; i++)
printf("%d ", *(p + i));
printf("\n");
newbase = (int *)realloc(p, 2*count*sizeof(int));
if (newbase == NULL)
exit(-2);
for (int i = count; i < 2 * count; i++)
scanf("%d", newbase + i);
for (int i = 0; i < 2*count; i++)
printf("%d ", *(newbase + i));
printf("\n");
free(newbase);
return 0;
}
程序运行结果如下:
关于该函数,注意:若原先的内存块的大小无法改变,realloc将分配另一块指定大小的内存块,并将原先内存块的内容复制到新的内存块上。若发生这种情况,内存块的首地址就发生了改变,因此应使用新的指针接收realloc函数的返回值。
free
当动态分配的内存不再需要时,应该将其释放掉,以便以后重新分配,若使用完不释放会引起内存泄漏。free函数用来释放malloc、calloc、realloc申请的内存空间,函数原型为“void free(void *ptr)”,ptr是指向要释放的内存块的指针,该函数没有返回值,free的参数若是NULL,则free不会产生任何效果。示例程序如下:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int *p;
p = (int *)calloc(5, sizeof(int));
if (p == NULL)
exit(-2);
printf("地址和值分别为:%p %d\n", p, *p);
for (int i = 0; i < 5; i++)
printf("%d ", *(p + i));
printf("\n");
free(p);
printf("释放后地址和值分别为:%p %d\n", p, *p);
p = NULL;
printf("地址为:%p\n", p);
return 0;
}
运行结果如下:
注意:使用free释放的是内存块,而不是ptr这个指针变量,ptr的这个指针变量仍然指向该内存空间,只不过该内存空间的内容是毫无意义的是,是未定义的,为了防止以后程序不小心使用了它,要把它指向NULL。
new
new是C++的一个关键字,也是操作符,C语言并没有new,new也经常被用于动态内存分配,示例程序如下:
#include <iostream>
using namespace std;
int main(void)
{
int *p, *q,*s;
p = new int; //申请分配一个存储int型数据的内存块,相当于"p=(int *)malloc(sizeof(int));"
q = new int(0); //申请分配一个存储int型数据的内存块,并初始化为0,相当于"q=(int *)calloc(sizeof(int))";
s = new int[10]; //申请分配十个存储int类型数据的内存块,相当于"s=(int *)malloc(10*sizeof(int))";
delete p;
delete q;
delete[] s;
return 0;
}
它和malloc有如下区别:
(1)new申请内存分配时无需指定内存块的大小,编译器会自己计算,malloc则需要使用sizeof等函数计算好内存块的大小;
(2)若申请成功,new会返回指定类型的指针,不需再进行类型转换;而malloc返回的是void *类型的,还需强制转换成我们需要的类型;
(3)若申请失败,new会抛出bac_alloc异常,而malloc会返回NULL。
关于更详细的解释,可见博客动态内存分配、malloc与new的区别,这篇博客从更深层次的角度分析了malloc和new的区别。
delete
new申请的内存块不被释放,也会造成内存泄露,C++主要使用delete来释放new申请的内存。delete用于释放new申请的单个元素分配的内存,delete[]用于释放new []申请的多个元素分配的内存,具体使用方法见上述程序。注意:使用delete释放的是内存块,而不是s这个指针变量,释放完后,s就变为随机值了,如果在接下来的程序中,没有给s赋新值就再调用s,由于s在内存中是随机值,就有可能指向重要地址,以致使系统崩溃,稳妥起见,如果delete之后程序还没结束,就将s的值赋为NULL。
补充
内存的分配方式有以下四种:
(1)从静态存储区域分配,例如全局变量,这些内存在程序编译时就已分配,程序运行期间都存在;
(2)从栈上分配,例如局部变量,这些内存在函数结束时被自动释放;
(3)从堆上分配,例如malloc函数申请的内存,动态内存的生存期不会随着函数结束释放,由程序员决定;
(4)其他,例如常量、二进制代码等,这些内存在程序运行期间一直存在,程序结束后释放。