C语言--动态内存管理(图解)

news2024/11/28 10:08:25

文章目录

  • C程序的内存开辟
  • 为什么存在动态内存分配
  • 动态内存分配函数
    • malloc和free
    • calloc
    • realloc
  • 常见的动态内存错误
    • 对空指针的解引用操作
    • 对动态开辟空间的越界访问
    • 对非动态开辟内存使用free释放
    • 使用free释放一块动态开辟内存的一部分
    • 对同一块动态内存多次释放
    • 动态开辟内存忘记释放
  • 例题
    • 1
    • 2
    • 3
    • 4
  • 柔性数组

C程序的内存开辟

  1. 静态存储区分配:静态存储区分配的内存是在编译时就确定的,生命周期随程序的运行始终存在。例如,全局变量和static变量就是在静态存储区分配内存的例子。
  1. 栈空间分配:栈空间分配是函数调用时自动进行的,函数内的局部变量和临时变量都是在栈上分配内存的。当函数调用结束时,栈上的内存会自动释放。
  1. 堆空间分配:堆空间分配是动态内存分配的一种方式,需要手动申请和释放内存。使用动态内存分配函数如malloc()、calloc()、realloc()来在堆上分配内存,使用free()函数来释放堆上的内存。堆上分配的内存在整个程序执行过程中都可用,并且需要手动释放,否则可能导致内存泄漏。
    这也是本章要讲的主要内容

这些不同的内存分配方式适用于不同的场景,静态存储区适用于全局变量和长时间存在的变量,栈空间分配适用于函数的局部变量和临时变量,而堆空间分配适用于需要在程序运行过程中动态分配和释放内存的情况。
在这里插入图片描述

为什么存在动态内存分配

C语言存在动态内存分配的主要原因是为了灵活管理内存资源,并提供对内存的动态调整和使用。
相比静态内存分配(全局变量和局部静态变量)在编译时就确定所占内存的大小,无法根据程序的运行进行动态扩展和缩小。而动态内存分配可以根据程序运行的需求而进行动态申请和释放内存
并且动态内存分配可以避免静态内存分配造成的过度浪费。例如一个数组:int arr[100];但我只想用一个整型大小,那么相对应的就浪费了99个整型大小的空间;

动态内存分配函数

动态内存分配函数都声明在

#include<stdlib.h>

malloc和free

这是C语言中最为常用的动态内存分配函数,且malloc()和free()经常搭配使用;

void* malloc (size_t size);
分配内存块
返回指向块开头的指针
新分配的内存块的内容未初始化,保留不确定的值。
如果开辟成功,则返回一个指向开辟好空间的指针。
如果开辟失败,则返回一个NULL指针,因此malloc的返回值一定要做检查.
由于malloc返回类型是void*,所以一般我们都会将它强转换对应的类型;

void free (void* ptr);
释放内存块
一般释放完的指针指向NULL;

下图是在空间开辟的大体思路:

在这里插入图片描述

下面看代码:

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

int main()
{
	//int arr[10];
    //开辟
	int* p = (int*)malloc(sizeof(int)*10);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功,就相当于一个数组,但是没有初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
    //释放
	free(p);
	p = NULL;//置空


	
	return 0;
}

在这里插入图片描述
对于开辟的空间,里面的内容是随机的;
下面就介绍一个自带初始化的内存开辟函数。

calloc

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

以上面例子变化:

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

int main()
{
	//int arr[10];
    //开辟
	int* p = (int*)calloc(10,sizeof(int));
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//开辟成功,就相当于一个数组,但是没有初始化
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
    //释放
	free(p);
	p = NULL;//置空


	
	return 0;
}

