C语言中的动态内存管理

这篇博客和大家讲一讲C语言中的动态内存管理。在C语言中我们知道指针很重要,同样的自定义类型中的结构体和动态内存管理也很重要,数据结构是严格依赖这三块知识点的。

1. 为什么要有动态内存分配

我们已经掌握的内存开辟方式有创建变量:

int a;//在栈空间申请4个字节
char ch[6];在栈空间上申请6个字节

虽然我们可以通过上面的方法来开辟空间,但他们有两个特点:

1.空间开辟大小是固定的

2.数组在申明的时候,必须指定数组的长度,数组的大小一旦确定就不可以改变了

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知
道,那数组的编译时开辟空间的方式就不能满足了。比如:我们用一个数组储存办公室教师的手机号,学校又请来了几个老师,我们添加信息的时候不能翻回我创建的时候再一个一个地添加吧?这时候就有人说C99中有个变长数组,用它不行吗?变长数组只是说数组的大小可以使用变量来指定,一旦创建好,大小也是不能修改的。申请的空间大小不能灵活地调整,这时候我i们该怎办呢?C语言引入了动态内存开辟,让程序员自己可以申请和释放空间,就比较灵活了。

2.malloc和free函数

2.1malloc函数 

C语言提供了一个动态内存开辟的函数:malloc函数(需要使用头文件是<stdlib.h>)。malloc函数是用来申请内存的:

void * malloc(size_t size);

 这个函数向内存申请⼀块连续可用的空间,并返回指向这块空间的指针。如果开辟成功,则返回⼀个指向开辟好空间的指针。如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。举个例子:我们申请40个字节的空间来打印0到9:

#include<stdlib.h>
#include<stdio.h>
int main()
{
  int *p=(int*)malloc(10*sizeof(int));//申请40个字节的空间
  if(p==NULL)
    {
      perror("malloc");//有错误信息打印出来
      return 1;
    } 
  int i;//使用
  for(i=0;i<=9;i++)
    {
      *(p+i)=i;
    }
  for(i=0;i<=9;i++)
    {
       printf("%d ",p[i]);
    }
  return 0;
}

malloc函数是在堆区上申请内存的:

 

2.2 free函数 

malloc函数申请的空间怎么回收呢?

1.free函数回收

2.自己不释放的时候,程序结束后,由操作系统回收 

 有的人就说了,程序结束后,操作系统回收内存,那我还用什么free函数啊?想象一下一个系统24小时都在运行,我们一直申请空间从不回收,内存就有可能被用完,程序会崩溃。我们申请的空间当我们不需要的时候,我们要主动使用free来回收,free函数专门是用来做动态内存的释放和回收的:

void free(void* ptr);

如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。如果参数 ptr 是NULL指针,则函数什么事都不做。malloc和free都声明在 stdlib.h 头⽂件中。我们将上面使用malloc函数申请空间打印0到9的程序使用free函数来回收空间:

#include<stdlib.h>
#include<stdio.h>
int main()
{
  int *p=(int*)malloc(10*sizeof(int));
  if(p==NULL)
    {
      perror("malloc");
      return 1;
    } 
  int i;
  for(i=0;i<=9;i++)
    {
      *(p+i)=i;
    }
  for(i=0;i<=9;i++)
    {
       printf("%d ",p[i]);
    }
  free(p);
  p=NULL;
  return 0;
}

我们调试一下,然后监视p的值:

上面是在free函数释放空间之前,那释放后呢?

 

我们可以看到p已经是一个野指针了,所以我们要及时给它赋个空值NULL。 

3. calloc和realloc函数

3.1 calloc函数 

C语言还提供了⼀个函数叫 calloc , calloc 函数也用来动态内存分配:

void* calloc(size_t num,size_t size);

函数的功能是为 num 个大小为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。我们一起看下面的代码来了解:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int *p=(int*)calloc(10,sizeof(int));
  if(p==NULL)
   {
     perror("calloc");
     return 1;
   }
  int i=0;
  for(i=0;i<=9;i++)
   {
     printf("%d ",p[i]);
   }
  free(p);
  p=NULL;
  return 0;
}

所以如果我们对申请的内存空间的内容要求初始化,那么可以很方便的使用calloc函数来完成任务。 

3.2 realloc函数 

 realloc函数的出现让动态内存管理更加灵活。有时会我们发现过去申请的空间太小了,有时候我们⼜会觉得申请的空间过大了,那为了合理的时候内存,我们⼀定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。

void *realloc(void* ptr,size_t size);

