c语言中动态内存管理

说到内存,大家一定都知道。但是有一种函数可以实现动态内存管理,下面大家一起学习。

文章目录

  • 一、为什么要有动态内存管理?
  • 二、malloc 和 free
    • 1.malloc
    • 2.free
  • 三、calloc 和 realloc
    • 1.calloc
    • 2.realloc
    • 3.常见的动态内存的错误
      • 3.1对NULL指针的解引用错误
      • 3.2对动态开辟空间的越界访问
      • 3.3对非动态开普内存使用free释放
      • 3.4 使用free释放一块动态开辟内存的一部分。
      • 3.5 对同一块动态内存多次释放。
      • 3.5 动态开辟内存忘记释放(内存泄漏)
    • 4.经典笔试题分析
      • 4.1
      • 4.2
      • 4.3
      • 4.4
  • 四、柔性数组
    • 1.柔性数组的特点
    • 2.柔性数组的使用


一、为什么要有动态内存管理?

经过c语言的学习,相信大家都知道如何开辟一块内存。

int i = 6;
int arr[10]={0};

上面代码开辟的空间,有两个特点:

  1. 空间开辟大小是固定的。
  2. 数组在申明的时候必须指定数组的长度,数组空间一旦确定大小就不能调整了。

这时候我们会发现当我们写代码的时候,我们需要开辟多少空间在程序运行的时候才能知道。这时候就需要引入动态内存开辟了,让程序员自己申请和释放空间。

二、malloc 和 free

1.malloc

void* malloc (size_t size);

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

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

在这里插入图片描述
这个代码用了malloc函数申请空间,先是申请了20个字节的空间强制转换为整形变为存放5个整数。

2.free

 void free (void* ptr);

函数free,是专门用来给动态内存的释放和回收的。

  1. 如果参数ptr指向的空间不是动态开辟的,那么free函数的行为毫无意义。
  2. 如果参数ptr是NULL指针,则函数什么事都不做。
