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就可以把所有的内存也释放掉。
---有利于访问速度