【C语言】动态内存深入了解(一口气刨根问底学完系列,全乎,建议三连点赞收藏)

news2025/7/18 10:54:41

目录

1.动态内存分配的原因 

2.动态内存函数的介绍

2.1malloc和free函数

2.2calloc函数

2.3realloc函数 

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

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

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

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

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

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

4.动态内存经典笔试题

4.1题目1

4.2 题目2:

4.3 题目3:

5. C/C++程序的内存开辟

6.结语


 

1.动态内存分配的原因 

我们目前知道的申请内存的两种方法:

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

但是,上述申请内存的办法,一旦开辟完成,内存空间的大小就固定了,就像我们的数组超出内存范围就会产生越界的问题,没有办法改变内存的大小,非常死板,固定化,不够灵活。我们想要更灵活的使用内存空间,当我们觉得少了就补充空间。

总的来说:

上述的开辟空间的方式有两个特点:

        1. 空间开辟大小是固定的。

        2. 数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。

但是对于空间的需求,不仅仅是上述的情况。有时候我们需要的空间大小在程序运行的时候才能知道(比如主页的通讯录项目,只能存储1000个人信息,当我们的联系人信息到101个就无法存储了), 那数组的编译时开辟空间的方式就不能满足了。 这时候就只能试试动态存开辟了。

注意:

固定的内存开辟如我们熟知的两种方式虽然内存使用不自由但是内存管理更为简便。

自由的动态内存分配可能会导致更为复杂的内存管理问题。

2.动态内存函数的介绍

2.1malloc和free函数

C语言提供了动态内存开辟,也就是可以申请内存的函数:

函数头文件:<stdlib.h>

函数功能

     ①   这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针(空间的起始地址)。

②如果开辟成功,则返回一个指向开辟好空间的指针。

 比如:我们以前用数组申请40个字节的空间我们会这样来做:

int arr[10];

现在使用malloc可以这样来用:

malloc(40)

那么现在我们对这个malloc申请的空间进行访问,最好的方式是不是可以当做一个整型来访问,那我们将函数的返回值放在一个整型指针中,访问一次跳过4个字节,但是malloc的返回值是void*需要强制类型转换:

int *p = malloc(40);

③如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查。

	int* p = (int*)malloc(40);//开辟成功返回指针,自己要知道申请的这段空间用于做什么,才好确定类型
	//可能开辟失败,做一下判断
	if (p == NULL)
	{
		peror("malloc");//打印错误原因
		return 1;
	}
	//开辟成功,就可以利用p来访问这块空间
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}

看一下打印结果:

这里补充图解:

③返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使用的时候使用者自己 来决定。

④如果参数 size 为0,malloc的行为是标准是未定义的,取决于编译器

malloc(0)

建议不这么写。

⑤malloc只负责申请或者开辟空间,当空间用完(不是吧空间用尽的意思,是申请内存来做事这个动作结束)不会主动还给操作系统,除非程序结束。当程序不退出,动态申请的内存不会主动释放。所以当我们用完申请的空间后,就要使用free空间来释放我们申请的空间将他还给操作系统,不然就会出现很大的内存问题。

举例来说:

我们看这段代码:

while(1)
{
int* p = malloc(40);
}

这就是一个病毒程序,只要我们一启动,程序就一直向我们的内存申请空间原先申请的又不释放,最终会导致内存卡死,除非关机,,如果写入开机程序,就会循环往复。所以申请空间后需要释放我们介绍free内存释放函数。

C语言提供了free函数,用于做动态内存的释放和回收:

函数头文件:<stdlib.h>

函数功能:

①如果参数 ptr 指向的空间不是动态开辟的,那free函数的行为是未定义的。

就是说我们的free只能释放我们的动态申请的内存空间,如果:

int  a = 0;
int * ptr = &a;
free(ptr);//错误写法

这种用法就是C语言标准未定义的是错误的。

②如果参数 ptr 是NULL指针,则函数什么事都不做。

③当空间被释放后,原先指向这片空间的指针需要我们手动将其置空,变为空指针,因为原先的空间被释放了,但是指针里面还保存着我们原先的地址,当不慎使用这个指针的时候,这个时候我们的指针就会变成野指针,会造成程序出现问题所以需要手动置空。

int main()
{
 //代码1
 int num = 0;
 scanf("%d", &num);
 int arr[num] = {0};
 //代码2
 int* ptr = NULL;
 ptr = (int*)malloc(num*sizeof(int));
 if(NULL != ptr)//判断ptr指针是否为空
 {
 int i = 0;
 for(i=0; i<num; i++)
 {
 *(ptr+i) = 0;
 }
 }
 free(ptr);//释放ptr所指向的动态内存
 ptr = NULL;//手动置空
 return 0;
}

