C语言动态内存管理(26)

news2024/11/27 16:43:58

文章目录

  • 前言
  • 一、引子
  • 二、malloc
  • 三、calloc
  • 四、realloc
  • 五、free
  • 六、常见的动态内存错误
    • 对NULL指针进行解引用操作
    • 对动态开辟空间的越界访问
    • 对非动态开辟的内存使用free释放
    • 使用free释放动态开辟内存的一部分
    • 对同一块内存多次释放
    • 动态开辟内存忘记释放(内存泄漏)
  • 七、柔性数组(flexible array)
    • 定义
    • 特性
    • 对比
  • 八、C/C++中程序内存区域规划
  • 总结


前言

  如果未来要想学好数据结构,那么你对指针、结构体还有本篇的动态内存的理解掌握能力是要很高的
  所以跟我一起开始本篇的学习吧!


一、引子

假设我们现在想开辟一个根据用户需求大小的数组

#include <stdio.h>

int main()
{
	int n = 0;
	scanf("%d", &n);
	int arr[n];
	
	return 0;
}

这个代码在C99标准下是可以运行的,但大多数编译器并不支持C99标准,所以这种代码缺乏了跨平台性(可移植性)

例如常用的VS的编译器就不支持这么做

那么我们有没有办法写出一个既可以满足题目要求,又可以在任何一个编译器下都编译得过去的代码呢?答案是肯定的。这就和C语言中的动态内存的开辟有关了,动态开辟,即可以按照需求开辟内存的大小

另外,下面介绍的几个函数的操作对象都是堆区的内存
局部变量存放在内存中的栈区;全局变量、静态变量(static修饰的变量)存放在内存中的静态区(也叫数据段),以后我们会再详细介绍这个,不用着急

二、malloc

void* malloc(size_t size);

在这里插入图片描述
malloc函数的功能是开辟指定字节大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL(所以我们必须要加个条件判断一下)。传参时只需传入需要开辟的字节个数

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)malloc(10 * sizeof(int));
	// 因为malloc函数的返回值为void*,所以需要强制类型转换为对应类型。
	if (p == NULL)
	{
		printf("内存开辟失败\n");
	}
	else
	{
		printf("内存开辟成功\n");
		// 使用...
		free(p);
		p = NULL;
	}
	return 0;
}

这是假设我们要开辟一个可以存放10个整型的空间的话,应进行的步骤

同时,我们也要注意malloc的几个特性:

  1. 向内存申请空间不完成初始化,返回指向这块空间的大小
  2. malloc是void*类型,当我们申请空间时候,需要知道申请空间交给什么类型去维护,也就是说这需要指针强转
  3. 如果参数size为0,malloc可能会报错(取决于编译器)
  4. 同时申请空间有时候不一定会成功。如果失败的话,将会返回一个空指针,比如申请的空间太大,就会申请失败,这一点使用的时候要去注意

三、calloc

void* calloc(size_t num, size_t size);

在这里插入图片描述

calloc函数的功能也是开辟指定大小的内存空间,如果开辟成功就返回该空间的首地址,如果开辟失败就返回一个NULL。但calloc函数传参时需要传入两个参数(开辟的内存用于存放的元素个数和每个元素的大小)

真要说用法的话,与malloc大同小异:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)calloc(10 , sizeof(int));
	// 开辟一个可以存放10个整型的内存空间
	if (p == NULL)
	{
		printf("内存开辟失败\n");
	}
	else
	{
		printf("内存开辟成功\n");
		// 使用...
		free(p);
		p = NULL;
	}
	
	return 0;
}

但是,跟malloc有一个很大的区别在于:calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0

四、realloc

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

在这里插入图片描述

realloc函数可以调整已经开辟好的动态内存的大小,第一个参数是需要调整大小的动态内存的首地址,第二个参数是动态内存调整后的新大小

realloc函数与上面两个函数一样,如果开辟成功便返回开辟好的内存的首地址,开辟失败则返回NULL

具体使用方法如下:

#include <stdio.h>
#include <stdlib.h>

int main()
{
	int* p = (int*)calloc(10, sizeof(int));
	if (p == NULL)
	{
		printf("内存开辟失败\n");
	}
	else
	{
		printf("内存开辟成功\n");
		// 使用...
		int* ptr = (int*)realloc(p, 100);
		// 将空间扩展为100个字节大小
		if (ptr != NULL)
		{
			p = ptr; // 开辟成功时
			// 使用...
		}
		// 使用结束,释放内存(后面介绍)
		free(p);
		p = NULL;
	}
	
	return 0;
}

realloc相当于是重新开辟一块空间,因此会有几种不同情况:

