导言
在C语言学习阶段,指针、结构体和动态内存管理,是后期学习数据结构的最重要的三大知识模块,也是C语言比较难的知识模块,但是“天下无难事”,只要认真踏实的学习,也能解决,所以下文将介绍动态内存管理涉及到的一些函数以及概念。
目录
为什么存在动态内存管理
int a;
char arr[10];
这是我们常用的用于向内存申请空间的办法,但是:
在实际编写程序时,可能我们对于内存空间的需求不是固定,那么使用动态内存管理自己申请空间、自己释放空间就是一个很好的选择。
malloc和free
malloc
函数参数及其返回值
void* malloc(size_t size);
//申请size个字节的空间
//返回值,成功申请:返回开辟空间的首地址、失败:返回NULL
注意点
使用举例:
free
函数参数及其返回值
void free(void* ptr);
//释放动态内存申请的ptr指向的空间
注意点
使用举例:
#include<stdlib.h>
int main() {
int* ptr = NULL;
int count = 0;
scanf("%d", &count);
ptr = (int*)malloc(count * sizeof(int));
free(ptr);
ptr = NULL;
return 0;
}
calloc和realloc
calloc
函数参数及其返回值
void* calloc(size_t num,size_t size);
//申请num个size个字节的空间,并初始化为0
//返回值,成功申请:返回开辟空间的首地址、失败:返回NULL
注意点
使用举例:
realloc
函数参数及其返回值
void* realloc(void* ptr,size_t size);
//ptr是要调整的内存地址
//size是调整之后的大小
//返回值,成功申请:返回调整空间的首地址、失败:返回NULL
使用举例:
#include<stdlib.h>
#include<stdio.h>
int main() {
int count;
scanf("%d", &count);
int* ptr = (int*)calloc(count, sizeof(int));//申请count个int大小的空间
if (ptr) {//判断是不是NULL:是否申请成功
for (int i = 0; i < count; i++)
ptr[i] = i;//赋值:从0开始到count-1步为1的序列
for (int i = 0; i < count; i++)
printf("%d ", ptr[i]);
}
printf("\n");
printf("调整前的地址:%p\n", ptr);//观察动态(realloc)调整前的地址
int* p = (int*)realloc(ptr, (count + 5) * sizeof(int));
//申明一个新指针来接收,防止调整失败返回NULL,数据丢失,调整为多5个int大小的地址
if (p)//判断是否是NULL:是否调整成功
ptr = p;
printf("调整后的地址:%p", ptr);//观察动态(realloc)调整后的地址
free(ptr);
ptr = NULL;
return 0;
}
运行结果:
先开辟10个int字节大小空间的运行结果:
先开辟20个int字节大小空间的运行结果:
注意点
其实在前面的使用举例中我们已经观察到:
先开辟10个int字节大小空间的运行结果(第一种情况)
直接在原地址后面开辟新空间
先开辟20个int字节大小空间的运行结果(第二种情况)
找到一块能容纳调整后的空间的地址,将数据移动到其中
关于参数size为0时的举例:
因为我们没有办法直接观察一块动态开辟的内存是否被释放,且这种size为0行为是未定义的,所以我们只能观察它的返回值
关于参数ptr是NULL时的情况
此时realloc等同malloc
注意:动态内存管理的4个函数都包含在<stdlib.h>中
常见的关于动态内存管理错误
1.对可能是NULL指针的引用
2.对不是动态开辟的内存进行释放
// 2.对不是动态开辟的内存进行释放
#include<stdio.h>
#include<stdlib.h>
int main() {
int a = 0;
int* p = &a;
free(p);
p = NULL;
return 0;
}
3.对动态开辟的内存进行越界访问
//3.对动态开辟的内存进行越界访问
#include<stdlib.h>
int main() {
int* p = (int*)malloc(sizeof(int));
p++;
*p = 1;
free(p);
p = NULL;
return 0;
}
4.使用free释放动态开辟内存的一部分
//4.使用free释放动态开辟内存的一部分
#include<stdlib.h>
int main() {
int* p = (int*)malloc(4*sizeof(int));//动态开辟4个int大小的空间
p++;//指向第二个元素
free(p);
p = NULL;
return 0;
}
5.忘记内存释放(忘记free),造成内存泄漏
//5.忘记内存释放(忘记free),造成内存泄漏
#include<stdlib.h>
int main() {
int* p = (int*)malloc(sizeof(int));
return 0;
}
例题
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;
}运行会咋样
p虽然在GetMemory函数中开辟了内存,但是在出函数时,该地址被销毁,所以str还是NULL指针,对NULL指针进行赋值是一个未定义行为。(传值调用而没有使用传址调用)
改正(二级指针):
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;
}
改正(将开辟的空间返回):
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;
}//运行结果?
GetMemory函数返回了一个地址,但是这个地址出了函数,权限已经收回给了操作系统,str接收的是一个野指针,并将它打印出来,这种行为是未定义的,可能造成错误。(说到底是栈空间返回会被销毁的问题)
我们知道只要是函数内的变量都是栈空间申请的空间,在出函数时,都会被回收,但是动态内存管理申请的空间,必须要手动释放,所以在函数中我们使用动态内存申请的地址是不会被收回的(堆区申请),所以我们尝试改正时,在函数内部使用动态内存申请,并返回。
改正:
char* GetMemory(void)
{
char* p = (char*)malloc(20);
strcpy(p, "hello world!");
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main() {
Test();
return 0;
}
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;
}//运行结果,以及问题
没释放空间,内存泄漏
改正:
void GetMemory(char** p, int num)
{
*p = (char*)malloc(num);
}
void Test(void)
{
char* str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
free(ptr);
ptr=NULL;
}
int main() {
Test();
return 0;
}
使用了已经被释放的内存