第二十九章 动态内存管理(1)

C语言学习之路

第一章 初识C语言
第二章 变量
第三章 常量
第四章 字符串与转义字符
第五章 数组
第六章 操作符
第七章 指针
第八章 结构体
第九章 控制语句之条件语句
第十章 控制语句之循环语句
第十一章 控制语句之转向语句
第十二章 函数基础
第十三章 函数进阶(一)(嵌套使用与链式访问)
第十四章 函数进阶(二)(函数递归)
第十五章 数组进阶
第十六章 操作符(详解及注意事项)
第十七章 指针进阶(1)
第十八章 指针进阶(2)
第十九章 指针进阶(3)
第二十章 指针进阶(4)
第二十一章 数据的存储(秒懂原、反、补码)
第二十二章 指针和数组笔试题详解(1)
第二十三章 指针和数组笔试题详解(2)
第二十四章 字符串函数(1)
第二十五章 字符串函数(2)
第二十六章 内存函数
第二十七章 自定义数据类型之结构体进阶(1)
第二十八章 自定义数据类型之结构体进阶(2)
第二十九章 动态内存管理(1)



前言

在我们之前创建一个数组的时候,数组的长度都是固定死的,无法修改。比如我们想要创建一个储存名字的字符串,但是名字的长度一般是不确定的,因此我们开辟的数组空间一般都比较大,以便存放下任意长度的字符串。


一、为什么要有动态内存开辟?

我们平常开辟内存的途径仅仅是去创建几个不同数据类型的变量,例如:

int a=10;
int arr[100]={0};

上面的创建方式有一个共同的特点,即这些内存开辟后,其大小是完全固定的。我们以数组为例,每当这个数组的大小确定后,我们便无法对其进行修改,这就导致了一个非常尴尬的问题。每次我们都会预留足够的内存空间来防止空间不足。但当我们的数组中的元素较少的时候,也造成了内存空间的浪费。

倘若我们的内存是可以动态伸缩的,那么这个问题就可以得到有效地解决。这也就是动态内存开辟的意义之一。

除此以外,电脑内存管理的区域包括栈区、堆区、静态区等,而在学习动态内存开辟之前,了解一下这方面的知识是非常有必要的。
在这里插入图片描述
平常我们创建的变量大多数为局部变量,这些变量存储在栈区,而栈区中的内存空间的声明周期完全由电脑自身决定,我们自己没有控制其声明周期的权力。例如,当代码运行完一个大括号内的代码段时,其内部的局部变量就会被释放掉。这时候,就会有人说,我们可以用static关键字修饰。但是当我们将其改变为静态变量的时候,只有当程序结束,才会将其释放,很显然,这也并不是我们能够控制的。

但是,堆区中的内存则完全由我们自己的需求决定,堆区中的内存释放与否完全由我们自己控制。当然, 为了避免程序员在堆区创建了变量后忘记销毁的问题,系统会在程序进行结束后,将堆区中未释放的内存释放。简而言之,我们对动态开辟的内存空间有更大的控制权。这也是动态内存开辟的另外一个用途。

一、malloc函数

1、函数的用途

在这里插入图片描述
这个函数的作用就是在堆区内开辟以字节为单位的空间,具体开辟多大,取决于我们在形参列表中填入的数字。
同时,这个函数的返回值是一个void*指针。这是非常好理解的,因为我们只告诉这个函数我们要开辟多大的内存空间,但是我们并未说明我们所开辟的内存空间是来存储什么数据类型的数据的。所以这个函数最终返回的是一个void类型的指针。这就导致,我们在接受这个返回值的时候,通常会对其进行强制类型转化。其次,我们开辟完空间后,这段空间内的数据不会被初始化,依旧保留随机值。
另外,在使用这个函数之前不要忘记包含头文件:stdlib.h
还有一点,我们需要注意,即如果我们想要申请开辟的内存空间过大,可能会出现开辟失败的情况。倘若开辟失败,那么返回的就是一个空指针。
倘若开辟成功,返回的就是这段内存空间的起始地址。
倘若我们想要开辟0字节的内存空间,那么他的返回值可能是空指针,也可能不是,这取决于具体的库实现。