一、当内存空间足够的时候,直接在申请好的空间追加
在这里插入图片描述
realloc函数直接在原空间后方进行扩展,并返回该内存空间首地址(即原来的首地址)

二、当内存空间不够的时候,会在内存中寻找一块更大的空间存放,将目前的数据拷贝一份到新的空间位置中,再将原来的空间释放掉
在这里插入图片描述
realloc函数会在堆区中重新找一块满足要求的内存空间,把原空间内的数据拷贝到新空间中,并主动将原空间内存释放(即还给操作系统),返回新内存空间的首地址

三、需扩展的空间后方没有足够的空间可供扩展,并且堆区中也没有符合需要开辟的内存大小的空间
在这里插入图片描述
结果就是开辟内存失败,返回一个NULL

五、free

void free(void* memblock);

  free函数的作用就是将malloc、calloc以及realloc函数申请的动态内存空间释放,其释放空间的大小取决于之前申请的内存空间的大小

使用方式非常简单,就是在使用完后加上两个语句:

	free(p); // p为要释放的代码块的首地址
	p = NULL; // 必不可少

  我们也已经看到了,上面每一个开辟了动态内存的代码,在使用完该动态内存后,都将该内存空间释放了(即还给操作系统),如果使用完动态内存后忘记将其空间释放,便会造成内存泄漏的问题

  1. 内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果
  2. 内存泄漏缺陷具有隐蔽性、积累性的特征,比其他内存非法访问错误更难检测。因为内存泄漏的产生原因是内存块未被释放,属于遗漏型缺陷而不是过错型缺陷。此外,内存泄漏通常不会直接产生可观察的错误症状,而是逐渐积累,降低系统整体性能,极端的情况下可能使系统崩溃

就像对一个人来说,有时候急性病并不可怕,现发现治,怕得就是躲藏深处的慢性病,等到发作那天,纵是八方助力也无力回天
就像一段感情来说,有时候激烈的争吵并不可怕,这叫磨合,怕得就是两人不说话不沟通,矛盾隔阂由小积大,往后就是有力挽回也再难回到从前

请注意!

  1. 在释放代码块后,必须将该代码块的首地址改为NULL,否则该指针将变为野指针,我们都知道,野指针非常危险
  2. 如果传入free函数的为空指针(NULL),则free函数什么也不做

六、常见的动态内存错误

对NULL指针进行解引用操作

 如果我们在调用了malloc、calloc以及realloc函数之后,没有检测返回的指针的有效性(即是否为NULL指针),那我们在后面使用该指针的时候就可能会导致对NULL指针进行解引用操作,对于,我们应当予以避免

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

 我们需要时刻注意,不能访问未申请的动态内存空间。比如你向动态内存申请了10个字节,那就绝不能访问第11个字节

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

 free函数只能释放动态开辟的内存空间

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

 free函数只能从开辟好的动态内存空间的起始位置开始释放,所以使用free函数释放动态内存时,传入的指针必须是当时开辟内存时返回的指针

对同一块内存多次释放

 对同一块动态内存空间只能释放一次。避免这个问题的出现也很简单,我们只要记住在第一次释放完空间后立即将该指针置为NULL即可,因为当传入free函数的指针为NULL指针时,free函数什么也不做(也就不会出现对同一内存多次释放的问题)

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

 一定要做到自己开辟的动态内存自己记得释放,也许你觉得这件事没什么,但当你需要从几十万甚至几百万行代码中找出一个因忘记释放动态内存而造成的内存泄漏问题时,你就会真正知道这件事的重要性

七、柔性数组(flexible array)

定义

在C99中,结构体最后一个成员为未知大小的数组,这个被称为柔性数组的成员,帮助用户根据要求自己给大小,更加轻松地处理可变长度的数据结构

struct st_type
{
    int i;
    int nums[0]; // 有些编译器可能会编译失败,可以化成nums[]
}type_a;

几个定语修饰词要搞明白,最后一个成员、未知大小、数组

特性

  1. 结构体中至少有一个成员在柔性数组前面(如果顺序错了,也会报错)
  2. sizeof返回的这种结构大小是不包含柔性数组的内存,编译器在计算结构体大小时会忽略柔性数组成员
  3. 对包含柔性数组的结构体,申请空间的时候适度大于结构体的大小,以便于适应柔性数组的大小
typedef struct st_type
{
    int i;
    int a[0]; // 柔性数组成员
}type_a;

int main()
{
    printf("%zd\n", sizeof(type_a)); // 输出的是4
    
    return 0;
}

对比

下面来看两份代码:

struct S
{
	char c;
	int n;
	int arr[0]; //柔性数组
};


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

int main()
{
	// printf("%zd\n", sizeof(struct S));
	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;
}
struct S
{
	int n;
	int* arr;
};

