本篇文章我要给大家梳理一下C语言中的动态内存管理相关知识。其中主要包括如何进行动态内存管理、一些常见的动态内存错误及柔性数组的介绍。
❤️ 博主码云gitee链接:https://gitee.com/byte-binxin ❤️
在此之前,我们基本都是在栈上开辟空间且开辟的空间大小也都是要明确指定的。
例如:
int val = 10;
这个变量的大小是在栈上开辟的,大小是4个字节。缺点大小是固定的。
int arr[10] = {0};
这个数组也是在栈上开辟的,大小是40个字节。缺点是数组大小要明确指定。这样会导致空间不能够按需所取。
显然,用数组开辟空间大小已经不能满足我们的需求了,这时候就产生了动态内存开辟这一说了。
malloc原型如下:
void *malloc( size_t size );
这个函数是向内存申请一块连续的空间,并且返回这块空间的指针。
这个函数要结合free一起使用,动态内存申请的空间要由使用者自己来手动释放,所以C语言就专门提供了一个free函数来释放和回收动态内存空间,函数原型如下:
void free( void *memblock );
使用这个函数需要注意几点:
举一个malloc和free使用的实例:
#include
#include
int main()
{
int n = 0;
scanf("%d", &n);
int *ptr = (int*)malloc(sizeof(int)* n);//开辟4*n个字节大小的连续空间
//检查空间是否申请成功
if (ptr == NULL)
{
perror("malloc fail:");
exit(-1);
}
int i = 0;
for (i = 0; i < n; i++)
{
*(ptr + i) = i;
printf("%d ", *(ptr + i));
}
printf("\n");
//释放和回收空间
free(ptr);
ptr = NULL;
return 0;
}
这个是正常运行。
这个是空间申请过大,然后报错,程序提前结束。
calloc原型如下:
void *calloc( size_t num, size_t size );
看一个实例:
int main()
{
int *ptr = (int*)calloc(10, sizeof(int));//开辟40个字节大小的连续空间
//检查空间是否申请成功
if (ptr == NULL)
{
perror("malloc fail:");
exit(-1);
}
//释放和回收空间
free(ptr);
ptr = NULL;
return 0;
}
realoc函数原型:
void *realloc( void *memblock, size_t size );
所以realloc函数在使用时,我们要注意其返回值不能直接用原指针接收,如果动态内存空间申请失败,那么原来那块空间也就找不到了,所以我们要创建一个新的指针变量来接收并检查指针是否为空,这样就能保证空间申请失败时元原空间不丢失,下面我们来看一个实例:
int main()
{
int *ptr = (int*)malloc(10*sizeof(int));//开辟40个字节大小的连续空间
//检查空间是否申请成功
if (ptr == NULL)
{
perror("malloc fail:");
exit(-1);
}
//扩展40个字节的空间
//创建一个临时指针变量接收新的空间地址
int* tmp = (int*)realloc(ptr, 10 * sizeof(int));
if (tmp == NULL)
{
perror("realloc fail:");
exit(-1);
}
ptr = tmp; //扩展成功就把新的空间地址给指向旧的空间的指针变量
//释放和回收空间
free(ptr);
ptr = NULL;
return 0;
}
#include
int main()
{
int* ptr = (int*)malloc(INT_MAX);
*ptr = 10;
free(ptr);
ptr = NULL;
return 0;
}
指针ptr未检查是否为空,直接使用造成对NULL进行解引用操作程序直接崩溃了。
int main()
{
int* ptr = (int*)malloc(10 * sizeof(int));
if (ptr == NULL)
{
perror("malloc fail");
exit(-1);
}
int i = 0;
for (i = 0; i <= 10; i++)
{
*(ptr + i) = i;//当i = 10时,指针越界访问,程序崩溃
}
free(ptr);
ptr = NULL;
return 0;
}
当i = 10时,指针越界访问,程序崩溃。
int main()
{
int arr[10] = {
0 };
int i = 0;
for (i = 0; i < 10; i++)
{
arr[i] = i;
}
free(arr);
return 0;
}
void test()
{
int *p = (int *)malloc(100);
p++;
free(p);//p不再指向动态内存的起始位置
}
指针p的内容发生了改变,只释放了一部分的内存空间 。程序用运行起来也是直接崩溃。
int main()
{
int *p = (int *)malloc(100);
if (p == NULL)
{
perror("malloc fail");
}
free(p);
free(p);
return 0;
}
程序再一次如我们所想地崩了。
这里就是在告诉我们要养成一个好的习惯,已经释放过的空间要记得置为NULL。
int main()
{
int *p = (int *)malloc(100);
if (p == NULL)
{
perror("malloc fail");
}
while (1);//程序一直停留在这里不结束,申请的那块空间就不会被释放,这样就导致了内存泄漏
return 0;
}
程序一直停留在循环那不结束,申请的那块空间就不会被释放,这样就导致了内存泄漏。
我们程序员自己申请的空间一定要自己手动释放
,养成一个好的习惯。加入一个服务器一天24小时都在跑,一直申请内存空间但又不是放,程序跑的时间长了,服务器就会崩了,直接结束程序,这是一个很危险的事情。
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);![请添加图片描述](https://img-blog.csdnimg.cn/a9c5179be36e45c6af526590e896e245.gif)
}
可以先思考一下这里有什么错误,str会打印预期的结果吗?
我们可以发现在Test函数内部,Getmemory中传了一个一级指针过去,GetMemory这个函数也是用以及指针接收这个参数,相当于
值传递
,也就是说在GetMemory函数内部,给指针p申请一块空间,ptr是得不到的,ptr的内容不会发生任何改变。所以Test函数内部进行字符串拷贝会由于目标字符串空间不足而导致程序崩溃。而且要记得free释放动态内存申请的空间
。
void GetMemory(char** p)
{
*p = (char *)malloc(100);//修改3
}
void Test(void)
{
char *str = NULL;
GetMemory(&str);//修改1
strcpy(str, "hello world");
printf(str);
free(str);//修改2
str = NULL;
}
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
看一下这串代码会发生什么?
我们来一步一步分析一下,首先创建了一个char类型的指针变量,然后用了接收
GetMemory
函数的返回值。GetMemory
函数内部创建了一个字符数组p,返回了首元素地址,与此同时,p指向的空间也被销毁了
,Test
函数内部的str接收了这个地址,其实str就是一个野指针了,因为它指向的空间已经被销毁了,然后打印的时候进行了访问,这就对野指针进行了访问操作,这是一个很危险的动作,会导致程序崩了。
void GetMemory(char** p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
这里其实就是
发生了内存泄漏
。动态内存开辟的空间没有释放。GetMemory
函数内部开辟了100个字节大小的空间给了str,str指向的空间在Test
函数内部使用后没有及时释放。造成了内存泄漏
。
void Test(void)
{
char *str = (char *)malloc(100);
strcpy(str, "hello");
free(str);
if (str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
str指针申请了一块大小为100个字节的空间,然后进行字符串拷贝,
"hello"
被拷贝到str指向的空间中,然后str指向的空间被free
释放和回收了,由于没有将str置为NULL
,str就变成了一个野指针。下面有再一次的对str进行使用,也就造成了野指针的访问
,程序同样会崩,这里又再一次告诫我们,被free
释放和回收后的指针一定要置空,你面造成不必要的麻烦。
在C99中,结构体最后一个元素是一个未知大小的数组,这就叫做柔性数组成员
。
例如:
//第一种
typedef struct st_type
{
int i;
int a[0];//柔性数组成员
}type_a;
//第二种
typedef struct st_type
{
int i;
int a[];//柔性数组成员
}type_a;
以上两种哪一种不报错就使用哪一种,因为这是根据编译器来选择的。
柔性数组实现方式:
typedef struct st
{
int i;
int arr[];//柔性数组成员
}st;
int main()
{
st* p = (st*)malloc(sizeof(st)+sizeof(int)* 10);
if (p == NULL)
{
perror("malloc fail");
exit(-1);
}
//开始使用
p->i = 100;
int i = 0;
for (i = 0; i < 10; i++)
{
p->arr[i] = i;
}
//如果不够,继续扩展
st* tmp = (st*)realloc(p, sizeof(st)+sizeof(int)* 20);
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
else
{
p = tmp;
}
for (i = 10; i < 20; i++)
{
p->arr[i] = i;
}
//释放回收空间
free(p);
p = NULL;
return 0;
}
typedef struct st
{
int i;
int* ptr;
}st;
int main()
{
st* p = (st*)malloc(sizeof(st));//先开辟一个结构体大小的空间
if (p == NULL)
{
perror("malloc fail");
exit(-1);
}
p->i = 100;
p->ptr = (int*)malloc(sizeof(int)* p->i);
if (p->ptr == NULL)
{
perror("malloc fail");
exit(-1);
}
int i = 0;
//使用
for (i = 0; i < p->i; i++)
{
p->ptr[i] = i;
}
//扩展空间
int* tmp = (int*)realloc(p->ptr, sizeof(int)* (p->i+10));
if (tmp == NULL)
{
perror("realloc fail");
exit(-1);
}
else
{
p->ptr = tmp;
}
for (i = 10; i < 20; i++)
{
p->ptr[i] = i;
}
//释放空间
free(p->ptr);
p->ptr = NULL;
free(p);
p = NULL;
return 0;
}
对比上面两种方法,柔性数组有两个优势:
第一个优势:第一种方法只需要释放一次内存空间,方便内存释放
,但第二种方法要进行两次free
,不方便释放。
第二个优势:由于空间连续,有利于内存访问,所以访问速度快
。连续的内存有益于提高访问速度,也有益于减少内存碎片。内存碎片化会导致右下空间不可用。所以柔性数组还是比较有优势的。