#include <stdio.h>
#include <stdlib.h>
 int main()
 {
 	int num = 0;
 	scanf("%d", &num);
 	int arr[num] = {0};
 	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函数释放内存后,我们不能再访问该内存块,否则会导致未定义的行为。此外,如果我们尝试释放已经释放过的内存块,或者释放非动态分配的内存块,也会导致未定义的行为。当动态内存使用完毕之后,用free释放,释放后的指针是野指针,记得置空。

三、calloc 和 realloc

1.calloc

void* calloc (size_t num, size_t size);

calloc函数的功能是为num个大小为size的元素开辟一块空间,并把空间的每个字节置为0。
calloc函数与malloc函数的区别只在于calloc会在地址返回之前把申请的空间全部置为0。
在这里插入图片描述
输出结果全为 0;
在这里插入图片描述
当我们需要对申请的内存空间的内容进行初始化的时候,calloc函数就很方便的解决了这一问题。

2.realloc

 void* realloc (void* ptr, size_t size);

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

  1. ptr是要调整的内存地址。

  2. size是调整之后新的大小。

  3. 返回值为调整之后的内存起始位置。

  4. 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。

  5. realloc函数调整内存的是存在两种情况:

    	 1. 原有空间之后有足够大的空间。
    	 2. 原有空间之后没有足够大的空间。
    

在这里插入图片描述

在这里插入图片描述
这种情况当原有空间后面内存正好可以放下我们扩展的内存的时候。扩展内存就直接再原有内存之后直接追加空间,原有空间数据不发生变化。
在这里插入图片描述
这种情况当原有空间之后没有足够多的空间时,扩展方法时:在堆空间上另找一个合适大小的连续空间来使用,这样函数返回的是一个新的内存地址。

3.常见的动态内存的错误

3.1对NULL指针的解引用错误

 void test()
 {
	 int *p = (int *)malloc(INT_MAX/4);
 	 *p = 20;
 	 free(p);
 }

这个代码没有判断p是否为空指针,如果对空指针解引用可能会出现非法访问的错误。

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

 void test()
 {
	 int i = 0;
 	 int *p (int*)malloc(10*sizeof(int));
 	if(NULL == p)
 	{
 		exit(EXIT_FAILURE);
 	}
 	for(i=0; i<=10; i++)
 	{ 
 		*(p+i) = i;
 	}
	 free(p);
 }

当i是10的时候,指针已经越界访问了。

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

	void test()
	{
		int a =10;
		int* p=&a;
		free(p);
	}

要知道栈上的内存是不需要程序员手动释放的,只有堆上的内存需要程序员手动释放。

释放堆上内存的主要原因:

  1. 避免内存泄漏:如果我们不释放动态分配的内存,那么这块内存将一直被程序占用,无法被其他部分使用。如果重复分配内存而不释放,最终会导致内存耗尽,程序崩溃或运行缓慢。
  2. 释放不再使用的内存:在程序执行过程中,可能会动态地分配和释放内存。当我们不再需要某个内存块时,通过释放它,可以将其返回给操作系统,以便其他程序可以使用这块内存。
  3. 防止悬空指针:如果我们释放了一块内存,但仍然保留了指向该内存的指针,那么这个指针就成为了悬空指针。使用悬空指针可能导致未定义的行为,例如访问无效的内存,引发程序崩溃或产生不可预测的结果。通过释放内存并将指针置为,可以避免悬空指针的问题。
  4. 提高内存利用率:释放不再使用的内存可以提高内存的利用率。这对于内存有限的嵌入式系统或需要处理大量数据的应用程序尤为重要。

不需要释放栈上内存的原因:

1. 自动管理:栈上的内存分配和释放是由编译器自动完成的,程序员不需要手动干预。当函数执行完毕或者局部变量超出作用域时,编译器会自动将其所占用的内存空间释放。
2. 先进后出:栈是一种后进先出(LIFO)的数据结构,栈上的内存分配和释放遵循这个原则。每次函数调用时,会将局部变量和参数压入栈中,当函数返回时,会将这些变量从栈中弹出,实现内存的自动释放。
3. 快速高效:由于栈上的内存分配和释放是由编译器自动完成的,所以速度非常快。相比于堆上的内存分配和释放,栈上的内存管理更加高效。

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

 void test()
 {
 	int *p = (int *)malloc(100);
	p++;
 }
 	free(p);

这个代码p指针发生变化,p指针不在指向这块内存的起始位置,那么最后free只能释放一部分内存造成内存泄漏。

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

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

对同一块动态内存多次释放可能会造成内存泄漏、悬空指针、程序崩溃。

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

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

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
切记:动态开辟的空间⼀定要释放,并且正确释放。

4.经典笔试题分析

4.1

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

在GetMemory函数中,用malloc函数为p重新分配了内存,但是这个分配的内存并没有被传回函数中,因为在main函数中函数参数是按值传递的,所以在GetMemory函数中重新分配内存并不会改变str指针在main函数中的值。
在main函数中,str指针被初始化为NULL传递给GetMemory函数。由于GetMemory函数中的参数是按值传递的,所以GetMemory函数内部对p指针的修改不会影响到str指针。所以str指针仍然是NULL指针,在后面的strcpy和printf中会导致对空指针的解引用,产生未定义的内容。

4.2


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

这个代码中GetMemory函数是创建在栈上的,里面的char数组也是栈空间。随着函数的释放,返回到main函数的虽然是这个数组的首地址,但是str已经无权访问了变成了空指针。虽然有可能代码最后会输出hello word ,那是因为这个空间还没有被系统的其他空间所覆盖。

4.3

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

传址调用,并且 GetMemory函数用二级指针**p接收。在GetMemory函数释放空间的时候,成功的传回了动态内存的首地址使得str不在是空指针,可以进行strcpy和printf。但是没有释放p。

4.4

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

过早释放动态,没有了对动态内存空间的访问权限。后面的world无法拷贝。

四、柔性数组

C99中,结构中的最后⼀个元素允许是未知⼤⼩的数组,这就叫做『柔性数组』成员。

 struct st_type
 {
 int i;
 int a[];//柔性数组成员
 
};

1.柔性数组的特点

1.结构中的柔性数组成员前⾯必须⾄少⼀个其他成员。
2.sizeof返回的这种结构⼤⼩不包括柔性数组的内存。
3.包含柔性数组成员的结构⽤malloc()函数进⾏内存的动态分配,并且分配的内存应该⼤于结构的⼤⼩,以适应柔性数组的预期⼤⼩。

2.柔性数组的使用

struct S
{
	int n;//4
	int arr[];
};

int main()
{
	
	struct S* ps = (struct S*)malloc(sizeof(struct S) + 5*sizeof(int));
	if (ps == NULL)
	{
		perror("malloc");
		return 1;
	}
	ps->n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps->arr[i] = i;
	}
	//调整空间
	struct S* ptr = (struct S*)realloc(ps, sizeof(struct S)+10*sizeof(int));
	if (ptr != NULL)
	{
		ps = ptr;
	}
	//....

	//释放
	free(ps);
	ps = NULL;

	return 0;
}

