【C语言进阶】动态内存管理

真正的人生,只有在经过艰难卓绝的斗争之后才能实现。                             ——塞涅卡

 

目录

一.为什么存在动态内存分配?

二.动态内存管理的函数

1.malloc函数

2.free函数

​3.calloc函数 

4.realloc函数 

三.常见的动态内存错误 

1.对NULL指针的解引用

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

3.对非动态开辟内存使用free释放 

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

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

四.经典的几个面试题 


一.为什么存在动态内存分配?

我们已经掌握的内存开辟方式有:

int val = 20;//在栈空间上开辟4哥字节
int arr[10] = { 0 };//在栈空间上开辟10个字节的连续的空间

但是上述的开辟空间的方式有两个特点:
1.空间开辟大小是固定的
2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。


但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。
这时候就只能试试动态内存开辟了。

 

二.动态内存管理的函数

1.malloc函数

C语言提供了一个动态内存开辟的函数:

void *malloc( size_t size );


 

 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

●如果开辟成功,则返回一个指向开辟好空间的指针。
●如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。
●返回值的类型是void*,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定。
●如果参数size为0,malloc的行为是标准是未定义的,取决于编译器。


我们用代码来理解:

#include<stdlib.h>//malloc函数需要的头文件
#include<string.h>//strerror函数需要的头文件
#include<errno.h>//errno需要的头文件
int main()
{
	//申请
	int* p = (int*)malloc(20);//向内存申请20个字节
	if (p == NULL)//这里必须要判断,可能会开辟失败
	{
		printf("%s\n", strerror(errno));//打印错误信息
		return;
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

这里使用malloc函数申请很多空间时,可能会开辟内存空间错误。

int main()
{
	//申请
	int* p = (int*)malloc(200000000000);
	if (p == NULL)//这里必须要判断,可能会开辟失败
	{
		printf("%s\n", strerror(errno));//打印错误信息
		return;
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

所以这里我们必须if语句判断一下,不然在我们使用p这块空间的时候,就会出错。

2.free函数

C语言提供了另外一个函数free, 专门是角来做动态内存的释放和回收的,函数原型如下:

void free (void* ptr);

free函数用来释放动态开辟的内存。
●如果参数ptr指向的空间不是动态开辟的,那free函数的行为是未定义的。
●如果参数ptr是NULL指针,则函数什么事都不做。
malloc和free都声明在stdlib.h头文件中。

举个例子:

int main()
{
	
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
	}
	for (i = 0; i < 5; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);//这里必须返回空间给操作系统
	p = NULL;//且这里要给开辟的空间赋为NULL
	return 0;
}


3.calloc函数 

C语言还提供了一个函数叫calloc,calloc 函数也用来动态内存分配。函数原型如下:

void* calloc (size_ t num, size_ t size) ;

●函数的功能是为num个大小为size的元素开辟一块空间, 并且把空间的每个字节初始化为0
●与函数malloc的区别只在于calloc会在返回地址之前把申请的空间的每个字节初始化为全0。
 

int main()
{
	int* p = (int*)calloc(10, sizeof(int));//开辟10个大小为int的空间
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

这里即使你没有赋值,malloc内部也会自动初始化为0。 

calloc和malloc的对比:

1.参数不一样,calloc时两个参数,而malloc是一个参数

2.都是在堆区上面申请内存空间,但是malloc函数不初始化calloc函数会初始化为0

两个函数都是开辟空间,都可以使用。随便你使用哪个。

4.realloc函数 

●realloc函数的出现让动态内存管理更加灵活
有时会我们发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的时候内存,我们一定会对内存的大小做灵活的调整。那realloc函数就可以做到对动态开辟内存大小的调整。

函数原型如下:

void* realloc (void* ptr, size. _t size);

●ptr是要调整的内存地址
●size 调整之后新大小.
●返回值为调整之后的内存起始位置。
●这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc内部使用规则:

如果是这样:

1.realloc会找到更大的空间。

2.将原来的数据拷贝到新的空间。

3.释放新的空间。

4.返回新空间的地址。 

realloc的使用:

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i + 1;
	}
	int* ptr = realloc(p, 40);
	if (ptr != NULL)      
	{
		p = ptr;
		for (i = 5; i < 10; i++)
		{
			*(p + i) = i + 1;
		}
		for (i = 0; i < 10; i++)
		{
			printf("%d ", *(p + i));
		}
	}

	free(p);
	p = NULL;
	return 0;
}

 

三.常见的动态内存错误 

1.对NULL指针的解引用

int main()
{
	int* p = (int*)malloc(20);
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	free(p);
	p = NULL;
	return 0;
}

 

这里malloc函数的返回值可能是NULL,这里就会对NULL指针进行解引用,这就会导致错误。

所以我们必须对malloc函数的返回值进行判断。

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

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		//上面只开辟了20个字节,但是这里要访问40个字节,就会越界访问
		*(p + i) = i;
	}
	free(p);
	p = NULL;

	return 0;
}