在这里插入图片描述
但对于上面的两个函数来说,当我们在堆区开辟内存后,当我们想改变开辟空间的大小时就得重新free掉。当然free完指针也该指向NULL,所以重新开辟不一定在原来地址上;
![在这里插入图片描述](https://img-blog.csdnimg.cn/003b03cfdec54c64bccb693331642f0c.png
下面就介绍一种不用free掉的。

realloc

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

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

realloc在调整内存空间的是存在两种情况:
情况1:原有空间之后有足够大的空间
情况2:原有空间之后没有足够大的空间

在这里插入图片描述

int main()
{
	int* p = (int*)malloc(40);
	if (p == NULL)
	{
		perror("malloc");
		return 1;
	}
	//初始化为1~10
	int i = 0;
	for (i = 0; i < 10; i++) 
	{
		p[i] = i + 1;
	}
	//增加一些空间
	int* ptr = (int*)realloc(p, 8000);
	if (ptr != NULL)
	{
		p = ptr;
		ptr = NULL;
	}
	else
	{
		perror("realloc");
		return 1;
	}
	//打印数据
	for (i = 0; i < 20; i++)
	{
		printf("%d\n", p[i]);
	}
	//释放
	free(p);
	p = NULL;

	return 0;
}

在这里插入图片描述

常见的动态内存错误

对空指针的解引用操作

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

对于开辟失败的空间,那么p指向的就为空,所以我们开辟空间要养成习惯,加入条件判断;

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

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;//当i是10的时候越界访问
 }
 free(p);
}

这个道理和在数组中越界访问是一样的,在开辟空间之外访问,程序就会报错;
在这里插入图片描述

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

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

对于指针p来说,它指向的是变量a的地址,而变量a开辟的空间所属空间在栈区,而free作用在堆区,两者根本没有任何联系,所以禁止这种操作;
在这里插入图片描述

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

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

这样操作,在堆区中开辟的空间还有剩余,程序就会报错;

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

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

对于一块动态内存,释放完在堆区上就没有空间的开辟了,再一次释放就会程序报错。

动态开辟内存忘记释放

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

没有进行free释放malloc的空间,会造成内存泄漏

例题

1

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

当程序跑起来时,会报错;
这是因为str传参过去,指针p指向的地址会创建一个内存空间;对于str指针来说没有任何影响,仍然为空,strcpy对于目标指针为空,将会报错;而当该函数结束时,那么p创建的内存由于没有free掉,会造成内存泄漏。
在这里插入图片描述

2

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

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

程序可以执行,但没有显示或者显示结果为错误信息;

这是由于GetMemory函数返回的是一个空指针,函数结束的时候p就为空
返回的就是一个空指针,打印出来的就是错误的;

在这里插入图片描述

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);
}
int main()
{
	Test();
	return 0;
}

打印结果:hello

相对于第一题来说,是地址传参,所以可以打印出来,但没有free掉p指向的空间,所以还会造成内存泄漏
在这里插入图片描述

4

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

打印结果:
gcc环境:无
VS:world

在上面我们就说,对于释放完的空间,最好置空;当str指向的空间被释放,虽然str指向的地址不为空,但由于没有空间大小,在不同的编译环境下就会出错;
在这里插入图片描述

柔性数组

柔性数组(Flexible Array)是一种在编程语言中使用的数据结构,它允许在已知的固定部分后面添加可变长度的数据

通常情况下,数组的大小是固定的,即在声明数组时需要指定数组的大小。然而,柔性数组在声明时只需要指定已知的固定部分的大小,而不需要指定可变部分的大小。

柔性数组的一种常见应用是在C语言中实现变长结构体。通过在结构体中定义一个数组成员作为柔性数组,可以根据需要动态改变结构体的大小,而不需要重新分配内存空间。

示例代码:

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

struct flex_array {
    int length;
    int data[]; // 柔性数组
};

int main() 
{
    int size = 5;
    struct flex_array* my_array = malloc(sizeof(struct flex_array) + size * sizeof(int));
   if(my_array==NULL)
    {
        perror("my_array");
        return;
    }
    //柔性数组的动态内存分配形式
    my_array->length = size;
    for (int i = 0; i < size; i++) {
        my_array->data[i] = i + 1;
        printf("my_array->data[%d]:%d\n",i,my_array->data[i]);
    }
     free(my_array);
    my_array=NULL;
    return 0}