int main()
{
	struct S* ps = (struct S*)malloc(sizeof(struct S));
	if (ps == NULL)
		return 1;
	
	ps -> arr = (int*)malloc(5*sizeof(int));
	if (ps -> arr == NULL)
		return 1;
	//使用
	ps -> n = 100;
	int i = 0;
	for (i = 0; i < 5; i++)
	{
		ps -> arr[i] = i;
	}

	// 调整数组大小
	int* ptr = (int*)realloc(ps -> arr, 10 * sizeof(int));
	if (ptr != NULL)
	{
		ps -> arr = ptr;
	}
	
	// 使用
	// ...

	// 释放
	free(ps->arr);
	free(ps);

	return 0;
}

对比之下,你更喜欢哪一种方式?

对于第一种使用柔性数组的办法
  一个好处是如果里面做了二次内存分配,并把整个结构体返回给用户。当用户需要释放空间时候,并不知道这个结构体内成员也需要free,如果结构体的内存以及其成员要的内存一次性分配好,返回一个结构体指针,用户只需要一次free就可以把所有的内存也给释放掉了
  另一个好处是连续的内存有益于提高访问速度,也有益于减少内存碎片

八、C/C++中程序内存区域规划

在这里插入图片描述
你可以先大概有个认识,具体一点可以移步Cpp专栏的动态内存管理,那边介绍得更为详尽一些
Cpp内存管理

我们可以来稍微了解一下几个内存区域的作用:

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

二、堆区(heap):
 一般由程序员分配释放,若程序员不释放,程序结束时可能由OS(操作系统)回收

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

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


总结

  应该不算太难,算修炼内功,但很重要,部分代码必须自己去实践!

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

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

相关文章

k8s 之安装helm服务

helm安装包下载helm官网_zh 作者&#xff1a;程序那点事儿 日期&#xff1a;2024/01/30 00:51 下载安装包 wget https://get.helm.sh/helm-v3.2.3-linux-amd64.tar.gz 解压安装包 tar -zxcf helm-v3.2.3-linux-amd64.tar.gz 进入到解压目录 cd linux-amd64 将helm目录拷贝…

101. 对称二叉树【 力扣(LeetCode) 】

文章目录 零、原题链接一、题目描述二、测试用例三、解题思路3.1 递归3.2 迭代 四、参考代码4.1 递归4.2 迭代 零、原题链接 101. 对称二叉树 一、题目描述 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 进阶&#xff1a;你可以运用递归和迭代两种方法解决…

YOLO11改进|注意力机制篇|引入上下文锚注意力机制CAA

目录 一、【CAA】注意力机制1.1【CAA】注意力介绍1.2【CAA】核心代码 二、添加【CAA】注意力机制2.1STEP12.2STEP22.3STEP32.4STEP4 三、yaml文件与运行3.1yaml文件3.2运行成功截图 一、【CAA】注意力机制 1.1【CAA】注意力介绍 CAA注意力机制的结构图如下&#xff0c;下面根据…

Selenium WebDriver和Chrome对照表

PS&#xff1a;我的没下载WebDriver 也没配置环境变量 也能用Selenium 网上有说把WebDriver放到chrome的安装目录并将路径配到path中【可能之前用playwright下载过】 查看浏览器版本号 在浏览器的地址栏&#xff0c;输入chrome://version/&#xff0c;回车后即可查看到对应版…