3.对非动态开辟内存使用free释放 

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	free(p);//这里的p是静态的,不能进行free
	p = NULL;
	return 0;
}

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

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	free(p);//这里连续free了两次,会出错
	free(p);
	p = NULL;
	return 0;
}

如果是free了一次,把空间置为NULL之后,再次free,这是没有问题的。我们应当避免连续两次free同一块空间。

这种情况就是可行的:

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	free(p);//这样就是可行的
	p = NULL;
	free(p);
	p = NULL;
	return 0;
}

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

int main()
{
	int* p = (int*)malloc(20);
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return;
	}
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		*(p + i) = i;
	}
	//这里没有对内存进行free,就会造成内存泄漏
	return 0;
}

内存函数所申请的空间,如果不想使用,需要free给释放掉。如果没有释放掉,程序结束后,操作系统也会回收空间。

怕的就是程序还没有结束,也没有free给释放空间,这样就会造成内存泄露。

四.经典的几个面试题 

第一题:目的是把hello world拷贝到str中去。 

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;
}

这个代码有很多个错误:

1. 调用GetMemory函数的时候,str的传参为值传递,p是str的临时拷贝,所以在GetMemory函数内部讲动态开辟空间的地址存放在p中的时候,不会影响str。所以GetMemory函数返回之后,str中依然是NULL指针。strcpy函数就会调用失败,原因是对NULL的解引用操作,程序会崩溃。

2. GetMemory函数内容malloc申请的空间没有机会释放,造成了内存泄漏

改进:

void GetMemory(char** p)
{
	*p = (char*)malloc(100);
}
void Test(void)
{
	char* str = NULL;
	GetMemory(&str);//使用传址调用
	strcpy(str, "hello world");
	printf(str);
	free(str);//释放掉空间
	str = NULL;
}
int main()
{
	Test();
	return 0;
}

第二题:

char* GetMemory()
{
	char p[] = "hello world";
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}

返回栈空间地址的问题:
GetMemory函数内部创建的数组是临时的,虽然返回了数组的起始地址给了str,但是数组的内存出了。GetMemory函数就被回收了,而str依然保存了数组的起始地址,这时如果使用str, str就是野指针。

改进:

char* GetMemory()
{
	char *p = "hello world";//p是放在静态区的,只有整个程序退出时,才会销毁
	return p;
}
void Test()
{
	char* str = NULL;
	str = GetMemory();
	printf(str);
}
int main()
{
	Test();
	return 0;
}


 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/2965.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python编程:使用pyecharts绘制拟合曲线图

pyecharts库是python下实现的echarts图表绘制库&#xff0c;接下来&#xff0c;我们使用pyecharts来绘制一条曲线&#xff0c;来体验一下pyecharts的基本使用效果。 1、首先&#xff0c;我们要安装下pyecharts库&#xff0c;在pycharm终端输入安装命令&#xff1a; pip install…