到这里动态内存管理基本就结束了,大家如果有问题可以一起讨论。谢谢大家!

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

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

相关文章

【SpringBoot框架篇】37.使用gRPC实现远程服务调用

文章目录 RPC简介gPRC简介protobuf1.文件编写规范2.字段类型3.定义服务(Services) 在Spring Boot中使用grpc1.父工程pom配置2.grpc-api模块2.1.pom配置2.2.proto文件编写2.3.把proto文件编译成class文件 3.grpc-server模块3.1.pom文件和application.yaml3.2.实现grpc-api模块的…

Linux——信号概念与信号产生方式

目录 一、概念 二、前台进程与后台进程 1.ctrlc 2.ctrlz 三、信号的产生方式 1.键盘输入产生信号 2.系统调用发送信号 2.1 kill()函数 2.2 raise()函数 2.3 abort()函数 3.异常导致信号产生 3.1 除0异常 3.2 段错误异常 4.软件条件产生信号 4.1 管道 4.2 闹钟…

最新可用免费VPS云服务器整理汇总

随着云计算技术的不断发展&#xff0c;越来越多的个人和企业开始关注和使用VPS云服务器。VPS云服务器以其高度的灵活性、可定制性和安全性&#xff0c;成为了一种受欢迎的服务器解决方案。然而&#xff0c;对于初学者或者预算有限的用户来说&#xff0c;如何选择合适的免费VPS云…

ZYNQ学习之Ubuntu系统的简单设置与文本编辑

基本都是摘抄正点原子的文章&#xff1a;<领航者 ZYNQ 之嵌入式Linux 开发指南 V3.2.pdf&#xff0c;因初次学习&#xff0c;仅作学习摘录之用&#xff0c;有不懂之处后续会继续更新~ 一、Ubuntu的简单操作 1.1 切换拼音输入法 Ubuntu 自带的拼音输入法&#xff0c;有两种…

ADAS多传感器后融合算法解析-下篇

ADAS多传感器后融合算法解析-下篇 在ADAS多传感器后融合(上)中我们介绍了后融合的接口、策略。本文将主要介绍后融合的实现流程、难点及注意事项。 附赠自动驾驶学习资料和量产经验&#xff1a;链接 二、后融合处理流程 如下图为基本RC后融合系统流程图&#xff0c;接下来将…

day 36 贪心算法 part05● 435. 无重叠区间 ● 763.划分字母区间 ● 56. 合并区间

一遍过。首先把区间按左端点排序&#xff0c;然后右端点有两种情况。 假设是a区间&#xff0c;b区间。。。这样排列的顺序&#xff0c;那么 假设a[1]>b[0],如果a[1]>b[1]&#xff0c;就应该以b[1]为准&#xff0c;否则以a[1]为准。 class Solution { public:static bo…

argocd部署

一、前言 ArgoCD 是一个开源的、持续交付工具&#xff0c;用于自动化部署应用程序到 Kubernetes 集群。它基于 GitOps 理念&#xff0c;通过使用 Git 作为单一的源头来管理应用程序的配置和部署状态&#xff0c;argocd会定时监控git仓库中的yaml配置文件&#xff0c;当git仓库中…

验证码/数组元素的复制.java

1&#xff0c;验证码 题目&#xff1a;定义方法实现随机产生一个5位的验证码&#xff0c;前面四位是大写或小写的英文字母&#xff0c;最后一位是数字 分析&#xff1a;定义一个包含所有大小写字母的数组&#xff0c;然后对数组随机抽取4个索引&#xff0c;将索引对应的字符拼…

JSON Web Token 入门教程