补充:free函数可能运作的机制:

当我们的malloc去开辟空间的时候,比如我们需要开辟40个字节的空间,但是当我们的函数在执行的时候可能会稍微开辟大一点的空间:

 

2.2calloc函数

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

函数头文件:stdlib.h

函数功能:

①函数的功能是为 num 个大小为 size 的元素开辟一块空间,并且把空间的每个字节初始化为0。

②与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

③也有可能开辟失败,所以需要和malloc一样做判断

例子使用:

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//开辟成功打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d", p[i]);
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

将每个字节初始化为0.

我们看一下开辟失败的情况:

int main()
{
	int* p = (int*)calloc(INT_MAX*2, sizeof(int));
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	//开辟成功打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d", p[i]);
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

2.3realloc函数 

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

  •  函数头文件:stdlib.h
  • ptr 是要调整的内存地址(被调整空间的起始地址,如果这里传入一个空指针和malloc函数功能一样)
  • size 调整之后新大小,这里不是在后面补充,是包括原先已经申请好的大小,比如原先申请了10个空间,不够,不能说size传10变成20个空间,而是传20,变成一个20大小的空间。
  • 返回值为调整之后的内存起始位置。(所以当我们用一个指针来接收返回值的时候,这个指针可能指向原先malloc开辟的空间的起始位置也有可能指向一块新空间,这和realloc函数功能相关,下面两种情况:)
  • 增容失败会返回空指针。

补充:

int * p = (int*)malloc(40);
if(p ==NULL)
{
perror("malloc");
return 1;
}
//增加空间
p  = realloc(p,80);

注意:这种写法不严谨,因为我们的realloc函数有可能增容失败,比如后续实在没有空间了,就会增容失败,失败返回空指针赋值给P,就导致P原先维护的40个字节的空间也找不到了,内存空间既用不上也找不到,就会造成内存泄漏。正确的做法应该是重新使用一个指针来接收。

  • 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到 新 的空间。 realloc在调整内存空间的是存在两种情况:

情况1:原有空间之后有足够大的空间:直接在后面开辟空间后返回原始空间的地址。

情况2:当申请空间的后续空间的内存被别的占用无法扩展,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小 的连续空间来使用。这样函数返回的是一个新的内存地址。

四步:
1.首先会开辟新的空间

2.会将旧空间中的数据看拷贝到新空间

3.释放旧的空间(realloc函数会做)

4.返回新的空间的起始地址

我们来看一下这个函数的正确使用:

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功打印
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		p[i] = i + 1;
	}
	//增加一些空间
	int * ptr = (int*)realloc(p, 80);
	//判断一下
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;

	}
	else
	{
		perror("realoc");
		return 1;
	}
	for (i = 0; i < 20; i++)
	{
		printf("%d\n", p[i]);
	}
	//释放
	free(p);
	p = NULL;
	return 0;
}

3. 常见的动态内存错误

3.1 对NULL指针的解引用操作

void test()
{
 int *p = (int *)malloc(INT_MAX/4);
 *p = 20;//如果p的值是NULL,就会有问题
 free(p);
}

如果malloc开辟空间失败,返回空指针就会出问题,应该是开辟伴随着判断。

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

我们申请的空间也是有大小的,不可以申请40字节空间,做需要80空间的事。

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

我们的free只能释放我们的动态申请的内存空间

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

注意对比:

p[i] = i+1;

p[i] = *(p+i)//p指向的位置没变

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

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

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

这是一段极端代码,我们的程序运行起来,,由于p是局部变量,出了函数就销毁了,那开辟的空间虽然在,我却没有地址找到了,想释放都释放不了用不上也找不到就内存泄漏了。

每天跑一跑,内存少一少,内存耗干重启又开始耗,这就是一个服务器BUG代码。

最好的理解例子:

梁朝伟的无间道:

当卧底的一直和唯一的警察单线联系,当这个警察死掉了,就没人知道你的真实身份了。

4.动态内存经典笔试题

4.1题目1

void GetMemory(char *p)
{
 p = (char *)malloc(100);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(str);
 strcpy(str, "hello world");
 printf(str);
}

问运行会有什么样的后果

分析:

代码问题:

①对空指针进行解引用,导致程序崩溃

②动态申请的空间没有释放,且无法释放导致内存泄漏。

4.2 题目2:

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

问,运行以上代码会有什么样的结果:

分析:

结果是非法访问

这是典型的返回栈空间地址的问题

当我们使用static修饰,延长这个空间的生命周期就可以了:

4.3 题目3:

请问运行Test 函数会有什么样的结果?

void GetMemory(char **p, int num)
{
 *p = (char *)malloc(num);
}
void Test(void)
{
 char *str = NULL;
 GetMemory(&str, 100);
 strcpy(str, "hello");
 printf(str);
}