pytorch实现深度神经网络与训练

目录 1. 随机梯度下降算法 2.优化器 3. 损失函数 3.1 均方误差损失 3.2 交叉熵损失 4.防止过拟合 4.1 过拟合的概念 4.2 防止过拟合的方法 5. 网络参数初始化 5.1 网络参数初始化方法 5.2 参数初始化方法应用实例 1.针对某一层的权重进行初始化 2.针对一个网络的权…

基于ESP32做低功耗墨水屏时钟

基于ESP32做低功耗墨水屏时钟电子墨水屏概述ESP32实验低功耗电子时钟功能描述接线开发实验结果电子墨水屏 概述 电子墨水是一种革新信息显示的新方法和技术。和传统纸差异是电子墨水在通电时改变颜色&#xff0c;并且可以显示变化的图象&#xff0c;像计算器或手机那样的显示。…

使用ArcGIS为科研论文制作正确、美观、详细的插图

科研论文中的插图&#xff0c;如果图中包含地理信息&#xff0c;那么首先需要在图中标明指北针、比例尺、图例&#xff0c;然后在此基础上再对作的图进一步的美化和修改。 来源&#xff1a;https://doi.org/10.1016/j.uclim.2022.101326 这种就是属于是最常见的研究区概况图&a…

(只需五步)注册谷歌账号详细步骤,解决“此电话号码无法验证”问题

目录 第一步&#xff1a;打开google浏览器 第二步&#xff1a;设置语言为英语&#xff08;美国&#xff09; 第三步&#xff1a;点击重新启动&#xff0c;重启浏览器 第四步&#xff1a;开始注册 第五步&#xff0c;成功登录google账号&#xff01; 如果出现这样的原因&…

java多线程之线程安全(重点,难点)

