在以前的C语言学习中,可以通过数组、二维数组的形式开辟空间。而上述方法不可避免的会出现以下情况:
- 1. 空间开辟大小是固定的。
- 2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
- 3. 开辟的空间在内存上,无法存储大量数据
但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道, 那数组的编译时开辟空间的方式就不能满足了,这时候就只能试试动态存开辟了。(当然,数据量足够庞大时还是需要以数据库的形式存储,再存储到内存上就不合适了)
目录
系统在接收到分配一定大小内存的请求时,首先查找内部维护的内存空闲块表,并且需要根据一定的算法(例如分配最先找到的不小于申请大小的内存块给请求者,或者分配最适于申请大小的内存块,或者分配最大空闲的内存块等)找到合适大小的空闲内存块。
如果该空闲内存块过大,还需要切割成已分配的部分和较小的空闲块。然后系统更新内存空闲块表,完成一次内存分配。类似地,在释放内存时,系统把释放的内存块重新加入到空闲内存块表中。如果有可能的话,可以把相邻的空闲块合并成较大的空闲块。
malloc和free
malloc是C语言中用来动态开辟内存的,通过malloc函数可以向计算机申请一串连续的内存空间。
malloc和free使用的基本方式:
void* malloc (size_t size);
void free (void* ptr);
- size是指定的开辟内存的大小,单位是字节
- size_t的无符号整型则限制程序员误操作开辟负字节的空间
- 如果开辟成功,malloc会返回一个void*类型的指针
- 如果开辟失败,则返回的是空指针,所以在malloc之后需要对指针进行检查
- 当malloc的东西不再使用时,需要free对其进行释放,否则会造成内存泄漏
- malloc和free均需要包含头文件<stdlib.h>
示例:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* pf = (int*)malloc(4);
if (pf == NULL)
{
return -1;
}
*pf = 10;
free(pf);
pf = NULL;
return 0;
}
malloc开辟了4个字节的空间,并且其返回值由void*类型的强制转换成int*类型,然后赋值给pf,pf通过解引用操作可以对其内存进行访问。
当pf不再使用时,用free(pf)释放掉pf指向的内存,并将pf置成空,防止后续继续访问造成野指针。
接下来演示关于malloc更加灵活的应用:
示例:存储学生学号
malloc开辟结构体大小内存
typedef struct class
{
int zhangsan;
int lisi;
int wangwu;
}class,*pclass;
int main()
{
pclass class1 = (pclass)malloc(sizeof(class));
if (class1 == NULL)
{
return -1;
}
class1->lisi = 210857;
class1->wangwu = 210858;
class1->zhangsan = 210859;
printf("%d %d %d\n", class1->lisi, class1->wangwu, class1->zhangsan);
return 0;
}
- 上述代码先声明了结构体class,然后用typedef对结构体进行重定义。
- sizeof(class)求的是一个结构体所占空间的大小。
- 开辟空间后,将其转换成pclass类型的指针,也就是struct class*类型的。
当学习完单链表后,可以以更加灵活的方式存储学生的信息,单链表中也需要malloc去开辟新节点的信息。
malloc实现一维、二维数组功能
在平时创建二维数组时,必须指定列数,而数组一旦开辟就不能更改其空间大小。而malloc开辟的空间还可以通过realloc重新生成内存。
示例:
实现一维数组功能:
int* arr = (int*)malloc(sizeof(int) * 10);//实现一维数组
int i, j;
if (arr == NULL)return -1;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
free(arr);
其赋值方式也跟一维数组一致,通过调试对arr进行观察,可以发现内存是连续且都被赋了值的。
实现二维数组的功能
int** arr2 = (int**)malloc(sizeof(int*) * 10);//实现二维数组
int i, j;
if (arr2 == NULL)return -1;
for (i = 0; i < 10; i++)
{
arr2[i] = (int*)malloc(sizeof(int) * 10);
}
if (*arr2 == NULL)return -1;
for (i = 0; i < 10; i++)
{
for (j = 0; j < 10; j++)
{
arr2[i][j] = i + j;
}
}
free(arr2);
malloc先开辟了10个int*类型的空间大小,并将返回值转换成int**类型,赋值给arr2。
通过for循环,对每个int*空间开辟10个int类型的空间大小。
对arr2进行打印,malloc确实实现了二维数组的功能。
通过上述代码发现,malloc开辟好空间后,其使用方式和数组的方式是一样的,而且相较于数组更加灵活,易于内存的管理。
realloc
realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那realloc 函数就可以做到对动态开辟内存大小的调整。
realloc函数原型如下:
void* realloc (void* ptr, size_t size);
- ptr 是要调整的内存地址
- size调整之后新大小
- 返回值为调整之后的内存起始位置
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间
realloc在调整内存空间的是存在两种情况:
1、原空间后续由足够大的空间。
2、原空间后续没有足够大的空间,需要重新申请一块内存,将原来内存中的数据拷贝到新内存中,并且将新内存的地址返回。
鉴于此,频繁的使用realloc重新开辟内存也会导致程序缓慢。
并且当重新申请的内存过大时,也会产生错误,例如:
int main()
{
int* a = (int*)malloc(40);
if (a == NULL)return -1;
a = (int*)realloc(a,1000000000000000000000);
free(a);
return 0;
}
这时编译器就会报警告:常量太大
calloc
C语言还提供了一个函数叫calloc,calloc函数也用来动态内存分配。
原型如下:
void* calloc (size_t num, size_t size);
- 函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
示例:
int main()
{
int* a = (int*)calloc(10,sizeof(int));
if (a == NULL)return -1;
free(a);
a = NULL;
return 0;
}
通过观察内存发现,所有变量已被初始化为0。
动态管理的不足
如果应用程序频繁地在堆上分配和释放内存,则会导致性能的损失。并且会使系统中出现大量的内存碎片,降低内存的利用率,这时会引入一个新的概念——内存池。
内存池:在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
这里先引入内存池的概念,在后续再对其进行详细讲解。
常见的动态内存错误
- 对NULL指针的解引用操作
- 对动态开辟空间的越界访问
- 对非动态开辟内存使用free释放
- 使用free释放一块动态开辟内存的一部分
- 对同一块动态内存多次释放
- 动态开辟内存忘记释放(内存泄漏)