为什么存在动态内存分配
关于C语言我们大部分人都掌握的内存开辟方式有:
//1.创建变量
int a=20;//在栈空间上开辟四个字节 局部变量
int g_a=100//在静态区开辟4个字节 全局变量
char arr[10]= {0}//在栈空间上开辟10个字节的连续空间
内存类型分布如下:
但是上述开辟空间的方式有两个特点:
1.空间开辟大小是固定的。
2.数组在声明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态内存开辟了。
假设我们需要一个数组 来存储班级的学生信息
struct S{
char name[20];
int age;
};
int main(){
struct S arr[50];
return 0;
}
那么这样的话如果有的班级有60人有的班级有30人这样是不满足我们日常生活中需求的。所以就需要一个动态可变大小的数组,根据你的所需分配大小,这样是最合理的。而动态内存分配就是做这个工作的。
当然有没有人会想这样操作:
struct S{
char name[20];
int age;
};
int main(){
int n;
scanf("%d",&n);
struct S arr[n];
return 0;
}
这样可以吗?,这种写法在某些编译器是可以的,C99中增加了这种写法。gcc是支持C99的也就是用linux开发得时候用gcc编译器你是可以这样写。但是用的很少。称之为变长数组
但是有些编译器是不支持的,所以没有跨平台可言。
动态内存函数的介绍
C语言给我们提供了一些有关动态内存的函数,在stdlib.h中
malloc
C语言提供了一个动态内存开辟的函数:
void* malloc (size_t size);
在cplusplus.com中的描述如下:
分配内存块, 分配一个size字节的内存块,返回指向块开头的指针。并且新分配的内存块的内容没有初始化,留有不确定的值。如果size为零,返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。
int main(){
//向内存申请10个整形的空间
int *p= (int*) malloc(10*sizeof (int ));
return 0;
}
动态内存开辟,方式其实很简单。就上面这样开辟。
但是malloc 是有可能开辟空间失败的,如果没有足够的内存可用,就会返回一个NULL指针
int main(){
//向内存申请10个整形的空间
int *p= (int*) malloc(10*sizeof (int ));
if(p==NULL){
printf("%s\n",strerror(errno));
}else{
//成功申请
int i=0;
for (i = 0; i < 10; ++i) {
*(p+i)=i;
}
for (i = 0; i < 10; ++i) {
printf("%d",*(p+i));
}
}
return 0;
}
这样我们把申请到的空间,进行初始化,然后打印输出了
然后我们试一下没够足够空间可以用的情况
int main(){
int *p= (int*) malloc(10*INT_MAX);
if(p==NULL){
printf("%s\n",strerror(errno));
}else{
//成功申请
int i=0;
for (i = 0; i < 10; ++i) {
*(p+i)=i;
}
for (i = 0; i < 10; ++i) {
printf("%d",*(p+i));
}
}
return 0;
}
INT_MAX的值为:2147483647
#define INT_MAX 2147483647
/* max value for an int */
定义在limits.h中。
但即使是这样有些还是会申请到,所以我们让其在*10试一下。
输出结果为:
但是我们申请了,用完之后不用了,还要还回去,不能一直申请不还,所以还给操作系统就是用下面的free函数。
malloc指针申请完一定要做检查,判断指针是否为NULL。
free
C语言提供了一个动态内存回收的函数:
void free (void* ptr);
在cplusplus.com中的描述如下:
回收内存块
以前通过调用malloc、calloc或realloc分配的内存块被释放,使其再次可用于进一步分配。如果ptr不指向与上述函数分配的内存块,它会导致未定义的行为。如果ptr是空指针,该函数什么都不做。请注意,此函数不会改变ptr本身的值,因此它仍然指向相同的(现在无效)位置。
我们在上面的代码基础上加入内存释放:
int main(){
int *p= (int*) malloc(10*sizeof (int));
if(p==NULL){
printf("%s\n",strerror(errno));
}else{
//成功申请
int i=0;
for (i = 0; i < 10; ++i) {
*(p+i)=i;
}
for (i = 0; i < 10; ++i) {
printf("%d",*(p+i));
}
}
free(p);//这里加上free
p=NULL;
return 0;
}
输出结果为:
好像从输出结果上看并没有什么不同,但是这里区别可就大了去了
首先关于C语言运行一个程序,在执行结束,这个程序结束的时候会自动把你申请的空间释放掉,自动还给操作系统。但是这里是程序结束,假设这里之后程序还没有结束,如果没有释放。那么这块空间用完之后,一直没有还给操作系统,那么后续这块空间就不能被别人使用。这空间就被白白浪费了
同时最后在free之后要把指针p给赋值为NULL。因为即使这片空间还给操作系统了但是指针p依然具有访问这片空间的权限,所以释放完毕之后要将指针指向NULL。
所以 malloc 和free 一般都是成对使用
calloc
calloc也是一个动态内存分配的函数。
void* calloc (size_t num, size_t size);
在cplusplus.com中的描述如下:
开辟一块空间,并把元素改为0
为num个元素的数组分配一个内存块,每个元素size字节长,并将其所有位初始化为零。有效结果是分配一个(num*size)字节的零初始化内存块。如果size为零,返回值取决于特定的库实现(它可能是也可能不是空指针),但返回的指针不应被解引用。
num:要分配的元素数量。
size:每个元素的字节数目
举个例子使用一下这个函数
int main(){
int* p=(int *) calloc(10,sizeof(int));
if (p==NULL){
printf("%s\n", strerror(errno));
}else{
int i=0;
for ( i = 0; i < 10; ++i) {
printf("%d",*(p+i));
}
}
//释放
free(p);
p=NULL;
return 0;
}
输出结果为:
发现输出结果均为0
那么malloc 和calloc 都是开辟一段内存,然后malloc是不初始化,而calloc需要初始化所以他的效率要比malloc 低一点,在具体使用中如果需要初始化为0就是用calloc,如果不需要就是用malloc,malloc使用偏多一点。
realloc
realloc函数的出现,让动态内存管理更加灵活
有时候我们发现过去申请的空间太小了,有时候我们又觉得申请的空间过大了,那为了合理的内存分配,我们一定会对内存的大小做灵活的调整,那么realloc函数就可以做到怼冬天开辟内存大小的调整,函数原型如下:
void* realloc (void* ptr, size_t size);
在cplusplus.com中的描述如下:
更改ptr指向的内存块的大小。该函数可能会将内存块移动到新位置(其地址由该函数返回)。即使将块移动到新位置,内存块的内容也会保留到新旧size中的较小。如果新size较大,则新分配部分的值不确定。如果ptr是一个空指针,该函数的行为类似于malloc,分配一个新的大小字节块,并返回指向其开头的指针。
如果函数未能分配请求的内存块,则返回一个空指针,并且参数ptr指向的内存块不会被取消分配(它仍然有效,其内容不变)。
ptr:指向以前用malloc、calloc或realloc分配的内存块的指针。或者,这可以是一个空指针,在这种情况下,会分配一个新的块(就像调用malloc一样)。
size:内存块的新大小,以字节为单位。Size_t是一个无符号integral类型。
举个例子:
int main(){
int *p=(int*) malloc(20);
if (p==NULL){
printf("%s\n", strerror(errno));
} else{
for (int i = 0; i < 5; ++i) {
*(p+i)=i;
}
}
//假设malloc 开辟的20个字节空间不够用
//我们期望有40个字节空间
//就可以使用realloc来调整动态开辟的内存。
int* p2= realloc(p,40);
int i=0;
for (int j= 0; j < 10; ++j) {
printf("%d ",*(p2+j));
}
free(p2);
p2==NULL;
return 0;
}
输出结果为:
前面5个数字我们初始化了,这是前20个字节的空间,后续我们使用realloc 又开辟了20个字节空间,后面没有初始化就是随机值,这里我的环境下编译器是0,换个平台可能是其他随机值。
另外注意:我在使用realloc的时候我用的是新的指针p2来存放realloc开辟的空间,并没有使用p继续去存储。如果使用p来存储realloc重新分配的空间是有很大风险的。
再说这个风险之前我们先分析一下realloc分配空间可能出现的情况:
realloc 再重新分配空间的时候如果你要用它新增空间,那么一定会出现两种情况。
情况1
重新分配所增加的空间 不影响后面其他程序开辟的空间
那么直接在这个空间后面追加这么大的空间即可,返回的地址还是p。
情况2
重新分配所增加的空间比较大,会影响到后续其他程序开辟的空间。那么此时realloc会在另外一个地方(能够满足所申请空间的大小的地方)去重新开辟一段空间,并且把原有的内容拷贝过来在返回一个新的地址。然后释放旧的也就是p指向的空间。此时空间的地址已经发生变化了。
风险
int* p3= realloc(p2,10*INT_MAX);
假设你需要使用realloc重新开辟的空间分配失败了(如上述代码,没有那么大的空间可以分配),那么就会返回一个空指针,你如果还用p去接收那么此时p就变成了NULL指针了。原本p还指向了20个字节,现在就找不到了这个已有的20字节的空间。
所以最好的办法要么是重新声明一个指针来存储realloc开辟的新的空间
或者判断一下:
int* p2= realloc(p,40);
int* p3= realloc(p2,10*INT_MAX);
if(p3!=NULL){
p2=p3;
p3=NULL;
}
如果没有开辟失败,再使用p2指向这个地址。
那么以上就是动态内存分配的含义以及一些函数的定义及使用。
后续还有
常见的动态内存错误、以及相关经典的笔试题、还有柔性数组
会在之后更新后面的内容。