在了解到动态内存分配这块儿的知识之前,大家或许也和我一样,掌握的内存开辟空间只有创建变量 和创建数组 。或许大家平时使用这类方法时也并没有觉得有什么不妥,甚至觉得很方便好用。但其实当我们在编程这条路上走的更远的时候就会发现,使用数组开辟空间也是有非常多的坏处的 ,而为了避免这些坏处,就引出了我们今天要学习的内容:动态内存管理~
一、什么是动态内存?
① 为什么要有动态内存分配
首先让我们回忆一下创建变量 和创建数组的方式开辟内存格式:
cs
int main()
{
int a;//在栈空间上开辟四个字节
char str[20];//在栈空间上开辟10个字节连续空间
return 0;
}
同时让我们思考一下,这样的开辟内存方法的缺点是什么呢?主要是以下两点:
• 空间开辟大小是固定的。
• 数组在声明的时候,必须指定数组的长度,数组空间一旦确定了大小不能调整。
• 有时需要的空间大小在程序运行过程中才能得知,而使用数组无法做到灵活调整空间。
而为了避免这些缺点,并且为了使程序员能够灵活的调整空间,就出现了动态内存分配 ~使用这种方法就能让我们自己申请和释放内存了,非常的方便。
② 动态内存分配的好处
首先我们看一下,这个使用数组开辟空间而导致的错误:
cs
int main()
{
int arr[300000];
return 0;
}
而出错的原因就是溢出了,是"栈溢出"。而接下来我们尝试一下用动态内存分配方法开辟相同大小的空间。
cs
int main()
{
int* arr = (int*)malloc(sizeof(int) * 300000);
printf("YES\n");
return 0;
}
此时我们会发现,使用这种方法便不会出现报错情况。 这是因为动态内存分配与使用数组内存分配,开辟出的内存空间并不在一个区域内:而这三个区域的大小又各有不同,所以使用动态内存分配不会导致溢出,而栈区导致了溢出。
栈区大小:2M或1M。
函数内申请的变量、数组,是在栈(stack)中申请的一段连续的空间。
静态区大小:2G.
全局变量,全局数组,静态数组(static)大小为2G。
堆区大小:视内存而定,可以开很大
malloc、new的空间,则是开在堆(heap)的一段不连续的空间,理论上则是硬盘大小。
可以看到栈区的空间是很小的,所以这也是动态内存分配的一大好处。
而后续介绍的动态内存分配函数中也会提到它的其他好处,比如灵活修改内存大小。
++(以下函数都需要头文件stdlib.h)++
二、malloc函数
malloc函数的作用是向内存申请一块连续的空间,并返回指向这段空间的指针。
malloc函数接收的参数是一个size_t(无符号整型) 的变量,参数size代表的是申请分配的内存大小,单位为字节。
因为想要创建什么类型的变量取决于程序员 ,所以malloc函数的返回类型需要做到灵活,所以是void*类型。
当然~malloc归根结底也还是申请内存的函数,只要申请空间就需要占用空间,只要占用空间就避免不了可能会申请失败:
++• 如果开辟成功,则返回一个++++指向开辟好空间的指针++++。++
++• 如果开辟失败,则返回一个++++NULL++++指针,++++因此malloc的返回值一定要做检查++++。++
++• 返回值的类型是 void* 所以++++malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定++++。++
++• 如果参数 size 为 0(开辟0个字节),malloc的行为是标准是未定义的,取决于编译器。++
让我们写一段代码来具体的学习一下malloc函数的使用吧~
cs
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);//开辟大小为40字节的空间
if (p == NULL)
{
perror("malloc");//打印错误信息
return 1;//异常返回
}
return 0;
}
当我们运行这段代码时,很顺利的就成功了,而当我们将10改成100,1000,甚至更多呢 ?这次就没能如愿的分配出这么多的空间了,原因就是:not enough space(空间不足)。
接下来我们尝试一下:使用动态内存分配模拟实现数组。
cs
int main()
{
int* p = (int*)malloc(sizeof(int) * 10);
for (int i = 0; i < 10; i++)
{
*(p + i) = i + 1;
printf("%d\n", *(p + i));
}
return 0;
}
这样就成功模拟出来啦~怎么样,其实还是挺简单的吧。
三、free函数
在动态内存中,malloc函数的作用是开辟空间,那么自然也要有释放空间的函数。free的作用就是释放动态内存分配函数所开辟的空间。
free接收的参数类型为void*,ptr所指代的就是动态开辟出的空间的指针。
++• 如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。++
++• 如果参数 ptr 是NULL指针,则函数什么事都不做。++
++(需要注意的是:free的作用仅仅是将开辟的空间释放,而并不是置空,所以为了防止误访问已释放的空间,最好再加一步置空操作~)++
函数的使用:
cs
int main()
{
int* p = (int*)malloc(sizeof(int) * 100);//开辟100个sizeof(int)大小的空间
for (int i = 1; i <= 100; i++)
{
*(p + i - 1) = i;//为100个空间赋值
printf("%4d", *(p + i - 1));
if (i % 10 == 0)
printf("\n");
}
free(p);//释放空间
p = NULL;//空间置空
return 0;
}
可能有人就会觉得,这个释放空间的作用到底为何呢?因为当我们使用完以后,自然而然的退出就好了,为何要多此一举的去释放它呢?实则不然,free的作用非常之重要,动态内存分配最大的优点就在于灵活:
如果使用数组开辟空间,++当题目变量过多++ ,++需要多个数组存储数据时++ ,用完一个数组后无法像动态内存一样,能够使用free去释放空间,从而导致使用完毕的数组浪费空间,甚至可能导致溢出。而动态内存分配的优点也体现于此,当我们使用完一块空间后可以灵活的将它释放掉,并使用新的空间进行接下来的操作,这就大大的节省了不必要的空间~
如果使用空间过多,而每次使用完后并不即使将空间释放,那么就会造成++内存泄漏:++
cs
int main()
{
int i = 0;
int* arr[100000];//储存每一次开辟的空间
for (i = 0; i < 100000; i++)
{
arr[i] = (int*)malloc(sizeof(int) * 10000);//每次开辟10000*4大小的空间
//free(arr[i]); //我故意的...
if (arr[i] == NULL)
{
printf("%d\n", i);//如果失败则打印开辟多少次时失败
perror("arr[i]");//如果开辟失败则返回错误信息
break;
}
}
return 0;
}
代码解读:我们使用一个指针数组来存储每一次使用malloc开辟出的空间,并且每次开辟都不释放,当开辟次数达到一个限度,使得没有足够的空间开辟下一次时,便会报错并退出运行。 我们可以看到,如果使用完后不进行释放,那么在开辟空间的多重积累下,即便拥有的空间再多也会可能发生溢出 。而当我们将被注释掉的free函数解除注释后,此代码就能完美无误的彻底运行结束。
四、calloc函数
calloc函数和malloc函数其实大同小异,都是用来开辟动态内存空间的。只不过它们的区别在于:① malloc函数开辟出空间后并未初始化,而calloc函数开辟出空间后会自动初始化为0。
② calloc函数接收的参数为两个,num代表元素的个数,size代表每个元素的大小。
两者区别的验证:
cs
int main()
{
int* arr1 = (int*)malloc(10 * sizeof(int));
int* arr2 = (int*)calloc(10, sizeof(int));
printf("malloc开辟:\n");
for (int i = 0; i < 10; i++)
{
printf("%d\n", arr1[i]);
}
printf("\n");
printf("calloc开辟:\n");
for (int i = 0; i < 10; i++)
{
printf("%d\n", arr2[i]);
}
free(arr1);
free(arr2);
arr1 = NULL;
arr2 = NULL;
return 0;
}
总而言之,如果我们想开辟存储数字数据的空间,那么calloc无疑是更加合适的人选~
五、realloc函数
realloc函数的作用是:重新调整之前使用malloc或calloc所开辟的空间大小。
其中两个参数:
① void* ptr:代表的是想要改变大小的空间的指针(必须是之前用malloc或calloc开辟出的动态内存空间才可以)
② size_t size:此块动态内存改变后的大小。
realloc函数的使用:
cs
int main()
{
int* arr = (int*)calloc(5, sizeof(int));//开辟出大小为5*sizeof(int)的空间
for (int i = 0; i < 10; i++)
{
if (arr[i] != 0)//判断此处空间是否被开辟(初始化)
{
arr = (int*)realloc(arr, (i + 1) * sizeof(int));//开辟新空间,用于储存新数据
}
arr[i] = i;
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
(如果不使用realloc函数进行内存调整,就会提醒"错误出现在创建对象时内存分配")使用realloc函数就成功的,真正的做到了灵活控制开辟空间大小~
当然,realloc也可能出现空间调整失败的情况,而使用realloc改变内存空间大体有以下三种情况:
① 当原空间后有足够空间时,直接在原空间后追加新空间。
② 当原空间后没有足够空间时,选取一块新的空间进行开辟,然后将原空间的数据复制到新选取的空间后释放原空间,最后返回新空间的地址。 ③ 找不到空间 了解了三种情况之后,让我们再看看应对这三种情况,改进之后的代码:
cs
int main()
{
int* arr = (int*)calloc(5, sizeof(int));//开辟出大小为5*sizeof(int)的空间
if (arr == NULL)
{
perror("arr");
return 1;
}
for (int i = 0; i < 10; i++)
{
if (arr[i] != 0)//判断此处空间是否被开辟(初始化)
{
//创建新变量接收扩大后的空间,防止返回NULL
int* arr1 = (int*)realloc(arr, (i + 1) * sizeof(int));//开辟新空间,用于储存新数据
if (arr1 != NULL)//判断,若不为NULL则将新指针给原指针
arr = arr1;
}
arr[i] = i;
printf("%d ", arr[i]);
}
free(arr);
arr = NULL;
return 0;
}
这样就非常安全啦~非常滴严谨。
**六、**常见的动态内存的错误
① 对同一块动态内存多次释放
cs
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (arr == NULL)
{
perror("arr");
return 1;
}
free(p);
free(p);
return 0;
}
② 动态开辟内存忘记释放(内存泄漏)
cs
int main()
{
int i = 0;
int* arr[100000];
for (i = 0; i < 100000; i++)
{
arr[i] = (int*)malloc(sizeof(int) * 10000);
if (arr[i] == NULL)
{
printf("%d\n", i);
perror("arr[i]");
break;
}
}
return 0;
}
③ 使用free释放一块动态开辟内存的一部分
cs
int main()
{
int* arr = (int*)calloc(10 , sizeof(int));
if (arr == NULL)
{
perror("arr");
return 1;
}
int i = 0;
for (i = 0; i < 10; i++)
{
*arr = i;
printf("%d ", *arr);
arr++;
}
//虽然能够成功打印0-9,但arr指向发生了变化
free(arr);//而free要求的是:指向动态开辟内存的指针,所以会错误
arr = NULL;
return 0;
}
④ 对动态开辟空间的越界访问
cs
int main()
{
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL)
{
perror("arr");
return 1;
}
for (int i = 0; i <= 10; i++)
{
*(arr + i) = i;
printf("%d ", *(arr + i));
}
free(arr);
arr = NULL;
return 0;
}
⑤ 对非动态开辟内存使用free释放
cs
int main()
{
int arr[10];
free(arr);
return 0;
}
⑥ 对NULL指针的解引用操作
cs
int main()
{
int* arr = (int*)malloc(INT_MAX);
*arr = 10;
free(arr);
return 0;
}
七**、动态内存管理习题**
习题一:
以下代码运行后会是怎样的结果?
cs
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;
}
答案:出错了 T A T
解析: GetMemory函数的作用顾名思义,就是用来开辟空间的,但是此空间在函数结束后同时也被释放了,于是str仍然还是指向NULL,而NULL无法访问!!!故报错。
改进方案:
cs
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;
}
其实还是比较简单的啦~既然开辟的空间会被释放,那不妨我们直接将str的地址作为参数传过去,同时将函数的参数改成二级指针,这样我们改变的就是地址,而地址既被改变,就不会被释放,所以就能够正常的拷贝hello world啦~
注意:还有一个隐含的错误就是,使用malloc开辟动态内存空间后,并没有使用free释放内存!!!非常重要!!!
习题二:
运行这段代码,能够成功的打印hello world吗?
cs
char* GetMemory(void)
{
char p[] = "hello world";
return p;
}
void Test(void)
{
char* str = NULL;
str = GetMemory();
printf(str);
}
int main()
{
Test();
return 0;
}
答案:烫烫烫烫烫!!! T A T 解析: 注意:在函数中定义的值和改变的值是局部变量,虽可以返回地址,但里面的值会随函数结束而被销毁。
习题三:
此段代码运行后,结果是什么?
cs
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;
}
答案: 将前两题研究明白后,这题也就比较简单啦~其实就和习题一的改良后差不多,只是参数发生了些许变化。因为使用地址接收创建的空间,所以不会销毁,故hello是能够成功打印的,但是有一处错误就是:可能会出现内存泄漏!使用malloc开辟动态内存空间后并没有使用free释放!可不要马虎了~
那么关于动态内存管理的知识,就为大家分享到这里啦~如果有什么讲的不明白,或者有出现错误的地方,还请大家在评论区多多指出,我也会虚心改进的!!那么我们下期再见啦~