C语言之动态内存库函数
测试环境:vs2017
为何有动态动态内存
众所周知我们使用数组开辟空间十分的方便,那为什么要去动态内存中特意开辟一个空间来用于保存数据?原来,在我们使用数组开辟空间时,会经常浪费许多空间,而如果有函数可以任意在空间中申请一个大小来保存你的数据,让你的动态内存利用率得到提升,还能学到新的知识,那岂不是一石二鸟。C99标准中,我们可以使用变量来决定数组的长度,但是使用起来非常危险!!!现在的编译器大部分已经把该项功能去除了。所以我们想要得到一个可以由我们控制的空间大小的确很难,动态内存开辟函数就很好的解决了这个问题。
想要在动态内存中开辟一个空间用于保存数据,那就必须要了解一些库函数可以在动态内存中开辟空间,他们是以什么样的方式或者方法来开辟空间?开辟成功了会返回什么?开辟失败了会怎样?如果开辟空间没有释放会发生什么?还有各种各样关于动态内存函数容易忽略的问题,动态内存函数开辟空间可以越界吗?内存会被耗干吗?如果开辟好的动态内存用光了怎么办?如果开辟动态内存空间浪费太多可以缩小吗?等这些问题,希望你能在看完本章可以得到答案。
动态内存中的四个区
首先,我们先要了解内存分为四个区,栈区,堆区,静态区(常量区和全局变量区),程序代码区。
栈区存放函数的参数值局部变量等临时值,由编译器自动分配和释放。
堆区一般用于动态内存开辟与释放,由程序员使用和释放,程序结束后,可能会由操作系统回收。
静态区是用于存放常量和局部变量,而且初始化和未初始化的存放在不同的区域,程序结束后,由操作系统进行释放。
程序代码区存放函数体的二进制代码。
malloc
注意:每一个函数都是在内存中开辟一段连续的空间。
在MSDN中malloc的解析:

malloc的使用要先引用头文件<stdlib.h>,开辟的字节数是size_t类型,表示是无符号整型,所以当我们开辟字节时,不能写入浮点数或者负数。

这个是malloc的返回值,动态内存开辟成功返回一个void*的指针指向开始的地址,当动态内存开辟失败返回一个空指针,你可以用void转换成一个你想要的类型,并用对应指针接收。
这里我创建三种类型不同的接收方法:
#include<stdio.h>
#include<stdlib.h>
struct Id
{
char name[10];
char sex[6];
int age;
};
//整型
int main()
{
int* p = (int*)malloc(sizeof(int) * 5);//开辟可以存放5个整型的空间
if (p == NULL)
{
return 0;//直接结束
}
printf("数字:");
//使用
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
printf("%d ", *(p + i));
}
free(p);//释放
p = NULL;
//字符
printf("\n字符:");
char* q = (char*)malloc(sizeof(char)*5);//开辟可以存放5个整型的空间,因为我们知道一个字符是一个字节,可直接写为5
if (q == NULL)
{
return 0;
}
//使用
for (i = 0; i < 5; i++)
{
*(q + i) = 'A' + i;
printf("%c ", *(q + i));
}
free(q);//释放
q = NULL;
//结构体
//同样根据前两个例子我们可以知道
struct Id* k = (struct Id*)malloc(sizeof(struct Id) * 5);//开辟可以存放5个结构体的空间
if (k == NULL)
{
return 0;
}
//使用
free(k);//释放
k = NULL;
return 0;
}
可以发现,使用起来没什么困难,然后再来看一下动态内存的变化:
这里先教大家如何打开内存条,记得要在调试模式下才可以打开内存条,进入调试模式只需按Fn+F11或者Fn+F10。

这里我们点击完可以看见屏幕上会多出一个窗口,我们可以进行调节,让他显示我们想要的效果。

这里的列可以改为我们想要的列数,可以更便于去观察内存的变化,什么输入框如果没有显示,可以拉长,然后输入你想监测的内存。
下图是p的内存,可以看到我们调试出的效果。