在这里插入图片描述
好处:

第一个好处是:方便内存释放
如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第二个好处是:这样有利于访问速度.
连续的内存有益于提高访问速度,也有益于减少内存碎片。

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

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

相关文章

浅析电力企业一体化云运维管理平台

摘要&#xff1a;电力的发展,关系着我国社会和谐和稳定,在当今科学技术不断向前发展的时代,在电力企业发展中须要结合现今的科学技术,保证电力企业的信息化建设水平能够符合时代的发展趋势。本文主要分析当前电力企业一体化云运维管理的重要性,并就云运维管理中存在的问题进行有…

Spring 6【BeanFactory代码演示、实例化Bean的两种方式】(三)-全面详解(学习总结---从入门到深化)

目录 六、BeanFactory代码演示 七、实例化Bean的两种方式 六、BeanFactory代码演示 上面的案例代码就是我们平时使用Spring Framework的代码。 为了让小伙伴们能感受到BeanFactory&#xff0c;我们还是用实际代码来进行演示一下。毕竟 ApplicationContext在牛&#xff0c;对…

hadoop学习之hdfs学习

HDFS 文件系统,可以说是分布式数据库吧 结构是 目录树 适用场景:一次写入,多次读出.好像不太支持改删 优点: 1.高容错: 因为他会备份,所以一份出问题了,并不影响其他几份 如果副本丢失后,定时恢复.应该是定时检查然后恢复 每次启动,DN向NN汇报备份的存储情况.默认每个6个小时重…

波奇学Linux:git和gdb调试

git用来版本控制&#xff0c;同样是版本控制的软件还有svn等。 git的特定是具有网络功能的版本控制器&#xff0c;开源&#xff0c;client和server是一体的。(去中心化分布式管理) client和server一体意味着远程仓库和本地仓库是平等地位&#xff0c;远程仓库是特殊的仓库而已…

rtmp推流

目录 1、解压代码工程2、进入工程文件夹3、修改Makefile中的交叉编译路径4、编译5、板子上6、window上打开ffplay进行拉流注意:推流之前要先搭建好nginx服务器 1、解压代码工程 sudo unzip ffmpeg_rv1126_network_project_mark_finally.zip 2、进入工程文件夹 cd ffmpeg_rv…

AudioFocus源码分析

使用情景 在音视频app开发中一般会遵循音频焦点的机制&#xff0c;播放时申请音频焦点&#xff0c;丢失焦点后暂停播放&#xff0c;恢复焦点后继续播放等。尤其在车载开发时&#xff0c;涉及到三方应用和自研应用&#xff0c;导致经常出现音频焦点混乱混音等问题。 private f…

分享几个不常用的web api

分享几个不常用的web api 屏幕捕获 顾名思义&#xff0c;屏幕捕获 API 允许我们捕获屏幕内容&#xff0c;从而使构建屏幕录制的过程变得轻而易举。 在示例中我们使用video标签来显示捕获屏幕内容。 <video id"preview" autoplay>不支持HTML5 </video>…

Jmeter(119)-函数threadNum妙用

今天的接口场景是&#xff1a;有N个用户需要每隔5秒去查询一次数据&#xff0c;也就是说N个用户会去循环执行同一个接口。一开始的时候将用户参数化时使用了counter&#xff0c; 要执行2个线程3次循环&#xff0c;发现每次循环时&#xff0c;接口中用户参数的数据就会不一样&am…

统计页面左右+上下自适应布局

1:如果需要调整分栏数量,那么只需要删除对应数据,修改百分比即可. <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><style type"text/css" lang"less" >body{margin: 0px ;}.box…

2.1Label Button 标签和按钮

2.1Label & Button 标签和按钮 窗口主体框架 每一个 tkinter 应用的主体框架都可以包含下面这部分. 定义 window 窗口 和 window的一些属性, 然后书写窗口内容, 最后执行window.mainloop让窗口活起来. import tkinter as tkwindow tk.Tk() window.title(my window) wind…