2、函数的使用

#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
	int* p = (int*)malloc(40);
	int* ptr;
	if(p==NULL)
	{
		printf("%s\n",strerror(errno));
		return 1;
	}
	else
	{
		ptr=p;
	}
	//使用:非常类似于数组的使用,几乎是一致的。
	forint i = 0;i < 10 ; i++{
		ptr[i]=i;
	}
	//使用结束后:
	free(p);//释放空间内存,这个函数一会儿讲
	p=NULL;
	ptr=NULL;
	return 0;
}

我们先来解读一下上面的代码:
我们将开辟的空间的地址强制转换为整型指针,然后再用一个整形指针接收。但是我们知道,开辟的空间过大时,可能会导致空间开辟失败。基于失败的情况,我们调用一下我们之前学过的字符串函数:strerror函数。这个函数会将错误码转化成错误信息。而错误码会存储在errno类型里,所以我们包含一下头文件errno.h,因此,当我们开辟空间失败的时候,就会将错误码存储在errno里,然后我们将错误码传入函数strerror中,即可打印空间开辟失败的原因,即下图所示:
在这里插入图片描述
当我们使用完这段空间后,我们会利用后续学到的free函数释放这个段空间。释放完这段空间后,这段空间的访问权限已经不归我们所有,因此,我们无法再对这段空间进行访问和修改。但是,虽然空间没了,但是空间所对的地址依旧存放在指针中,因此,我们需要将这个指针设置为空指针,防止野指针的出现。

注意:

  • 如果开辟成功,则返回一个指向开辟好空间的指针。
  • 如果开辟失败,则返回一个NULL指针,因此malloc函数的返回值一定要做检查。
  • 返回值的类型是void*,所以malloc函数并不知道开辟的空间中所存储而数据类型,具体在使用的时候,需要我们来对其进行转化。

二、free函数

1、函数用途

在上面介绍malloc函数的时候,我们提到了free函数,那么现在我们也知道了这个函数的作用就是来释放我们手动开辟的内存。这里一定要注意,我们释放的是我们自己开辟在堆区的内存。为什么要提这一点呢?在后面我们会提到,大家接着往下看吧。
在这里插入图片描述
这个函数的作用是来释放我们手动开辟的内存空间。但是在这个过程中有几个点我们是需要注意的。
注意:

  • 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为是未定义的,编译器也会在代码编写的时候发生报错。
  • 如果参数ptr是NULL指针,那么这个函数什么事情都不做。

2、函数使用:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p=malloc(40);
	if(p==NULL)
	{
		perror("malloc");
		return 1;
	}
	int*ptr=p;
	for(int i=0;i<10;i++)
	{
		*(ptr+i)=i;
	}
	free(p);
	p=NULL;
	ptr=NULL;
	return 0;
}

上面的代码其实和刚才在malloc函数中写的代码几乎是一致的,只不过我们把刚才的strerror函数换成了perror函数,目的就是帮助大家回顾一下之前学过的知识。千万要注意,当我们将一段空间释放后,一定要把指向这一段空间的设置为空指针,避免野指针的出现。

三、calloc函数

1、函数的用途:

在这里插入图片描述
这个函数的作用和malloc的函数作用几乎是一致的,只不过二者也有一些不同点。首先,之前学习malloc函数的时候,我们知道malloc函数仅仅是开辟新的空间,这段空间中存储的数据还是原本的随机值。但是calloc函数可以讲所开辟的空间初始化数据为0。
其次,我们看这个函数的形参列表,num是我们开辟空间中所打算存储的元素个数,
size是每一个元素大小。其实我们很容易就能想到,这两个数字相乘其实就是malloc函数的形参。

2、函数的使用:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p = (int*) calloc(10,sizeof(int));
	if(p==NULL)
	{
		perror("calloc");
		return 1;
	}
	int*ptr=p;
	for(int i=0;i<10;i++)
	{
		ptr[i]=i;
	}
	free(ptr);
	p=NULL;
	ptr=NULL;
	return 0;
}

在这里插入图片描述