线程安全1. 线程不安全的原因:1.1 抢占式执行1.2 多个线程修改同一个变量1.3 修改操作不是原子的锁(synchronized)1.一个锁对应一个锁对象.2.多个锁对应一个锁对象.2.多个锁对应多个锁对象.4. 找出代码错误5. 锁的另一种用法1.4 内存可见性解决内存可见性引发的线程安全问题(vo…

乐观锁和悲观锁 面试题

Mysql的乐观锁和悲观锁 实现方式加锁时机常见的调用方式优势不足适用场景乐观锁开发自定义更新数据的时候sql语句中进行version的判断高并发容易出现不一致的问题高并发读&#xff0c;少写悲观锁Mysql内置查询数据的开始select * for update保证一致性低并发互联网高并发场景极…

linux实验之shell编程基础

这世间&#xff0c;青山灼灼&#xff0c;星光杳杳&#xff0c;秋风渐渐&#xff0c;晚风慢慢 shell编程基础熟悉shell编程的有关机制&#xff0c;如标准流。学习Linux环境变量设置文件及其内容/etc/profile/etc/bashrc/etc/environment~/.profile~/.bashrc熟悉编程有关基础命令…

JVM类加载机制

文章目录定义类加载过程加载链接验证准备解析初始化类加载器双亲委派模型定义 Java 虚拟机把描述类的数据从 Class 文件加载到内存&#xff0c;并对数据进行校验、转换解析和初始化&#xff0c;最终形成可以被虚拟机直接使用的 Java 类型&#xff0c;这个过程被称为虚拟机的类…

有手就行 -- 搭建图床(PicGo+腾讯云)

&#x1f373;作者&#xff1a;贤蛋大眼萌&#xff0c;一名很普通但不想普通的程序媛\color{#FF0000}{贤蛋 大眼萌 &#xff0c;一名很普通但不想普通的程序媛}贤蛋大眼萌&#xff0c;一名很普通但不想普通的程序媛&#x1f933; &#x1f64a;语录&#xff1a;多一些不为什么的…

2023最新最详细【接口测试总结】

序章 ​ 说起接口测试&#xff0c;网上有很多例子&#xff0c;但是当初做为新手的我来说&#xff0c;看了不不知道他们说的什么&#xff0c;觉得接口测试&#xff0c;好高大上。认为学会了接口测试就能屌丝逆袭&#xff0c;走上人生巅峰&#xff0c;迎娶白富美。因此学了点开发…

嵌入式学习笔记——SysTick(系统滴答)

系统滴答前言SysTick概述SysTick是个啥SysTick结构框图1. 时钟选择2.计数器部分3.中断部分工作一个计数周期&#xff08;从重装载值减到0&#xff09;的最大延时时间工作流程SysTick寄存器1.控制和状态寄存器SysTick->CTRL2.重装载值寄存器SysTick->LOAD3.当前值寄存器Sy…

async与await异步编程

ECMA2017中新加入了两个关键字async与await 简单来说它们是基于promise之上的的语法糖&#xff0c;可以让异步操作更加地简单明了 首先我们需要用async关键字&#xff0c;将函数标记为异步函数 async function f() {} f()异步函数就是指&#xff1a;返回值为promise对象的函…

51单片机之喝水提醒器

定时器定时器介绍晶振晶体震荡器&#xff0c;又称数字电路的“心脏”&#xff0c;是各种电子产品里面必不可少的频率元器件。数字电路的所有工作都离不开时钟&#xff0c;晶振的好坏、晶振电路设计的好坏&#xff0c;会影响到整个系统的稳定性。时钟周期时钟周期也称为振荡周期…

数据库备份

数据库备份&#xff0c;恢复实操 策略一&#xff1a;&#xff08;文件系统备份工具 cp&#xff09;&#xff08;适合小型数据库&#xff0c;是最可靠的&#xff09; 1、停止MySQL服务器。 2、直接复制整个数据库目录。注意&#xff1a;使用这种方法最好还原到相同版本服务器中&…

银河麒麟v10sp2安装nginx

nginx官网下载&#xff1a;http://nginx.org/download/ 银河麒麟系统请先检查yum源是否配置&#xff0c;若没有配置请参考&#xff1a;https://qdhhkj.blog.csdn.net/article/details/129680789 一、安装 1、yum安装依赖 yum install gcc gcc-c make unzip pcre pcre-devel …

用嘴写代码?继ChatGPT和NewBing之后,微软又开始整活了,Github Copilot X!

用嘴写代码&#xff1f;继ChatGPT和NewBing之后&#xff0c;微软又开始整活了&#xff0c;Github Copilot X&#xff01; AI盛行的时代来临了&#xff0c;在这段时间&#xff0c;除了爆火的GPT3.5后&#xff0c;OpenAI发布了GPT4版本&#xff0c;同时微软也在Bing上开始加入了A…

新版logcat最全使用指南

前言&#xff1a; 俗话说&#xff0c;工欲善其事&#xff0c;必先利其器。logcat是我们通过日志排查bug的重要武器之一。从某个版本开始&#xff0c;logcat改版了&#xff0c;改版之后&#xff0c;也许某些人觉得不太习惯&#xff0c;但是如果稍微学习下之后&#xff0c;就发现…

从 X 入门Pytorch——BN、LN、IN、GN 四种归一化层的代码使用和原理

Pytorch中四种归一化层的原理和代码使用前言1 Batch Normalization&#xff08;2015年提出&#xff09;Pytorch官网解释原理Pytorch代码示例2 Layer Normalization&#xff08;2016年提出&#xff09;Pytorch官网解释原理Pytorch代码示例3 Instance Normalization&#xff08;2…

AJAX,Axios,JSON简单了解

一. AJAX简介概念: AJAX(Asynchronous JavaScript And XML): 异步的JavaScript 和XMLAJAX作用:1.与服务器进行数据交换: 通过AJAX可以给服务器发送请求&#xff0c;并获取服务器响应的数据使用了AJAX和服务器进行通信&#xff0c;就可以使用 HTMLAJAX来替换JSP页面了2.异步交互…
最新文章