说到内存,大家一定都知道。但是有一种函数可以实现动态内存管理,下面大家一起学习。
文章目录
- 一、为什么要有动态内存管理?
- 二、malloc 和 free
- 1.malloc
- 2.free
- 三、calloc 和 realloc
- 1.calloc
- 2.realloc
- 3.常见的动态内存的错误
- 3.1对NULL指针的解引用错误
- 3.2对动态开辟空间的越界访问
- 3.3对非动态开普内存使用free释放
- 3.4 使用free释放一块动态开辟内存的一部分。
- 3.5 对同一块动态内存多次释放。
- 3.5 动态开辟内存忘记释放(内存泄漏)
- 4.经典笔试题分析
- 4.1
- 4.2
- 4.3
- 4.4
- 四、柔性数组
- 1.柔性数组的特点
- 2.柔性数组的使用
一、为什么要有动态内存管理?
经过c语言的学习,相信大家都知道如何开辟一块内存。
int i = 6;
int arr[10]={0};
上面代码开辟的空间,有两个特点:
- 空间开辟大小是固定的。
- 数组在申明的时候必须指定数组的长度,数组空间一旦确定大小就不能调整了。
这时候我们会发现当我们写代码的时候,我们需要开辟多少空间在程序运行的时候才能知道。这时候就需要引入动态内存开辟了,让程序员自己申请和释放空间。
二、malloc 和 free
1.malloc
void* malloc (size_t size);
这个函数向内存申请一开连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
- 返回值的类型是void*,所有malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
- 如果参数size为0,malloc的行为是标准的未定义,取决于编译器。
这个代码用了malloc函数申请空间,先是申请了20个字节的空间强制转换为整形变为存放5个整数。
2.free
void free (void* ptr);
函数free,是专门用来给动态内存的释放和回收的。
- 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为毫无意义。
- 如果参数ptr是NULL指针,则函数什么事都不做。
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 0;
scanf("%d", &num);
int arr[num] = {0};
int* ptr = NULL;
ptr = (int*)malloc(num*sizeof(int));
if(NULL != ptr)//判断ptr指针是否为空
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr);/释放ptr所指向的动态内存
ptr = NULL;//是否有必要?
return 0;
}
需要注意的是,使用free函数释放内存后,我们不能再访问该内存块,否则会导致未定义的行为。此外,如果我们尝试释放已经释放过的内存块,或者释放非动态分配的内存块,也会导致未定义的行为。当动态内存使用完毕之后,用free释放,释放后的指针是野指针,记得置空。
三、calloc 和 realloc
1.calloc
void* calloc (size_t num, size_t size);
calloc函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节置为0。
calloc函数与malloc函数的区别只在于calloc会在地址返回之前把申请的空间全部置为0。
输出结果全为 0;
当我们需要对申请的内存空间的内容进行初始化的时候,calloc函数就很方便的解决了这一问题。
2.realloc
void* realloc (void* ptr, size_t size);
realloc函数的出现让动态内存管理更加灵活。
有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的使⽤内存,我们⼀定会对内存的⼤⼩做灵活的调整。那realloc函数就可以做到对动态开辟内存大⼩的调整。
-
ptr是要调整的内存地址。
-
size是调整之后新的大小。
-
返回值为调整之后的内存起始位置。
-
这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
-
realloc函数调整内存的是存在两种情况:
1. 原有空间之后有足够大的空间。 2. 原有空间之后没有足够大的空间。
这种情况当原有空间后面内存正好可以放下我们扩展的内存的时候。扩展内存就直接再原有内存之后直接追加空间,原有空间数据不发生变化。
这种情况当原有空间之后没有足够多的空间时,扩展方法时:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。
3.常见的动态内存的错误
3.1对NULL指针的解引用错误
void test()
{
int *p = (int *)malloc(INT_MAX/4);
*p = 20;
free(p);
}
这个代码没有判断p是否为空指针,如果对空指针解引用可能会出现非法访问的错误。
3.2对动态开辟空间的越界访问
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;
}
free(p);
}
当i是10的时候,指针已经越界访问了。
3.3对非动态开普内存使用free释放
void test()
{
int a =10;
int* p=&a;
free(p);
}
要知道栈上的内存是不需要程序员手动释放的,只有堆上的内存需要程序员手动释放。
释放堆上内存的主要原因:
- 避免内存泄漏:如果我们不释放动态分配的内存,那么这块内存将一直被程序占用,无法被其他部分使用。如果重复分配内存而不释放,最终会导致内存耗尽,程序崩溃或运行缓慢。
- 释放不再使用的内存:在程序执行过程中,可能会动态地分配和释放内存。当我们不再需要某个内存块时,通过释放它,可以将其返回给操作系统,以便其他程序可以使用这块内存。
- 防止悬空指针:如果我们释放了一块内存,但仍然保留了指向该内存的指针,那么这个指针就成为了悬空指针。使用悬空指针可能导致未定义的行为,例如访问无效的内存,引发程序崩溃或产生不可预测的结果。通过释放内存并将指针置为,可以避免悬空指针的问题。
- 提高内存利用率:释放不再使用的内存可以提高内存的利用率。这对于内存有限的嵌入式系统或需要处理大量数据的应用程序尤为重要。
不需要释放栈上内存的原因:
1. 自动管理:栈上的内存分配和释放是由编译器自动完成的,程序员不需要手动干预。当函数执行完毕或者局部变量超出作用域时,编译器会自动将其所占用的内存空间释放。
2. 先进后出:栈是一种后进先出(LIFO)的数据结构,栈上的内存分配和释放遵循这个原则。每次函数调用时,会将局部变量和参数压入栈中,当函数返回时,会将这些变量从栈中弹出,实现内存的自动释放。
3. 快速高效:由于栈上的内存分配和释放是由编译器自动完成的,所以速度非常快。相比于堆上的内存分配和释放,栈上的内存管理更加高效。
3.4 使用free释放一块动态开辟内存的一部分。
void test()
{
int *p = (int *)malloc(100);
p++;
}
free(p);
这个代码p指针发生变化,p指针不在指向这块内存的起始位置,那么最后free只能释放一部分内存造成内存泄漏。
3.5 对同一块动态内存多次释放。
void test()
{
int *p = (int *)malloc(100);
free(p);
free(p);//重复释放
}
对同一块动态内存多次释放可能会造成内存泄漏、悬空指针、程序崩溃。
3.5 动态开辟内存忘记释放(内存泄漏)
void test()
{
int *p = (int *)malloc(100);
if(NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while(1);
}
忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。
4.经典笔试题分析
4.1
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
在GetMemory函数中,用malloc函数为p重新分配了内存,但是这个分配的内存并没有被传回函数中,因为在main函数中函数参数是按值传递的,所以在GetMemory函数中重新分配内存并不会改变str指针在main函数中的值。
在main函数中,str指针被初始化为NULL传递给GetMemory函数。由于GetMemory函数中的参数是按值传递的,所以GetMemory函数内部对p指针的修改不会影响到str指针。所以str指针仍然是NULL指针,在后面的strcpy和printf中会导致对空指针的解引用,产生未定义的内容。
4.2
char *GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char *str = NULL;
str = GetMemory();
printf(str);
}
这个代码中GetMemory函数是创建在栈上的,里面的char数组也是栈空间。随着函数的释放,返回到main函数的虽然是这个数组的首地址,但是str已经无权访问了变成了空指针。虽然有可能代码最后会输出hello word ,那是因为这个空间还没有被系统的其他空间所覆盖。
4.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);
}
传址调用,并且 GetMemory函数用二级指针**p接收。在GetMemory函数释放空间的时候,成功的传回了动态内存的首地址使得str不在是空指针,可以进行strcpy和printf。但是没有释放p。
4.4
void Test(void)
{
char *str = (char *) malloc(100);
strcpy(str, "hello");
free(str);
if(str != NULL)
{
strcpy(str, "world");
printf(str);
}
}
过早释放动态,没有了对动态内存空间的访问权限。后面的world无法拷贝。
四、柔性数组
C99中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。
struct st_type
{
int i;
int a[];//柔性数组成员
};
1.柔性数组的特点
1.结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
2.sizeof返回的这种结构⼤⼩不包括柔性数组的内存。
3.包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。
2.柔性数组的使用
struct S
{
int n;//4
int arr[];
};
int main()
{
struct S* ps = (struct S*)malloc(sizeof(struct S) + 5*sizeof(int));
if (ps == NULL)
{
perror("malloc");
return 1;
}
ps->n = 100;
int i = 0;
for (i = 0; i < 5; i++)
{
ps->arr[i] = i;
}
//调整空间
struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+10*sizeof(int));
if (ptr != NULL)
{
ps = ptr;
}
//....
//释放
free(ps);
ps = NULL;
return 0;
}
到这里动态内存管理基本就结束了,大家如果有问题可以一起讨论。谢谢大家!