一、为什么存在动态内存管理❓❓
我们常用的内存开辟方式有:
int i=0;//在栈空间开辟了4个字节
char array[10]={0};//在栈空间开辟了十个连续的字节
但是上述开辟空间的方法仍存在不足:
- 空间开辟大小是固定的。
- 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。
我们常常对于空间的需求是在程序运行的时候才能实现,那数组的编译时空间的方式就无法满足了,这时候就需要试试动态开辟空间了。
二、动态内存函数的介绍 💗💗
首先看一下内存空间的分配情况:
1、free函数🌟🌟
void free ( void* ptr );
free函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
- 如果参数 ptr 是NULL指针,则函数什么事都不做。
2、malloc函数⭐⭐
void* malloc ( size_t size );
这个函数向内存申请一块 连续可用 的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
应用演示:
#include<stdio.h> #include<string.h> #include<errno.h> int main() { int* p = (int*)malloc(40);//申请空间 if (p == NULL) { printf("%s", strerror(errno)); } int* ptr = p; for (int i = 0; i < 10; i++) { *ptr = i; ptr++; } free(p);//释放p所申请的内存空间 p = NULL;//避免误使用p所指的内存空间,发生内存冲突,下同 ptr = NULL; return 0; }
3、calloc函数✨✨
void* calloc ( size_t num , size_t size );
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
- 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
应用演示:
#include<stdio.h> #include<string.h> #include<errno.h> int main() { int* p = (int*)calloc(10, sizeof(int)); if (p == NULL) { printf("%s", strerror(errno)); } for (int i = 0; i < 10; i++) { *(p + i) = i; } free(p); p = NULL; return 0; }
当我们申请内存空间要求初始化,就可以使用calloc函数完成。
4、realloc函数💫💫
void* realloc ( void* ptr , size_t size );
- ptr 是要调整的内存地址
- size 调整之后新大小,而不是在ptr指针后再拓展size大小的空间。
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc函数在申请内存空间存在两种情况:
情况 1 :原有空间之后有足够大的空间解决方案:要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
情况2:原有空间之后没有足够大的空间
解决方案:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
应用演示:
#include<stdio.h> #include<string.h> #include<errno.h> int main() { int* p = (int*)malloc(40); int* ptr = (int*)ralloc(p, 80);//当realloc开辟失败的是,返回的是NULL if (ptr != NULL) { p = ptr; ptr = NULL; } for (int i = 10; i < 20; i++) { *(p + i) = i; } //释放 free(p); p = NULL; return 0; }
三、常见动态内存错误💥💥
1、对NULL指针的解引用操作🍃🍃
void test() { int *p = (int *)malloc(INT_MAX/4); *p = 20;//如果p的值是NULL,就会有问题 free(p); }
在malloc函数申请内存之后,并没有判断内存是否申请成功,若没有申请成功,这就会出现空指针,引发NULL指针解引用错误
2、对动态开辟内存的越界访问🌿🌿
void test() { int i = 0; int *p = (int *)malloc(10*sizeof(int)); if(NULL == p) { exit(EXIT_FAILURE); } for(i=0; i<=10; i++) { *(p+i) = i;//当i是10的时候越界访问 } free(p); }
本题利用malloc函数申请了10个整型内存,但是在遍历的时候却遍历了十一次,访问了不属于本身的内存空间,造成了对内存空间的越界访问
3、对非动态开辟内存使用free函数🍀🍀
void test() { int a = 10; int *p = &a; free(p); }
a的内存空间并不是动态内存分配的,这时候使用free函数就会出现错误,free函数只能释放动态开辟的内存。
4、使用free函数释放一块动态开辟内存的一部分🌵🌵
void test() { int *p = (int *)malloc(100); p++; free(p);//p不再指向动态内存的起始位置 }
p指针指向一块动态开辟的内存,但是在进行p++之后,p就不再指向动态开辟内存的首地址,但是free函数的参数是指一块动态内存的起始位置的指针,否则就会出现错误。
5、对同一块动态内存进行多次释放🌳🌳
void test() { int *p = (int *)malloc(100); free(p); free(p);//重复释放 }
在free函数释放一块动态内存之后,p就会变成空指针,再次调用free函数就会出现错误
6、对动态开辟的内存忘记释放造成内存泄漏🌲🌲
void test() { int *p = (int *)malloc(100); if(NULL != p) { *p = 20; } } int main() { test(); while(1); }
忘记释放不再使用的动态内存就会造成内存泄漏,所以在每次使用完动态开辟的内存时一定要正确释放动态开辟的空间,避免造成系统内存空间的浪费。
四、C/C++程序的内存开辟💓💓
C/C++开辟内存的分区:
栈区:在执行函数时,函数内局部变量的内存都在栈区内,函数执行结束后,这些内存就会被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
栈区内通常分配的是函数的局部变量、函数参数、返回数据、返回地址等。
堆区:一般是程序员申请使用完毕后释放,否则,程序结束后由OS回收内存空间,存储空间的分配方式类似于链表。
数据段(静态区):通常存放全局变量、静态数据,程序结束后由系统对内存进行释放。
代码段:存放函数体(类成员函数和全局函数)的二进制代码 。
五、柔性数组🌊🌊
结构中最后一个元素是未知大小的数组,这就叫做柔性数组成员。
例如:
struct S { int i; int a[0];//在有些编译器可能会报错,可以修改为 int a[] };
1、柔性数组的特点🐋🐋
- 结构中柔性数组成员之前至少有一个其他成员。
- sizeof计算结构大小时,不包含柔性数组的大小。
- 含有柔型数组成员的结构使用malloc函数分配内存,malloc函数分配的内存空间应大于结构的内存空间,以适应柔性数组的预期。
例如:
typedef struct st_type { int i; int a[0];//柔性数组成员 }type_a; printf("%d\n", sizeof(type_a));
运行结果为:4
2、柔性数组的使用🐳🐳
int i = 0; type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int)); p->i = 100; for(i=0; i<100; i++) { p->a[i] = i; } free(p);
使用malloc函数开辟空间时,柔性数组成员a获得了100个整型的内存空间。
3、柔性数组的优势🐬🐬
在不使用柔性数组的情况下,我们通常会将代码设计为如下:
typedef struct st_type { int i; int *p_a; }type_a; type_a *p = (type_a *)malloc(sizeof(type_a)); p->i = 100; p->p_a = (int *)malloc(p->i*sizeof(int)); //业务处理 for(i=0; i<100; i++) { p->p_a[i] = i; } //释放空间 free(p->p_a); p->p_a = NULL; free(p); p = NULL;
相比之下,我们可以看到使用柔性数组的优势为:
- 方便内存释放,如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
- 有利于提高访问速度, 有益于减少内存碎片。