这篇博客和大家讲一讲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++编程》,这真的是一本很好的书,真心推荐。
本篇博客到这里就结束啦,大家有什么疑问可以发在评论区或者私信我都可以。