ptr 是要调整的内存地址,size 调整之后新大小,返回值为调整之后的内存起始位置。这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。我们就先以下面的代码来了解realloc函数如何调整申请空间的大小:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int *p=(int*)malloc(5*sizeof(int));//先用malloc函数申请20个字节的空间
  if(p==NULL)
   {
    perror("malloc");
    return 1;
   }
  int i=0;
  for(i=0;i<=9;i++)
   {
    *(p+i)=i;
   }
  //这时候我想再申请20个字节的空间
  realloc(p,40);//将申请的空间大小从20个字节调整到40个字节
  //....省略一系列操作
  free(p);
  p=NULL;
  return 0;
}

realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有足够大的空间

情况2:原有空间之后没有足够大的空间

 

当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。

当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找⼀个合适大小的连续空间来使用。这样函数返回的是⼀个新的内存地址,旧的地址就会被释放掉。
realloc函数调整大小的时候,也有可能申请空间失败(申请的空间特别大时),这时候他也会返回NULL。当realloc函数的第一个值是NULL时,功能类似malloc函数。

4. 常见的动态内存错误

4.1 对NULL指针解引用操作

我们知道malloc、calloc、realloc函数在开辟/调整失败的时候,会返回NULL,如果我们没有进行相关的判断就有可能解引用这个指针,我们看下面的代码:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int *p=(int*)malloc(20);
  int i=0;
  for(i=0;i<5;i++)
   {
     *(p+i)=i;
   }
  return 0;
}

这种写法就可能存在一个问题:万一malloc函数在开辟空间失败的时候,返回空指针,p是空指针的时候,i等于0的时候,*(p+i)就相当于对空指针解引用,就导致了我们这种错误。

4.2 对动态开辟空间的越界访问

我们在使用malloc、calloc、realloc函数开辟/调整空间时也有可能会存在越界访问的问题,我们看下面的代码:

#include<stdio.h>
#include<stdlib.h>
int main()
{
  int *p=(int*)malloc(20);//在这里我们申请20个字节的空间
  if(p==NULL)
   {
     perror("malloc");
     return 1;
   }
  int i=0;
  for(i=0;i<=5;i++)//这里是为了给大家展示错误,本来应该是i<5,但不小心写成i<=5的情况
   {
     *(p+i)=i;
   }
  free(p);
  p=NULL;
  return 0;
}

这里就相当于我们像空间申请5个整形的空间,用p来维护,当i=5的时候,*(p+i)就会造成越界访问。

4.3 对非动态开辟内存使用free释放

int main()
{
  int a=10;
  int *p=&a;
  //...经过一系列操作

  free(p);
  p=NULL;
  return 0;
}

我们在上面先使用了p指针,经过一系列的操作,忘了它是非动态开辟的内存,在结束的时候使用free函数释放了,这时候我们的编译器会报出警告窗口,我们不能任何一个指针指向的空间都free。

4.4 使用free释放一块动态开辟内存的一部分

int main()
{
  int *p=(int*)malloc(40);//申请40个字节的空间
  if(p==NULL)
   {
     perror("malloc");
     return 1;
   }
  inr i=0;
  for(i=0;i<5;i++)
   {
     *p=i+1;
     p++;
   }//我只想打印1到5,多余的空间想释放掉
  free(p);
  p=NULL;
  return 0;
}

就像上面一样我想把多的地方释放掉,直接使用free(p),这种写法是错误的,它是从p的起始位置开始释放,也就是说整个空间都释放掉了。所以这种写法是错误的。

4.5 对同一块动态内存多次释放

int main()
{
  int *p=(int*)malloc(20);
  if(p==NULL)
   {
     perror("malloc");
     return 1;
   }
  //...

  free(p);
  //...

  free(p);//忘记上面释放又释放了一次
  return 0;
}

犯这种错误,在我们运行程序的时候程序会崩掉,大家一定要注意这种情况,如何避免这种情况呢?当我们释放完后,就给p赋个NULL,当p为NULL时,再释放对程序没有任何影响。

4.6 动态开辟内存忘记释放(内存泄漏)

void test()
{
  int *p=(int*)malloc(20);
  if(p==NULL)
   {
     return;//为NULL直接返回
   }
  //使用  
  if(1==1)
   {
     return;//如果满足直接返回下面的free根本没机会释放
   }
  free(p);
  p=NULL;
}
int main()
{
  test();
  return 0;
}

我们在main函数种调用test函数申请空间,如果申请失败直接返回,对程序没有任何影响,如果申请成功,在下面进行操作的时候,提前返回了,p申请的局部变量就销毁了,而且p存的地址也没有带回来,那我们申请的空间没来的及释放就会造成内存泄漏。

