C语言动态内存管理

为什么存在动态内存分配?
因为当前内存分配满足不了需求或者说有一定的局限性。
int main()
{
int num = 10;//向内存申请了4个字节
int arr[10];//向内存申请了40个字节
return 0;
}
上述的开辟空间的方式有两个特点:
C语言提供了一个动态内存开辟的函数:

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器。
#include
#include
#include
INT_MAX
int main()
{
//int arr[10];//向内存申请了40个字节
int* p = (int*)malloc(10*sizeof(int));
int* ptr = p;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*ptr = i;
ptr++;
}
//释放
free(p);
p = NULL;
ptr = NULL;
return 0;
}
切记,malloc函数返回的是一个void *的指针,如果我们要使用的话不太方便,比如我们想要++操作的话编译器就没办法确定每一次++的步长到底是多少,而且没有办法对void *指针进行解引用操作,所以最好我们对它进行强制类型转换成对应类型的指针,这样方便我们后续的操作。
如果内存分配不合理的话就会开辟内存失败返回一个NULL,所以我们每次使用之前都应该判断一下返回的指针是否为NULL。
在监视里面,我们输入p,10就可以从p这个地址开始向后面看十个元素。

局部变量和形式参数的使用都是在栈区开辟对应的空间,但是动态内存分配都是在堆区分配空间,使用的原则就是如果开辟的空间如果程序员不主动归还这块内存是一直存在的直到整个程序彻底结束,不会像在栈区创建的那样出了作用域自动销毁。所以我们就需要主动去归还或者说释放内存。
C语言提供了另外一个函数free,专门是用来做动态内存的释放和回收的,函数原型如下:

free函数用来释放动态开辟的内存。
如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。
如果参数 ptr 是NULL指针,则函数什么事都不做。
malloc和free都声明在 stdlib.h 头文件中。
举个例子:
#include
#include
#include
INT_MAX
int main()
{
//int arr[10];//向内存申请了40个字节
int* p = (int*)malloc(10*sizeof(int));
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*p = i;
p++;
}
//释放
free(p);
return 0;
}
上述代码是有问题的,因为随着代码的运行,p指针指向的位置已经不是所开辟内存空间的首地址了,所以我们直接free(p)是错误的,这个时候我们就要想一个办法完成程序的功能还要不改变指针p所指向的地址,我们就可以将指针p先赋给另外一个指针,然后对另外那个指针操作。
#include
#include
#include
INT_MAX
int main()
{
//int arr[10];//向内存申请了40个字节
int* p = (int*)malloc(10*sizeof(int));
int* ptr = p;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*ptr = i;
ptr++;
}
//释放
free(p);
return 0;
}
上述代码就可以成功free(p)
但是我们在free(p)之后发现,虽然内存空间还给了操作系统,但是指针p还是指向原来所开辟空间的首地址,p就是一个野指针,如果我们再对p进行操作就会造成非法访问内存,这个时候我们应该将NULL赋给指针p,让指针p忘记之前指向的地址。
其实我们可以认为free函数要传入的是开辟内存空间的首地址。
#include
#include
#include
INT_MAX
int main()
{
//int arr[10];//向内存申请了40个字节
int* p = (int*)malloc(10*sizeof(int));
int* ptr = p;
if (p == NULL)
{
printf("%s\n", strerror(errno));
return 1;
}
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*ptr = i;
ptr++;
}
//释放
free(p);
p = NULL;
ptr = NULL;
return 0;
}
一定要注意释放内存,防止内存耗干。因为不主动释放空间就必须等到程序结束才会自动归还给操作系统。
我们来写一个吃内存的代码:
int main()
{
while (1)
{
malloc(10);
}
return 0;
}
我们可以打开任务管理器来查看内存的变化情况,发现在运行代码时我们的内存一直在减少,但是还有一定的保护机制不会将内存彻底耗干。
函数定义:

函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。
与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。
//calloc
#include
int main()
{
//40个字节 - 10个整型
//malloc(40);
int* p = (int*)calloc(10, sizeof(int));//这里有一个技巧,就是直接用 sizeof(基本数据类型)确定每个元素的大小
if (p == NULL)
{
perror("calloc");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//释放
free(p);
p = NULL;
return 0;
}
为什么上述代码直接free(p)呢?
因为我们是*(p + i) = i;,p本身没有发生任何改变、
ps:calloc也有可能会开辟失败,失败的话也会返回一个NULL指针,我们在使用之前还是要判断一下返回的指针是否为NULL
1、参数不一样
calloc好像已经直到要开辟多少个空间,同时知道每个元素需要的空间大小
malloc函数的参数就是无符号整型,也就是要开辟的空间大小的字节数
2、calloc开辟的空间初值都是0,也就是说calloc开辟空间之后将内容全部初始化为0之后在返回一个地址,但是malloc函数开辟的空间里面存储的是随机值、直接返回一个地址。
总体来说malloc是不初始化的,而calloc会整体对开辟的空间初始化为0.
calloc=malloc+memset(将内存空间设置为0)
malloc因为少了一步初始化所以效率会相对更高一些,我们可以根据需要选择malloc还是calloc。

函数的功能是改变指针所指向内存空间的大小。
ptr 是要调整的内存地址,
size 调整之后新大小,
返回值为调整之后的内存起始位置。
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间
int main()
{
int* p = (int*)malloc(40);
if (p == NULL)
return 1;
//使用
int i = 0;
for (i = 0; i < 10; i++)
{
*(p + i) = i;
}
//
for (i = 0; i < 10; i++)
{
printf("%d ", *(p + i));
}
//增加空间
int* ptr = (int*)realloc(p, 80);
//当realloc开辟失败的是,返回的是NULL
//....
if (ptr != NULL)
{
p = ptr;
ptr = NULL;
}
for (i = 10; i < 20; i++)
{
*(p + i) = i;
}
//释放
free(p);
p = NULL;
return 0;
}

因为realloc返回的是一个void 型的指针,我们int ptr = (int*)realloc(p, 80);强制类型转化成int *型的,然后赋给一个新的指针ptr。
但是realloc函数返回的是调整之后的内存地址,这就会面临一些问题:
int* ptr = (int*)realloc(p, 80);
//我们之前这么写没有问题,但是我们发现指针p是这样定义的:
int* p = (int*)malloc(40);
//那么我们可不可以直接用指针p来接收不用指针ptr呢?
//答案是不可以的
//因为我们p之前指向的是40个字节的内存空间,
//realloc也有可能创建失败,如果失败返回的是NULL,我们不仅没有增加内存反而将之前
//p所指向的40个字节的内存也弄丢了,所以不可以这么做
//可以这样做
int* ptr = (int*)realloc(p, 80);
//当realloc开辟失败的是,返回的是NULL
//....
if (ptr != NULL) //如果ptr不等于NULL,我们再赋值给p指针,让p去操作
{
p = ptr;
ptr = NULL;
}
realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间

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

如果要增加空间,后面的空间够用直接增加空间就行返回原先的地址,如果不够用,就要重新开辟一个空间然后将原来的数据全部拷贝下来,再将原先的内存空间释放掉,再返回一个新开辟的空间的地址。
int main()
{
int*p = (int*)realloc(NULL, 40);//等价于malloc(40);
return 0;
}
如果realloc函数第一个参数是NULL,功能就类似与malloc
内存泄漏:就是你申请了一块空间使用完了之后也不对其释放,自己不再使用但是操作系统也没有办法对这块空间重新进行分配,这就是内存泄漏。
int test()
{
int*p = (int*)malloc(40);
if (p == NULL)
{
//
return 1;
}
//...使用
if (1)
return 2;
//释放
free(p);
p = NULL;
return 0;
}
//内存泄漏
int main()
{
test();
return 0;
}
malloc和free成对出现
calloc和free成对出现
但是即便他们成对出现也有可能造成内存泄漏,比如
1、提前return结束了函数没来得及执行到free就已经结束了就会造成内存泄漏,即便成对出现也没有办法。
2、还有一种就是出现一种情况然后没有带回开辟空间的地址,出了作用域,保存开辟空间起始地址的p被销毁了,也会导致程序错误。就比如上述代码,如果条件为真就return 2,然后p保存的是开辟的内存空间地址,但是没有返回这个指针反而返回了一个2,最后没有办法找到开辟空间到底在哪里。即便成对出现也会导致程序错误。
我们动态内存分配成功才会返回分配好的内存首地址,如果分配失败就会返回NULL,但是NULL是不可以进行解引用操作的。
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;//如果p的值是NULL,就会有问题
free(p);
}
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);
}
当发生越界访问的时候程序就会崩溃;
void test()
{
int a = 10;
int *p = &a;
free(p);//ok?
}
这里的p属于局部指针变量,存储在栈区,所以不能使用free释放。
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
但是如果第一次free之后将指针赋为NULL,再次free其实没有发生任何操作,也不会报错。
void test()
{
int *p = (int *)malloc(100);
free(p);
p=NULL;
free(p);//重复释放
}
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使用的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间一定要释放,并且正确释放
到这里动态内存管理就结束了:
在这篇博客里面写到了为什么要动态内存分配,malloc函数、calloc函数、realloc函数、free函数的使用,分析了动态内存管理常见的一些错误以及如何去处理,希望可以对大家有所帮助,有问题可以随时私信,如果有帮助的话麻烦各位大佬点点关注,后面会持续分享自己学习的心得。