【C语言高阶篇】成为编程高手必学内容,程序中的动态内存分配我不允许还有人不会!

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《快速入门C语言》《C语言高阶篇》 ⛺️生活的理想&#xff0c;就是为了理想的生活! 文章目录 前言&#x1f4ac; 为什么存在动态内存分配&#x1f4ac; 动态内存函数的介绍1️⃣ 动态内存函数 malloc&#…

西门子PLC上位机测试

上一篇我们讲了三菱PLC的数据通信方法&#xff0c;今天我们讲讲另外一个PLC巨头--西门子。 西门子有很多系列&#xff0c;今天讲到的是用S7协议的S71200。西门子同样提供了丰富的集成库&#xff0c;例如S7.NET&#xff0c;对于C#上位机开发&#xff0c;是非常容易的事情。 首…

API开发,机器人api二次开发

由于自身在机器人方面滚爬多年&#xff0c;尝试了很多次&#xff0c;选择了一个信任的工具 可以给有需要的朋友们借鉴一下 开发起来很方便&#xff0c;技术也已经挺成熟的了 贴一点简单的给大家看下呢 测试文档&#xff1a;https://www.wkteam.cn/ 简要描述&#xff1a; …

10.Ceph接口使用

文章目录 Ceph接口使用CephFS文件系统服务端添加mds服务创建存储池授权用户权限 客户端前期准备客户端挂载方式一&#xff1a;基于内核方式二&#xff1a;基于 fuse 工具 Ceph 块存储系统 RBD 接口服务端创建存储池和镜像管理镜像 客户端镜像挂载快照管理快照分层快照展平镜像的…

Ubuntu20.04升级到Ubuntu 22.04

升级Ubuntu到最新版本 执行如下命令将Ubuntu升级到最新的版本&#xff1a; $ sudo apt update && sudo apt upgrade -y升级完成后&#xff0c;重启系统 reboot重启成功之后&#xff0c;查看系统的当前版本 $ lsb_release -a最新版本应该是20.04.6&#xff0c;如下图…

JTS-Orientation方向计算

org.locationtech.jts.algorithm.Orientation 使用说明 用于计算基本几何结构(包括点三重体(三角形)和环)的方向的函数。方向是平面几何的基本属性。 Orientation.index(Coordinate p1, Coordinate p2, Coordinate q) 说明 计算q点处在p1点->p2点方向的左侧还是右侧,左侧…

9. selenium API 【万字】

目录 1. 元素的定位 1.1 css selector 1.1.1 id 选择器 1.1.2 类选择器 1.1.3 标签选择器 1.1.4 后代选择器 1.2 xpath 1.2.1 相对路径 索引 1.2.2 相对路径 元素 1.2.3 相对路径 通配符 1.2.4 相对路径 部分元素定位 1.2.5 相对路径 文本定位 1.3 应用&…

gazebo软件建立带摄像和红外功能的小车

背景&#xff1a; 为了方便调整摄像头的高度&#xff0c;我需要重新构建以下带小车的模型。小白分享来了。 目录 1. 先构建一个能跑的小车。 1.1 gazebo设计四个物体&#xff1a;车体三个车轮。组合然后构建好后在本地保存成小车模型。 1.2 打开本地文件添加plugin插件&…

转行软件测试成功的关键因素是什么?

三年前张伟是一名厨师,职高毕业&#xff0c;团队一共有5个人&#xff0c;大家各自负责自己的模块&#xff0c;整体上感觉相差不大&#xff0c;特别的团结稳定。 可是后来随着疫情的发生&#xff0c;对整个社会都产生了严重的冲击&#xff0c;饭店每天的订单是越来越少&#xf…

Spark 4:Spark Core 共享变量

广播变量 # coding:utf8 import timefrom pyspark import SparkConf, SparkContext from pyspark.storagelevel import StorageLevelif __name__ __main__:conf SparkConf().setAppName("test").setMaster("local[*]")sc SparkContext(confconf)stu_inf…