1.为什么存在动态内存分配?
我们掌握的内存开辟方式:
int val=20; //在栈上开辟4个字节 char arr[10]={0}; //在栈上开辟10个字节的连续空间
但这种开辟方式空间大小固定,在数组声明的时候必须指定长度,它所需要的内存在编译时分配,这时就需要用到动态内存开辟。
2.动态内存函数:
>malloc和free:
malloc函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
void *malloc(size_t size); //调用实际生产空间比期望的大
---如果开辟成功,则返回一个指向开辟好空间的指针。
---如果开辟失败,则返回一个空指针,因此malloc的返回值一定要做检查。
---返回值的类型是void *,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
---如果参数size为0,malloc的行为是标准未定义,取决于编译器。
free,专门用来做动态内存的释放和回收:
void free(void *ptr); //释放前后地址不变,只是把指针和空间的关联取消。此时的指针为野指针(悬垂指针)
---如果参数ptr指向的空间不是动态开辟,那free的行为是未定义。
---如果ptr的NULL指针,则函数什么事都不做。
---开辟空间如果不释放就会发生内存泄漏。内存泄漏随着程序的关闭而结束。
malloc和free都声明在头文件中。
例子:
int main(){ //代码1 int num=0; scanf("%d",&num); int arr[num]={0}; //代码2 int *ptr=NULL; ptr=(int *)malloc(num*sizeof(int)); if(ptr){ int i=0; for(;i<num;i++){ *(ptr+i)=0; } } free(ptr); //动态释放内存 ptr=NULL; //释放后ptr再指向NULL,这一步很有必要 return 0; }
>calloc:
void *calloc(size_t num,size_t size);
---函数的功能是为num个大小为size的元素开辟一块空间,并且把空间的每个字节初始化为0。
---与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为0。
例:
int main(){ int *p=calloc(10,sizeof(int)); if(p){ //使用空间 } free(p); p=NULL; return 0; }
>realloc:
void *realloc(void *ptr,size_t size);
---realloc让动态内存管理更灵活,可以做到动态内存大小的调整。
---ptr要调整的内存大小
---size为调整之后的新大小
---返回值为调整之后的内存其实位置
---这个函数调整原内存空间大小的基础上,还会将原来内存的数据移动到新的空间。
---realloc在调整内存空间大小的是存在两种情况:
(1)原有的空间后有足够大的空间。
要扩展内存就直接在原有内存后追加内存原来的数据不发生变化
(2)原有的空间后没有足够大的空间。
扩展方法:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
例:
int main(){ int *p = malloc(100); if (p) { //使用空间 } else { exit(EXIT_FAILURE); } //扩展内存 //代码1 p = realloc(p,1000); //失败后将原来的空间为空,发生内存泄漏 //代码2 int ptr = NULL; ptr = realloc(p, 1000); if (ptr){ p = ptr; } free(p); p = NULL; return 0; }
2.常见的动态内存错误:
>对NULL指针的解引用操作
void test() { int *p=(int *)malloc(INT_MAX/4); *p=20; //如果p是NULL就会出问题 free(p); }
没有进行非空判断。
>对非动态开辟内存使用free释放:
void test() { int *a=10; *p=&a; free(p); }
free只用于动态内存开辟的释放。
>使用free释放一块动态开辟内存的一部分;
void test() { int *p=(int *)malloc(INT_MAX/4); p++; free(p); }
动态内存必须整体开辟,整体释放。
>对同一块动态内存多次释放:
void test() { int *p=(int *)malloc(INT_MAX/4); free(p); free(p); }
动态申请的内存不能重复释放。
>动态开辟内存忘记释放:
void test() { int *p=(int *)malloc(100); if(p) { *p=20; } } int main(){ test(); while(1); }
忘记释放不在使用的动态开辟的空间会造成内存泄漏。
4.经典的面试题:
>
void GetMemory(char *p) { p = (char *)malloc(100); } void test(void) { char *str = NULL; GetMemory(str); strcpy(str, "Holle world"); printf(str); }
问题解析:
传值之后把str传给p,然后p指向动态开辟的空间,申请的空间与str无关,造成内存泄漏。
解决方案:
char * GetMemory(char **p) { *p = (char *)malloc(100); return *p; } void test(void) { char *str = NULL; GetMemory(&str); strcpy(str, "Holle world"); printf(str); }>
char * GetMemory(void) { char p[] = "holle world"; return p; } void test(void) { char *str = NULL; GetMemory(&str); printf(str); }
运行test会出错。
因为函数定义的变量函数内有效,调用完后释放,这时调用GetMemory函数会产生栈帧,调完释放后,str还是指向p,然后调用printf函数也会产生栈帧,覆盖p所指向的栈帧。
>
void * GetMemory(char **p,int num) { *p = (char *)malloc(num); } void test(void) { char *str = NULL; GetMemory(&str,100); strcpy(str, "Holle world"); printf(str); }
会打印出holle world但是会出现内存泄漏。
这里有两点错误:没有判断是否申请成功和在用完后释放。
>
void test(void) { char *str = (char *)malloc(100); strcpy(str, "Holle world"); free(str); if (str) { strcpy(str, "Holle world"); printf(str); } }
free释放后指针会变成野指针,肯定不为空。
5.柔性数组
>在结构体中的最后一个元素是允许未知大小的数组,这就是[柔性数组]成员。
typedef struct st_type { int i; int a[0]; //柔性数组成员 }type; 或者 typedef struct st_type { int i; int a[]; //柔性数组成员 }type;
>柔性数组的特点:
---结构中的柔性数组成员前面必须至少一个其他成员。
---sizeof返回的这种结构体大小不包括柔性数组的成员。
---包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的大小应该大于结构体的大小,以适应柔性数组的预期大小。
typedef struct st_type { int i; int a[0]; //柔性数组成员 }type; printf("%d\n",sizeof(type)); //4
>柔性数组的使用:
int i= 0; type *p = (type *)mallloc(sizeof(type)+100*sizeof(int)); p->i = 100; for (i = 0; i < 100;i++){ p->a[i] = 1; } free(p);
这样柔性数组成员a,相当于获得了100个整形元素的连续空间。
这样做的好处是:
---方便内存的释放。
如果我们的代码是在给别人用的函数,你在里面做了第二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存及所有成员要的内存一次性分配好,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也释放掉。
---有利于访问速度