问题在于:最后申请弟弟空间没有释放
 

free(str);
str= NULL;

5. C/C++程序的内存开辟

C/C++程序内存分配的几个区域:

1. 栈区(stack):在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结 束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是 分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返 回地址等。

2. 堆区(heap):一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。分 配方式类似于链表。

3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。

4. 代码段:存放函数体(类成员函数和全局函数)的二进制代码。

     有了这幅图,我们就可以更好的理解在《C语言初识》中讲的static关键字修饰局部变量的例子了。 实际上普通的局部变量是在栈区分配空间的,栈区的特点是在上面创建的变量出了作用域就销毁。 但是被static修饰的变量存放在数据段(静态区),数据段的特点是在上面创建的变量,直到程序 结束才销毁 所以生命周期变长。

6.结语

以上就是本期的所有内容,知识含量蛮多,大家可以配合解释和原码运行理解。创作不易,大家如果觉得还可以的话,欢迎大家三连,有问题的地方欢迎大家指正,一起交流学习,一起成长,我是Nicn,正在c++方向前行的奋斗者,感谢大家的关注与喜欢。

 

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

程序员与电脑:不眠之夜的背后故事

在这个数字化飞速发展的时代&#xff0c;程序员和他们的电脑成了不可分割的伙伴。 如果你有机会深夜走过城市的某个角落&#xff0c;透过窗户瞥见那些亮着的电脑屏幕&#xff0c;你可能会好奇&#xff1a;这些电脑为什么总是开着的&#xff1f; 难道程序员们都有失眠症吗&…

代码随想录刷题笔记 DAY 25 | 组合问题 No.77 | 组合求和III No.216 | 电话号码的字母组合 No.17

文章目录 Day 2501. 组合问题&#xff08;No. 77&#xff09;2.1 题目2.2 笔记2.3 代码 02. 组合求和III&#xff08;No. 216&#xff09;2.1 题目2.2 笔记2.3 代码 03. 电话号码的字母组合&#xff08;No. 17&#xff09;3.1 题目3.2 笔记3.3 代码3.4 补充 Day 25 01. 组合问…

大数据 - Spark系列《五》- Spark常用算子

Spark系列文章&#xff1a; 大数据 - Spark系列《一》- 从Hadoop到Spark&#xff1a;大数据计算引擎的演进-CSDN博客 大数据 - Spark系列《二》- 关于Spark在Idea中的一些常用配置-CSDN博客 大数据 - Spark系列《三》- 加载各种数据源创建RDD-CSDN博客 大数据 - Spark系列《…

在Ubuntu上部署Stable Video Diffusion动画制作

Stable Diffusion团队推出的开源模型Stable Video Diffusion&#xff0c;支持生成约3秒的视频&#xff0c;分辨率为5761024。通过测试视频展示了其令人瞩目的性能&#xff0c;SVD模型是一个生成图像到视频的扩散模型&#xff0c;通过对静止图像的条件化生成短视频。其特点主要包…

AWD-Test2

1.已知账号密码&#xff0c;可SSH连接进行代码审计。2.登录可万能密码进入&#xff0c;也可注册后登录。3.修改url参数&#xff0c;发现报错。确定为Linux系统4.写入一句话&#xff0c;并提交。&#xff08;也可以文件上传&#xff0c;这里采用简洁的方法&#xff09; <?p…

书生·浦语大模型第四课作业

基础作业&#xff1a; 构建数据集&#xff0c;使用 XTuner 微调 InternLM-Chat-7B 模型, 让模型学习到它是你的智能小助手&#xff0c;效果如下图所示&#xff0c;本作业训练出来的模型的输出需要将不要葱姜蒜大佬替换成自己名字或昵称&#xff01; 1.安装 # 如果你是在 Int…

形态学操作之开操作与闭操作的python实现——数字图像处理

原理 图像处理中的开操作&#xff08;Opening&#xff09;和闭操作&#xff08;Closing&#xff09;是形态学&#xff08;Morphological&#xff09;操作的两个基本类型&#xff0c;它们都是基于膨胀&#xff08;Dilation&#xff09;和腐蚀&#xff08;Erosion&#xff09;操…

H12-821_31

31.下面是一台路由器的部分配置,关于该配置描述正确的是: A.源地址为1.1.1.1的数据包匹配第一条ACL语句rule 0,匹配规则为允许 B.源地址为1.1.1.3的数据包匹配第三条ACL语句rule 2,匹配规则为拒绝 C.源地址为1.1.1.4的数据包匹配第四条ACL语句rule 3,匹配规则为允许 D.源地址为…

【动态规划】:泰波那契模型_解码方法

