动态内存管理
1. 为什么要有动态内存管理
int val = 20;//在栈空间开辟四个字节的空间
char arr[10] = {0};//在栈空间开辟10个字节连续的空间
这是在栈上开辟的内存空间,一旦定义了,所开辟的内存空间就固定了,后续是无法修改内存空间的大小,但在实际应用中,我们定义时,大部分情况并不知道所需要开辟内存空间的大小。这时,就需要借用动态内存开辟了,这样,就可以自己按需要开辟空间,值得注意的是,动态开辟是在堆区开辟空间的,所以,动态开辟的空间后续如果不使用,一定要释放掉!
注:malloc,free,calloc,realloc都需要包含头文件stdlib!
2. malloc和free
2.1 malloc
函数malloc是用来开辟空间的,其函数原型如下:
这个函数向堆区申请了一块连续可用的空间,并返回指向这块空间的指针
- 如果开辟成功,则返回一个指向开辟好空间的指针
- 如果开辟失败,则返回NULL,因此当我们动态开辟空间后,需要检查是否开辟成功
- 默认返回类型为泛型指针void*,这也就意味着后续可以根据自己需要将其转化为其他类型的指针
- size为0是未定义的行为,取决于编译器
2.2 free
在开头,我们就了解到了,动态开辟的内存的空间后续如果不使用了,一定要手动释放掉!用malloc函数动态开辟空间,用什么来释放内存呢?答案就是free,free的函数原型如下:
注意:
- 这里的ptr一定要是申请内存空间的首地址,必须是起始地址!
- free只能释放动态开辟的空间
- 如果ptr是NULL,那么free什么都不做
举个例子:
int main()
{
int* arr = (int*)malloc(4 * sizeof(int));//开辟四个整型空间
if (arr == NULL)
{
perror("malloc");
exit(1);
}
for (int i = 0; i < 4; i++)
{
*(arr + i) = i;
printf("%d ", *(arr + i));
}
free(arr);//一定要记得释放,且要使用动态开辟空间的起始地址!
arr = NULL;//释放完空间后,该空间的使用权限已经还给了操作系统了,
//arr无权访问这篇空间了,也就需要将arr置为空指针!
return 0;
}
运行结果:
3. calloc和realloc
3.1 calloc
除了malloc,C语言还提供了一个动态开辟内存空间的函数叫calloc,其原型如下:
- calloc函数的功能和malloc唯一的区别就是,calloc会将所开辟内存空间的每个字节初始化为0
- calloc函数的参数与malloc不一样,num表示需要开辟空间的个数,size表示需要开辟的一个空间的大小
注意:参数里colloc是 *,malloc是 ,
话不多说,直接来看实例:
int main()
{
int* arr = (int*)calloc(4,sizeof(int));//开辟四个整型空间
if (arr == NULL)
{
perror("malloc");
exit(1);
}
//未赋值前
for (int i = 0; i < 4; i++)
{
printf("%d ", *(arr + i));
}
printf("\n");
//赋值后
for (int i = 0; i < 4; i++)
{
*(arr + i) = i;
printf("%d ", *(arr + i));
}
free(arr);
arr = NULL;
return 0;
}
运行结果:
在日常使用中malloc使用居多,如果需要给申请的内存空间初始化才使用calloc
3.2 realloc
有时候,我们在后续中发现之前动态申请的空间不够了或者太大了,需要对申请的动态内存进行调整,这时就可以使用realloc函数来调整动态申请的空间,realloc函数原型如下:
注:
- ptr表示需要调整的动态内存空间的起始地址
- size表示调整之后内存空间的新大小
补充: realloc除了可以实现灵活调整动态内存空间的大小,还可以实现类似于malloc的功能
realloc(NULL, size) == malloc(size) //两者等价!
传空指针给realloc,realloc就没法调整空间,就会帮你新开辟一块空间,大小为size
realloc调整动态空间时有下面3种情况
- 如果原有空间之后有足够的空间,就在原有空间之后在接上一块空间即可,原有空间的内容不改变,返回原有空间的起始地址
- 如果原有空间之后没有足够的空间,就在堆空间上另找一个合适大小的连续空间,再将原来空间的数据拷贝一份到新的空间,然后再释放旧的空间,返回新的内存空间的起始地址
- 调整失败,返回的是NULL
由于realloc调整空间时可能存在新开辟一块堆空间,使用realloc就需要注意一些
#include <stdio.h>
#include <stdlib.h>
int main()
{
int* arr = (int*)malloc(4 * sizeof(int));//开辟四个整型空间
for (int i = 0; i < 4; i++)
{
*(arr + i) = i;
printf("%d ", *(arr + i));
}
//代码1-直接将realloc的返回值放到arr中
arr = (int*)realloc(arr, 8 * sizeof(int));
//代码2-先将realloc的返回值放在临时指针变量temp中,如果返回值不为空,再将temp赋给arr
int* temp = (int*)realloc(arr, 8 * sizeof(int));
if (temp == NULL)
{
perror("realloc");
exit(1);
}
arr = temp;
free(arr);//一定要记得释放,且要使用动态开辟空间的起始地址!
arr = NULL;//释放完空间后,该空间的使用权限已经还给了操作系统了,arr无权访问这篇空间了,也就需要将arr置为空指针!
return 0;
}
代码1是不对的,请你思考以下,如果realloc调整失败会怎样?答案:arr原来指向的空间也找不到了,会造成数据的丢失!因此,代码1的处理方式是欠妥的,最好的方式是使用代码2!
4. 动态开辟内存忘记释放(内存泄漏)
#include <stdio.h>
void test()
{
int* p = (int*)malloc(100);
if (NULL != p)
{
*p = 20;
}
}
int main()
{
test();
while (1);
}
仔细分析这段代码,不难发现动态申请的空间没有释放,可能会导致内存泄漏
内存泄漏是指在程序运行过程中,动态分配的内存空间在不需要时没有被正确释放的过程情况。当程序中存在内存泄漏时,这些未释放的内存空间会一直占用系统资源,导致系统的可用内存逐渐减少,最终可能导致程序性能下降甚至崩溃
因此,在动态开辟空间后尽量做到:
- 谁申请的空间谁释放
- 如果不能释放,要告诉使用的人,记得释放
5. 动态内存经典笔试题分析
5.1 题目1
void GetMemory(char* p)
{
p = (char*)malloc(100);
}
void Test()
{
char* str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这道题存在的问题:
- 内存泄漏
- 程序崩溃
- GetMemory(str)是传值调用,GetMemory函数不影响str的值,str依然为NULL,而在strcpy函数中必定会涉及到对str的解引用操作,这会引起程序的崩溃!
- 在GetMemory函数结束后,指针变量p自身所占的栈区空间会被释放,而p所指向的动态内存空间(堆区)依然存在,这会导致无法找到动态开辟的空间,也就会造成内存泄漏的问题
可以更改为以下的代码:
char* GetMemory(char* p)
{
p = (char*)malloc(100);
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory(str);
strcpy(str, "hello world");
printf(str);
free(str);
str = NULL;
}
5.2 题目2
char* GetMemory()
{
char p[] = "hello world";
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
这道题是返回栈空间地址的问题
- p数组存放在栈区,当GetMemory函数结束后,p数组所占的内存空间就被释放了,这意味着内存空间中的内容也被清除了
- 等GetMemory函数返回后,使用str指针去访问p数组,就是非法访问了,因为p数组的内存已经还给操作系统了(注:还给了操作系统是没有这块空间的使用权限,但这块空间依然存在于操作系统)
解决方案:
- 将p数组动态分配内存
char* GetMemory()
{
char* p = (char*)malloc(20);
strcpy(p, "hello world");
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
free(str);
str = NULL;
}
动态分配的内存在堆区,需要使用free才会释放内存空间
- 将p数组声明为静态变量
char* GetMemory()
{
static char p[] = "hello world";//使用static
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
静态变量存放在静态区,生命周期是创建~主函数结束,程序结束才释放其所占内存空间
- 将p定义为一个常量字符串
char* GetMemory()
{
char* p = "hello world";//定义为常量字符串
return p;
}
void Test()
{
char* str = NULL;
str = GetMemory();
printf(str);
}
p指针指向了一个常量字符串,常量字符串在程序运行过程期间存储在常量区,GetMemory函数结束后不会被释放,直到程序结束后由系统释放
6. 综合应用
为一维数组分配动态内存
#include <stdio.h>
#include <stdlib.h>
int main()
{
int n = 0;
scanf("%d", &n);
int* arr = (int*)malloc(n * sizeof(int));
if (arr == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < n; i++)
{
arr[i] = i;
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
为二维数组分配动态内存
- 运用二级指针来动态分配二维数组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int rows = 0;
int cols = 0;
scanf("%d%d", &rows, &cols);
int** arr = (int**)malloc(rows * sizeof(int*));
int i = 0;
for (i = 0; i < rows; i++)
{
arr[i] = (int*)malloc(cols * sizeof(int));
}
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
arr[i][j] = i + j;
printf("%d ", arr[i][j]);
}
printf("\n");
}
free(arr);
arr = NULL;
return 0;
}
- 运用数组指针来动态分配二维数组
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int rows = 0;
int cols = 0;
scanf("%d%d", &rows, &cols);
int(*p)[cols] = (int (*)[])malloc(rows * sizeof(int[cols]));
if (p == NULL)
{
perror("malloc");
return 1;
}
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 5; j++)
{
p[i][j] = i + j;
printf("%d ", p[i][j]);//p[i][j]也可以写成*(*(p+i)+j)
}
printf("\n");
}
free(p);
p = NULL;
return 0;
}