JSON Web Token&#xff08;JWT&#xff09;是一种可以在多方之间安全共享数据的开放标准&#xff0c;JWT 数据经过编码和数字签名生成&#xff0c;可以确保其真实性&#xff0c;也因此 JWT 通常用于身份认证。这篇文章会介绍什么是 JWT&#xff0c;JWT 的应用场景以及组成结构…

46秒AI生成真人视频爆火,遭在线打假「换口型、声音」

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源 发布在https://it.weoknow.com 更多资源欢迎关注 是炒作还是真正的 AI 视频能力进化&#xff1f; AI 生成视频已经发展到这个程…

rabbitmq集群问题排查

blowcode-test-redis04、blowcode-test-redis05、blowcode-test-redis06 这3个节点搭建的rabbitmq集群&#xff0c;04是主节点。 某次分别观察3个节点的管理页面&#xff0c;先都只能看到自己的节点是正常的绿色状态&#xff0c;猜测节点都各自为政了。 下图是05节点成功加入0…

iOS_convert point or rect 坐标和布局转换+判断

文章目录 1. 坐标转换2. 布局转换3. 包含、相交 如&#xff1a;有3个色块 let view1 UIView(frame: CGRect(x: 100.0, y: 100.0, width: 300.0, height: 300.0)) view1.backgroundColor UIColor.cyan self.view.addSubview(view1)let view2 UIView(frame: CGRect(x: 50.0, …

Redis面试题-缓存穿透,缓存击穿,缓存雪崩

1、穿透: 两边都不存在&#xff08;皇帝的新装&#xff09; &#xff08;黑名单&#xff09; &#xff08;布隆过滤器&#xff09; 解释&#xff1a;请求的数据既不在Redis中也不在数据库中&#xff0c;这时我们创建一个黑名单来存储该数据&#xff0c;下次再有类似的请求进来…

TikTok养号保姆式操作攻略

TikTok养号的重要性不必多少&#xff0c;不仅可以在创号初期保障账号安全&#xff0c;后期的账号流量也需要以前期养好账号为前提。下面就给大家分享如何养号的真实操作攻略&#xff01; 一、为什么要养号 &#xff08;1&#xff09;提高系统推荐精准度 系统不了解新账户人设…

AtCoder Beginner Contest 337 A - E

A - Scoreboard 大意 高桥队和青木队进行了场比赛&#xff0c;给出每场比赛中高桥队和青木队的积分&#xff0c;问最后谁总分更高或平局。 思路 统计总分比较即可。 代码 #include<iostream> using namespace std; int main(){int n, a0, b0;cin >> n;while(…

报错:ValueError: assignment destination is read-only 的解决方案

运行出现报错&#xff1a; ValueError: assignment destination is read-only 解决方案&#xff1a; 加一个.copy() data open_file(r"G:/21.tif").transpose(2,0,1)data data.copy()

Linux下的多线程编程:原理、工具及应用(5)

&#x1f3ac;慕斯主页&#xff1a;修仙—别有洞天 ♈️今日夜电波&#xff1a;Flower of Life—陽花 0:34━━━━━━️&#x1f49f;──────── 4:46 &#x1f504; ◀️ ⏸ ▶️ ☰ …

python--切片

1.切片&#xff1a; 切片是编程语言为有序序列&#xff08;sequence&#xff09;准备的&#xff0c;用来切割或者截取某个片段 一个完整的切片是包含三个参数和两个冒号" : " ,用于分隔三个参数(start_index、end_index、step)。当只有一个“:”时&#xff0c;默认第…

java设计模式之适配器模式

适配器模式概述 众所周知,我们国家的生活用电的电压是220V,而笔记本电脑、手机等电子设备的工作电压没有这么高,为了使笔记本、手机等设备可以使用220V的生活用电,就需要使用电源适配器(ACAdapter),也就是人们常说的充电器或变压器,有了这个电源适配器,原本不能直接工作…

2024室内设计和建筑必须知道的十大3D渲染趋势!

2023年对建筑圈是非常不平凡的一年&#xff0c;高清视频渲染、元宇宙全覆盖、AI模型大爆发.....不断发展的 3D 数字技术世界正迅速重塑建筑设计行业。 2024年&#xff0c;室内设计和建筑设计领域在3D渲染方面又将迎来怎样的变革&#xff1f;以下十大3D渲染趋势&#xff0c;你不…