朋友们、伙计们&#xff0c;我们又见面了&#xff0c;本专栏是关于各种算法的解析&#xff0c;如果看完之后对你有一定的启发&#xff0c;那么请留下你的三连&#xff0c;祝大家心想事成&#xff01; C 语 言 专 栏&#xff1a;C语言&#xff1a;从入门到精通 数据结构专栏&…

Codeforces Round 169 (Div. 2)C. Little Girl and Maximum Sum(差分、贪心)

文章目录 题面链接题意题解代码总结 题面 链接 C. Little Girl and Maximum Sum 题意 给q个[l,r]将所有这些区间里面的数相加和最大。 可以进行的操作是任意排列数组 题解 对出现的每个区间内的位置加上1&#xff0c;代表权值 操作完之后求一遍前缀和&#xff0c;得到每个…

通俗易懂搞定forEach和map的区别和使用

前言 在前端开发的时候&#xff0c;我们有时候需要对后端传过来的数组的进行相应的遍历处理&#xff0c;但是很多小伙伴对map和forEach两个方法的界限还是有模糊&#xff0c;本文从具体的示例来弄清楚两者的相同点和不同点 相同点 1.都是用于遍历数组的方法&#xff0c;可以…

关于氢能,什么是绿氢、蓝氢、灰氢?

今年3月,国家有关部门出台《氢能产业发展中长期规划(2021—2035年)》,明确了氢的能源属性,同时明确氢能是战略性新兴产业的重点方向是构建绿色低碳产业体系、打造产业转型升级的新增长点。一时间,氢能发展也受到了万众瞩目。我国目前已经成为世界最大的制氢国年制氢产量约…

L2-002 链表去重

一、题目 二、解题思路 结构体数组的下标表示该节点的地址&#xff0c;value 表示该节点的值&#xff0c;next 表示下一个结点的地址。result1 表示去重后的链表的节点的地址&#xff0c;result2 表示被删除的链表的节点的地址。 flag 表示节点对应的值是否出现过&#xff0c;…

【小赛1】蓝桥杯双周赛第5场(小白)思路回顾

我的成绩&#xff1a;小白(5/6) 完稿时间&#xff1a;2024-2-13 比赛地址&#xff1a;https://www.lanqiao.cn/oj-contest/newbie-5/ 相关资料&#xff1a; 1、出题人题解&#xff1a;“蓝桥杯双周赛第5次强者挑战赛/小白入门赛”出题人题解 - 知乎 (zhihu.com) 2、矩阵快速幂&…

《Java 简易速速上手小册》第9章:Java 开发工具和框架 (2024 最新版)

文章目录 9.1 Maven 和 Gradle - 构建与依赖管理的神兵利器9.1.1 基础知识9.1.2 重点案例&#xff1a;使用 Maven 构建 Spring Boot 应用9.1.3 拓展案例 1&#xff1a;使用 Gradle 构建多模块项目9.1.4 拓展案例 2&#xff1a;利用 Gradle Wrapper 确保构建的一致性 9.2 Spring…

【JavaEE Spring】Spring 原理

Spring 原理 1. Bean的作⽤域1.1 概念1.2 Bean的作⽤域 2. Bean的⽣命周期 1. Bean的作⽤域 1.1 概念 在Spring IoC&DI阶段, 我们学习了Spring是如何帮助我们管理对象的. 通过 Controller , Service , Repository , Component , Configuration ,Bean 来声明Bean对象。通…

数学建模:K-means聚类手肘法确定k值(含python实现)

原理 当K-means聚类的k值不被指定时&#xff0c;可以通过手肘法来估计聚类数量。   在聚类的过程中&#xff0c;随着聚类数的增大&#xff0c;样本划分会变得更加精细&#xff0c;每个类别的聚合程度更高&#xff0c;那么误差平方和&#xff08;SSE&#xff09;会逐渐变小&am…

备战蓝桥杯---图论基础理论

图的存储&#xff1a; 1.邻接矩阵&#xff1a; 我们用map[i][j]表示i--->j的边权 2.用vector数组&#xff08;在搜索专题的游戏一题中应用过&#xff09; 3.用邻接表&#xff1a; 下面是用链表实现的基本功能的代码&#xff1a; #include<bits/stdc.h> using nam…

那些 C语言指针 你不知道的小秘密 (完结篇)

本篇会加入个人的所谓‘鱼式疯言’ ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 我会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. &#x1f92d;&#x1f92d;&#x1f92d;可能说的不是那么严谨.但小编初心是能让更多人能…

蓝桥杯嵌入式第11届真题(完成) STM32G431

蓝桥杯嵌入式第11届真题(完成) STM32G431 题目 代码 程序和之前的大同小异&#xff0c;不过多解释 main.c /* USER CODE BEGIN Header */ /********************************************************************************* file : main.c* brief :…