这一节又是C语言重点,主要讲的是动态内存的分配,那就不罗嗦了直接开始。
内存空间的动态分配
在我们之前学习中,我们知道如果需要定义批量的数据,我们就会使用数组,但是大家应该都发现了它的一个缺点,那就是数组定义时候的长度必须的是常量,不能改变,这样我们使用的时候难免会遇到空间不足或者空间浪费的情况。
如果我们使用堆空间,就能够解决这个问题,并且可以随时释放。
动态内存空间分配的步骤:
- 申请一个指针变量
- 申请一块内存空间,并将其首地址赋给指针变量,此时便可以通过指针变量访问这片内存
- 当我们使用完毕之后,需要释放这片内存空间
关于申请内存空间和释放内存空间主要涉及两个函数:malloc
--申请空间free
--释放空间
示例:
int *p = NULL;
//malloc这个函数的参数是我们需要申请空间的大小(字节)
p = (int *)malloc(sizeof(int));
*p = 10;
printf("%d",*p);
//释放申请的空间
free(p);
return 0;
注:
malloc 返回一个指向已分配空间的 void 指针,如果内存不足,则返回 NULL。
我们以后应该使用if判断堆空间申请是否成功。
使用堆和使用数组的差别
数组长度只能是常量,堆大小可以是变量
示例:
int main()
{
//我想申请能够存储10个整数的空间
//然后遍历输入
//遍历输出
int *p = NULL;
int num = 0;
printf("请输入需要申请int的个数:\n")
scanf_s("%d",&num);
p = (int *)malloc(num * sizeof(int));
//我们使用指针和数组是非常类似的
//输入
for (int i = 0; i < num; i++)
{
printf("请输入一个数:");
scanf_s("%d", &p[i]);
}
//输出
for (int i = 0; i < num; i++)
{
printf("%d ",p[i]);
}
free(p);
p = NULL;
return 0;
}
堆空间的释放
我们使用的数组会自动释放内存空间,但是我们使用堆空间的时候,需要自己释放内存口昂见,如果忘记释放,就会造成内存空间泄漏。
数组示例:
int main()
{
int n = 0;
int *p = NULL;
printf("你需要存储多少个整数:");
scanf_s("%d", &n);
//int Arr[n];//数组长度必须是常量
while(true)
{
int Arr[1024];
}
return 0;
}
我们使用数组申请的空间会自动释放。
堆示例:
int main()
{
int n = 0;
int *p = NULL;
printf("你需要存储多少个整数:");
scanf_s("%d", &n);
//int Arr[n];//数组长度必须是常量
while(true)
{
p = (int *)malloc(n * sizeof(int));
}
return 0;
}
我们申请的堆空间不会自动释放,他会占满你的内存。
关于悬空指针与野指针
悬空指针
当我们释放了内存之后,我们指针也应该指向NULL,因为那片内存空间已经不再属于这个指针,可能已经被别的代码或数数据占用,但是如果我们继续操作就会修改其不属于我们修改内容的内存。
这些释放之后没有赋值为NULL的指针被称为悬空指针。
示例:
int main()
{
int *p = (int *)malloc(sizeof(int));
free(p);
*p = 10; //继续使用了释放空间后的指针
return 0;
}
野指针
指针被定义之后,但是没有初始化,这种指针被称为野指针。
示例:
int main()
{
int *p;
*p = 10; //再使用野指针
//这个指针没有指向任何一个地址,但是却对其指针所指向的内容赋值
return 0;
}
总结:
指针变量,要么指向一个有意义地址(变量,数组,堆),要么指向NULL(0)
使用悬空指针或者野指针都会造成程序崩溃
一个程序的内存划分
通常来说,一个运行的程序,内存会划分为5个区域:
代码区、常量区、栈区、堆区、全局数据区
代码区
- 存放函数体的二进制代码
常量区
- 存放常量值,如常量字符串等,不允许修改,程序结束后由操作系统回收
栈区
- 由编译器自动分配和释放,用来存放函数的参数、局部变量等。其操作方式类似于数据结构中的栈
堆区
- 一般由程序员分配和释放(通过malloc/free、new/delete),若程序员没有释放,则程序结束时由操作系统回收。它与数据结构中的堆是两回事,分配方式类似于链表
全局数据区
- 全局变量和静态变量的存储是放在一块的,初始化的全局变量和初始化的静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,程序结束后由操作系统回收
示例:
int g_Num = 0;
void Fun(int nParam)
{
printf("参数地址:%p\n", &nParam);
static int nNum = 0;
printf("静态局部:%p\n", &nNum);
}
int main()
{
int nTest1 = 0;
int nTest2 = 0;
printf("局部变量:%p\n", &nTest1);
printf("局部变量:%p\n", &nTest2);
Fun(10);
printf("全局变量:%p\n", &g_Num);
int *p1 = (int *)malloc(10);
int *p2 = (int *)malloc(10);
printf("堆:%p\n", p1);
printf("堆:%p\n", p2);
printf("常量区:%p\n", "hello world");
printf("常量区:%p\n", "hello 15pb");
return 0;
}
其他内存操作函数
内存初始化函数memset
,其原型为:void *memset(起始地址,要设置的值,要设置多大区域);
一般用于给刚申请出来的内存设置一个初始值
内存拷贝函数memcpy
,其原型为:void *memcpy(目标地址,源数据地址,要拷贝多大区域);
一般用于把内存中的数据拷贝到另一块内存中
综合应用
在我们以后使用堆的时候,基本上都会大量使用指针,在我们才接触没多久的情况下,使用指针出错的概率相对较大,不过我们记住以下几个原则,能让我们少踩些坑:
- 刚申请的动态内存的初始值是补确定的
- 不能对同一指针(地址)连续两次free操作
- 不能对指向静态内存区(全局变量)或栈区(局部变量)的指针应用free(但是可以对空指针NULL应用free),对一个指针应用free之后,它的值不会变,但他指向了一个无效的内存区,这也就是我们说的悬空指针。
- 如果没有即使释放某块动态内存,并且将指针指向了别的地方,就会造成内存泄漏,执行malloc和free有一定的代价,所以数据量小的不应该放在动态内存中,并且应该避免频繁的申请和释放动态内存。
我们在进行内存区域的申请时,主要需要注意避免以下错误:
- 没有成功分配内存但是我们却使用了它
- 内存分配成功但是没有对他进行初始化就引用了它
- 内存分配成功并且已经初始化,但操作越过了内存边界
- 忘记释放内存,造成内存泄漏
- 释放了内存却继续使用
综合示例:
int *p = NULL;
p = (int *)malloc(5 * sizeof(int));
meset(p,0,5*sizeof(int));
int num = 0;
for(int i = 0;i<5;i++)
{
p[i] = i;
}
p = (int *)malloc(10 * sizeof(int));
free(p);
p[0] = 1; //此时使用了悬空指针
//p = &Num
//free(p);//不能释放除了堆空间之外的空间
//free(p);//错误对一块内存空间进行释放两次
//p = NULL;//即使置为空,配合下面的判断,避免使用空指针
if(p!=NULL)
{
//一般我们在使用指针之前,检测一下是不是空指针是一个好习惯
}