四、realloc函数

1、函数用途:

在这里插入图片描述
这个函数也是用来开辟空间的,但是我们很难保证我们开辟的空间是恰好的。所以这个函数的作用就是对我们开辟的空间进行大小上的调整。
在这个函数中:

  • ptr是调整的内存地址。
  • size是调整后的新大小。
  • 返回值是新空间的地址。
  • 这个函数调整原内存空间大小的基础上,还会将原来的数据移动到新空间中。
  • 倘若realloc函数开辟失败,此时返回的指针是空指针。

但是,这个函数开辟空间的时候,存在两种情况:
情况一:
在这里插入图片描述
情况二:
在这里插入图片描述
对于情况一而言,这段自己开辟的空间后面多余的空间足够我们的需要,那么这个函数就会在原空间的基础上继续向后开辟,然后最终返回原地址。
但是,也会出现情况二,即当我们原空间后面的空间不足时,系统会开辟一块新的内存空间,然后将原空间的内容复制过来,再释放原空间。再返回新空间的地址。
倘若整个内存空间都没有一块空间能够满足我们的需求,此时就会返回一个空指针。

2、函数的使用:

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int*p=(int*)calloc(20,sizeof(int));
	int*ptr;
	if(p==NULL)
	{
		perror("calloc");
		return 1;
	}
	ptr=p;
	for(int i=0;i<20;i++)
	{
		ptr[i]=i;
	}

	int* p2=realloc(ptr,200);
	if(p2!=NULL)
	{
		ptr=p2;
		p2=NULL;
	}
	free(ptr);
	ptr=NULL;
	
	return 0;
}

当我们利用realloc函数开辟内存后,不要直接用我们所需的指针去进行接收,这样做的目的是防止接受到空指针。假设内存重新开辟失败,那么这个函数返回得到空指针。我们直接利用原空间的指针去接受,这样做的话,会让我们丢失掉原空间的地址。

五、常见的动态内存开辟错误

1、对NULL指针进行解引用操作

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int* p=(int*)malloc(200000000000);
	*p=1;
	return 0;
}

如上述代码所示,假设我们没有对这个函数的返回值进行检验,很有可能出现对空指针解引用的操作。

2、对动态开辟空间的越界访问

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{
	int*p=(int*)calloc(10,sizeof(int));
	int*ptr;
	if(p==NULL)
	{
		printf("%s\n",strerror(errno));
		return 1;
	}
	for(int i=0;i<20;i++)
	{
		ptr[i]=i;//出现了越界访问
	}
	free(ptr);
	ptr=NULL;
	p=NULL;
	return 0;
}

我们在使用这个堆区的空间的时候,其使用方法和数组的使用方法其实是很类似的。因为数组其实就是在栈区开辟的一块连续的内存。所以二者的使用其实是非常类似的,当然两者可能出现的问题也是极其相似的。比如上述代码就出现了越界访问的问题。

3、对非动态开辟的内存进行free释放

#include<stdio.h>
#include<stdlib.h>
int main()
{
	int a=0;
	int*p=(int*)malloc(40);
	int*ptr;
	if(p==NULL)
	{
		perror("malloc");
		return 1;
	}
	ptr=p;
	free(p);
	p=NULL;
	ptr=NULL;
	free(a);//这里出现了错误,我们错误地释放了非动态开辟的内存。
	return 0;
}

这个错误很好理解,因为非动态开辟的内存基本上不在堆区,而堆区除外的内存区基本上我们不具备释放这些空间的权力。

4、使用free释放一块动态开辟内存的一部分

void test()
{
 int *p = (int *)malloc(100);
 p++;
 free(p);//p不再指向动态内存的起始位置
}

5、对同一块动态内存多次释放

void test()
{
 int *p = (int *)malloc(100);
 free(p);
 free(p);//重复释放
}

6、动态开辟内存忘记释放(内存泄漏)

void test()
{
 int *p = (int *)malloc(100);
 if(NULL != p)
 {
 *p = 20;
 }
}
int main()
{
 test();
 while(1);
}

忘记释放不再使用的动态开辟的空间会造成内存泄漏。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值