可以发现我们真的向动态内存申请存放5个整型的动态内存空间。
再观察一下动态内存的使用:

我们可以发现申请好的空间的确变成我们想要的数字。
接下来动态内存的释放,千万不要忘记:

经过free释放好的动态内存应该把当初的指针置为空,因为free只是单纯地将动态内存释放,而指针依旧指着那块已经释放的空间,这是非常不安全的!
calloc
这个函数也是用于开辟动态内存的一个函数:


经过分析,比较malloc,这个函数多了一个参数,原来num是要开辟的元素个数,size是要开辟的元素的大小占几个字节。其实他开辟空间的方式和malloc大同小异,经过观察内存才会发现其中的奥秘。
在使用calloc开辟的空间时:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//动态内存开辟
int* p = (int*)calloc(5, sizeof(int));
if (p == NULL)
{
return 0;
}
int i = 0;
//使用
for (i = 0; i < 5; i++)
{
printf("%d ", *(p + i));
}
//释放
free(p);
p = NULL;
return 0;
}


我们发现此时此刻的内存已经发生了变化,所以calloc和malloc之间的区别就在于,calloc使用时会将申请好的内存进行初始化,而malloc不会,所以当我们开辟内存空间时可以先决定用malloc还是calloc。
realloc
该函数可以随意调节开辟空间的大小,也就是可以有**“伸缩”**功能!!不过也要注意,有时候可能会“伸缩”失败。

这里的memblock是一个指向初始地址的指针!他的返回值和malloc和colloc一样,都是开辟成功返回一个指向初始地址指针,开辟失败返回空。不过既然他要向内存开辟空间,是以如下图两种方法开辟的。

不过如果你是想将空间缩小,那肯定不会像方法二那样活动。
接下来看看是如何使用的:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int*p = (int*)malloc(sizeof(int) * 5);
if (*p == NULL)
{
return 0;
}
int i = 0;
for (i = 0; i < 5; i++)
{
*(p + i) = i;
}
//增大空间
int *ptr = (int*)realloc(p, sizeof(int) * 10);
//开辟空间要另外创建一个指针接收,如果开辟失败则不赋值
if (ptr != NULL)
{
//开辟成功,赋值
p = ptr;
//观察空间变化
for (i = 5; i < 10; i++)
{
*(p + i) = i;
}
}
//缩小空间
*ptr = (int*)realloc(p, sizeof(int) * 3);
if (ptr != NULL)
{
p = ptr;
}
free(p);
p=NULL;
return 0;
}
**注意增大空间时,他不会将原来的空间加上,而是开辟你定义好的字节数。**而动态内存空间就是依靠realloc来将空间伸缩,可以提升空间的利用率。
我们先看一下malloc函数

内存增大:

内存缩小:

看到这里我们使用时才可以放心使用,不过使用完的空间一定要记得free!!
接下来我们看两道常见的内存错误题:
//请问他会输出什么呢?存在什么错误?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char *p)
{
p = (char *)malloc(100);
}
void Test(void)
{
char *str = NULL;
GetMemory(str);
strcpy(str, "hello world");
printf(str);
}
这段代码有很严重的指针错误,因为str已经是空指针,却还被传参,然后使用strcpy非法访问动态内存,所以该提是什么也不输出,还有很严重的内存空间开辟未释放。
//请问这段代码会输出什么?存在什么错误?
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void GetMemory(char **p, int num)
{
*p = (char *)malloc(num);
}
void Test(void)
{
char *str = NULL;
GetMemory(&str, 100);
strcpy(str, "hello");
printf(str);
}
**这段代码存在严重的内存泄漏问题,虽然会输出hello,但是内存没有得到及时释放,后期可能会有很大的麻烦!**所以使用动态内存函数任重而道远,需要多多练习,完全熟练掌控动态内存对于你们一定是不在话下!