Java项目实战II基于Java+Spring Boot+MySQL的智能推荐的卫生健康系统(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者 一、前言 在健康意识日益增强的今天&#xff0c;如何为用户提供个性化、精准的卫生健康建议成为了一个亟待解决…

java中的多层循环控制,包括金字塔和九九乘法表的打印

多重循环控制 多重循环控制练习 多重循环控制 1.将一个循环放在另一个循环体内&#xff0c;就形成了嵌套循环。其中&#xff0c;for&#xff0c;while&#xff0c;do…while均可以作为外层循环和内层循环。【建议一般用两层&#xff0c;最多不要超过3层&#xff0c;否则代码的…

【前端】前端数据转化为后端数据

【前端】前端数据转化为后端数据 写在最前面格式化数组代码解释hasOwnProperty是什么&#xff1f; &#x1f308;你好呀&#xff01;我是 是Yu欸 &#x1f30c; 2024每日百字篆刻时光&#xff0c;感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限…

k8s实战-3

k8s实战-3 一、Ingress1.安装2.测试 二、存储抽象1.环境准备2.PV和PVC3.ConfigMap4.Secret 总结 一、Ingress Ingress 类似于一个“网关”&#xff0c;它负责将外部 HTTP 请求路由到集群内的服务。你可以把它想象成一个“交通警察”&#xff0c;根据请求的 URL 和路径&#xf…

无锡自闭症康复寄宿学校:每天的康复方式揭秘

无锡自闭症康复寄宿学校的启示&#xff1a;揭秘广州星贝育园的日常康复方式 在自闭症儿童的教育与康复领域&#xff0c;每一所学校都在用自己的方式探索着前行的道路。无锡的自闭症康复寄宿学校以其独特的康复方式和显著的效果&#xff0c;为众多家庭带来了希望。而在广州&…

jvisualvm学习

系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程&#xff1a;封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…

前端工程化 - Vue

环境准备 Vue-cli是Vue官方提供的一个脚手架&#xff0c;用户快速生成一个Vue的项目模板。 Vue-cli提供了如下功能&#xff1a; 统一的目录结构本地调试热部署单元测试集成打包上线 需要安装Node.js 安装Vue-cli npm install -g vue/cli通过vue --version指令查看是否安装成…

蓝桥杯省赛真题打卡day4

[蓝桥杯 2013 省 A] 大臣的旅费 题目描述 很久以前&#xff0c;T 王国空前繁荣。为了更好地管理国家&#xff0c;王国修建了大量的快速路&#xff0c;用于连接首都和王国内的各大城市。 为节省经费&#xff0c;T 国的大臣们经过思考&#xff0c;制定了一套优秀的修建方案&am…

模电中二极管,三极管和电容的应用

&#x1f3c6;本文收录于《全栈Bug调优(实战版)》专栏&#xff0c;主要记录项目实战过程中所遇到的Bug或因后果及提供真实有效的解决方案&#xff0c;希望能够助你一臂之力&#xff0c;帮你早日登顶实现财富自由&#x1f680;&#xff1b;同时&#xff0c;欢迎大家关注&&am…

使用 Python 遍历文件夹

要解决这个问题&#xff0c;使用 Python 的标准库可以很好地完成。我们要做的是遍历目录树&#xff0c;找到所有的 text 文件&#xff0c;读取内容&#xff0c;处理空行和空格&#xff0c;并将处理后的内容合并到一个新的文件中。 整体思路&#xff1a; 遍历子目录&#xff1…

三维模型点云化工具V1.0使用介绍:将三维模型进行点云化生成

三维软件绘制的三维模型导入之后&#xff0c;可以生成点云&#xff0c;用于替代实际的激光扫描过程&#xff0c;当然&#xff0c;主要是用于点云算法的测试和验证&#xff0c;没法真正模拟扫描的效果&#xff0c;因为太过于理想化了。 功能介绍 将三维软件绘制的三维模型变成…

一个月学会Java 第3天 对类的深刻认识

Day3 对类的深刻认识 第一章 方法 在Day2的时候已经浅浅的认识过类&#xff0c;但是还是不够深刻&#xff0c;我们现在来深刻的认识一下类是什么和他的结构&#xff0c;首先在认识类的结构之前我们需要再认识和了解这么一个东西&#xff0c;他就是——方法 方法(method)也叫函…

iMazing只能苹果电脑吗 Win和Mac上的iMazing功能有区别吗

在当今数字时代&#xff0c;管理和备份手机数据变得越来越重要。无论是转移照片、备份短信&#xff0c;还是管理应用程序&#xff0c;一个强大的工具可以大大简化这些操作。iMazing作为一款备受好评的iOS设备管理软件&#xff0c;已经成为许多用户的选择。但是&#xff0c;许多…

用manim实现Gram-Schmidt正交化过程

在线性代数中&#xff0c;正交基有许多美丽的性质。例如&#xff0c;由正交列向量组成的矩阵(又称正交矩阵)可以通过矩阵的转置很容易地进行反转。此外&#xff0c;例如&#xff1a;在由彼此正交的向量张成的子空间上投影向量也更容易。Gram-Schmidt过程是一个重要的算法&#…

GESP C++三级样题卷

&#xff08;满分&#xff1a;100 分 考试时间&#xff1a;90 分钟&#xff09; 一、单选题&#xff08;每题 2 分&#xff0c;共 30 分&#xff09; 1.下列关于负数的原码、反码、补码的描述中&#xff0c;正确的是( ) A 原码和反码互为按位取反&#xff08;符号位除外&…

[ComfyUI]Flux:太强了!任意扩图神器,小红书极致逼真风格出游打卡写实风

随着人工智能技术的不断发展&#xff0c;图像生成与反推技术已经成为了AI领域的一大热点。今天&#xff0c;我们就来为大家详细介绍一款由ComfyUI团队开发的超强图像反推工具——Flux&#xff0c;以及如何使用它实现任意扩图和极致逼真风格出游打卡写实风。 一、Flux&#xff…