5. 动态内存经典试题

5.1 试题1:

void GetMemory(char *p)
{
  p = (char *)malloc(100);
} 
void Test(void)
{
  char *str = NULL;
  GetMemory(str);
  strcpy(str, "hello world");
  printf(str);
}
int main()
{
  Test();
  return 0;
}

请问运行Test函数会有怎样的结果?能否打印出"hello world"?答案是不能的。

首先进入main函数,调用Test函数,当我们进入test的时候创建了一个变量str,给它赋了个NULL,然后调用GetMemory函数,把str传给p,接着使用malloc函数申请了100个字节的空间,p最终指向图中红色的区域,假设红的区域的地址为0x012FFCF4,开辟好空间之后GetMemory函数就结束了,然后他就要返回,当GetMemory函数返回时p就要销毁,红色区域的空间还没有被释放,虽然它属于当前程序,但是它的地址也就获取不到了,然后接着执行test函数,这时候str依然是NULL,程序要把"hello  world"拷贝到空指针,在strcpy会对空指针解引用,这时候我们的程序会崩溃。在这里我们基本可以断定两个点:1.对空指针进行解引用,导致程序崩溃。 2.malloc函数申请的空间内有机会被free函数释放掉,导致内存泄露。这是两个致命的错误,如果我们期望刚才的"hello world"拷贝到申请的100个字节空间中,我们可以这么改正:

void GetMemory(char **p)
{
  *p = (char *)malloc(100);
} 
void Test(void)
{
  char *str = NULL;
  GetMemory(&str);
  strcpy(str, "hello world");
  printf(str);
}
int main()
{
  Test();
  return 0;
}

5.2 试题2:

char *GetMemory(void)
{
  char p[] = "hello world";
  return p;
}
void Test(void)
{
  char *str = NULL;
  str = GetMemory();
  printf(str);
}
int main()
{
  Test();
  return 0;
}

 请问运行Test函数会有怎样的结果?能否打印出"hello world"?也是不可以滴。

 在这里我们先进入main函数,然后调用Test函数,当我们进入Test函数时创建一个char*类型的指针变量str,同时赋个NULL;接着调用GetMemory函数,这个函数的返回值会放到str中去,在Getmemory函数内部创建一个数组并在其中放入"hello world",p是这个函数中的局部变量,return p,p表示数组首元素的地址,假设h的地址为0x012FFCF4,str中的地址也是0x012FFCF4,str其实有能力找到h,遗憾的是这个数组p是个局部数组,一旦返回p就会销毁,也就是说图中蓝色的区域就会还给操作系统,此时str记录的地址也就没用了,在下面的运行中会造成非法访问。这属于返回栈空间地址的问题。

5.3 试题3:

void GetMemory(char **p, int num)
{
  *p = (char *)malloc(num);
} 
void Test(void)
{
  char *str = NULL;
  GetMemory(&str, 100);
  strcpy(str, "hello");
  printf(str);
}
int main()
{
  Test();
  return 0;
}

请问运行Test函数会有怎样的结果?能否打印出"hello"?这次是可以的,虽然它能打印出来但还是有问题:没有使用free函数来释放。它和试题1的改造非常像。首先进入main函数,然后调用Test函数,当我们进入Test函数的时候创建一个char*类型的指针变量str,同时赋个NULL,然后将str的地址传给Getmemory函数,因为str是char*类型,所以p是char**类型,我们对p解引用,*p就是str,这样GetMemory函数就使得str申请到了100个字节的空间,然后strcpy函数将"hello"拷贝到str上。

5.4 试题4:

void Test(void)
{
  char *str = (char *) malloc(100);
  strcpy(str, "hello");
  free(str);
  if(str != NULL)
   {
    strcpy(str, "world");
    printf(str);
   }
}
int main()
{
  Test();
  return 0;
}

请问运行Test函数会有什么样的结果?思考一下它会不会打印出world?它会打印出,但是它也有相当大的问题:非法访问了。老样子进去main函数调用Test函数,给str申请100个字节的空间,然后把"hello"拷贝到str中,然后free释放掉,这里我们仅仅是释放掉,str仍然保存着100个字节空间的起始地址,str没有赋值NULL,此时它已经是个野指针了,野指针不等于NULL,然后把"world"拷贝进去,这时候其实已经形成了非法访问。所以我们在释放空间之后要及时给指针赋值NULL。

其实上面的4个试题出自《高质量C/C++编程》,这真的是一本很好的书,真心推荐。

本篇博客到这里就结束啦,大家有什么疑问可以发在评论区或者私信我都可以。
 

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

野生的编程萌新

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值