C语言 动态内存管理

目录

  • 1. C/C++程序的内存分配
  • 2. 动态内存分配的作用
  • 3. malloc - 分配内存
  • 4. free - 释放内存
  • 5. calloc - 分配并清零内存
  • 6. realloc - 调整之前分配的内存块
  • 7. 常见的动态内存的错误
    • 7.1 对空指针解引用
    • 7.2 对动态开辟空间的越界访问
    • 7.3 对非动态开辟内存使用free
    • 7.4 使用free释放动态开辟内存的一部分
    • 7.5 对同一块动态内存重复释放
    • 7.6 动态开辟内存未释放
  • 8. 动态内存相关题目
    • 8.1 题目1
    • 8.2 题目2
    • 8.3 题目3
    • 8.4 题目4
  • 9. 柔性数组
    • 9.1 柔性数组的定义
    • 9.2 柔性数组的特点
    • 9.3 柔性数组的使用
    • 9.4 柔性数组的优点


正文开始

动态内存管理,顾名思义就是动态的、灵活的管理内存的分配,这在工程中有着重要的用途,下面我们来学习一下如何实现。

1. C/C++程序的内存分配

C/C++程序会分配在以下位置:

  • 栈区(stack):主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等,函数执行结束后自动释放。
  • 堆区(heap):动态内存管理的区域,由程序员自行开辟和释放。
  • 数据段 / 静态区(static):存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段:存放可执行代码、只读常量(例如字符串常量)等。

图例:
在这里插入图片描述

今天我们所学习的动态内存所分配的区域就是堆区(heap)

2. 动态内存分配的作用

通常我们开辟内存的方式有:

//在栈空间上开辟四个字节
int a = 0;//在栈空间上开辟二十个字节的连续空间
char b[20] = { 0 };

但是!上述开辟空间的两种方式有两个很致命的缺点:

  • 空间开辟的大小是固定不变
  • 数组在定义时,就已经确定了数组的长度,后期不能再次调整

在实际需求中,我们对于内存的需求往往是多变的,所以我们需要灵活的、可调整的内存申请方式,C语言中为我们引入了动态内存开辟,让开发者可以自己申请和释放空间。

3. malloc - 分配内存

作用:分配指定字节的未初始化内存;使用该函数须引用头文件stdlib.h,本文其他所学函数也同样须引用此头文件,后文不再赘述。详情戳我>>><stdlib.h>

函数原型:

void* malloc( size_t size );

在这里插入图片描述
malloc 用法:

  • 在栈空间上开辟size个字节的未被使用的空间
  • 函数返回值为void *,需要使用强制类型转换来确定类型
  • 如果开辟成功,则返回一个指向所开辟空间的指针
  • 如果开辟失败,则返回空指针NULL,所以使用 malloc 函数要检查其返回值
  • 如果参数size为0,则该函数行为未定义

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{int num = 0;scanf("%d", &num);int arr[256] = { 0 };int* p = (int*)malloc(num * sizeof(int));//动态内存申请if (p == NULL)//判断是否申请成功return 1;int i = 0;//使用动态内存for (i = 0; i < num; i++){*(p + i) = i;}for (i = 0; i < num; i++){printf("%d ", *(p + i));}return 0;
}

在这里插入图片描述

4. free - 释放内存

作用:释放之前动态内存分配的空间,防止多余的空间占用。

函数原型:

void free( void* ptr );

在这里插入图片描述
free 用法:

  • 释放动态内存空间,即之前由malloc()calloc()aligned_alloc()realloc()所分配的内存
  • 参数ptr指向动态开辟内存的起点,即上述动态内存管理函数的返回值
  • ptr所指向的内存不是动态开辟的或者不是动态开辟内存的起点,则函数行为未定义
  • 若参数为NULL,则函数啥都不干

例如,将上述代码优化一下:

#include <stdio.h>
#include <stdlib.h>int main()
{int num = 0;scanf("%d", &num);int arr[256] = { 0 };int* p = (int*)malloc(num * sizeof(int));//动态内存申请if (p == NULL)//判断是否申请成功return 1;int i = 0;//使用动态内存for (i = 0; i < num; i++){*(p + i) = i;}for (i = 0; i < num; i++){printf("%d ", *(p + i));}//释放内存free(p);//设为空指针,避免野指针的出现p = NULL;return 0;
}

5. calloc - 分配并清零内存

作用:分配内存,并将分配存储中的所有字节初始化为0

函数原型:

void* calloc( size_t num, size_t size );

在这里插入图片描述

calloc 用法:

  • calloc 函数将num个大小为size的元素开辟一块空间,并且把空间的每个字节都初始化为0
  • 若开辟成功,返回值为指向开辟空间的首地址的指针
  • 若开辟失败,返回值为空指针NULL

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{//申请空间int* p = (int*)calloc(10, sizeof(int));//判断是否申请成功if (p == NULL)return 1;*p = 2;int i = 0;for (i = 0; i < 10; i++){printf("%d ", *(p + i));}//释放内存free(p);//置为空指针,避免野指针的出现p = NULL;return 0;
}

malloc 和 calloc 对比:
在这里插入图片描述

6. realloc - 调整之前分配的内存块

作用:对动态开辟内存大小进行调整

函数原型:

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

在这里插入图片描述

realloc 用法:

  • ptr所指向的空间调整为new_size个字节的大小
  • ptr所指向的空间必须是动态开辟内存
  • 若待调整空间后面有足够大的空间,则直接在原有内存之后追加空间,原数据不发生变化
  • 若待调整空间后面没有足够大的空间,则重新在堆空间上另找一个合适大小的连续空间使用,并将原数据复制到新空间
  • 若成功,则返回指向新分配内存的指针;若失败,则返回空指针NULL

例如:

#include <stdio.h>
#include <stdlib.h>int main()
{//申请动态内存int* ptr = (int*)malloc(100);//判断是否申请成功if (ptr == NULL)return 1;//使用申请的空间//...//调整动态内存大小//1.直接使用待调整空间的地址接收返回值ptr = realloc(ptr, 200);//2.使用中间变量接收返回值int* p = (int*)realloc(ptr, 300);if (p == NULL)//判断是否调整成功return 1;ptr = p;return 0;
}

上述代码中,书写了两种接收 realloc 函数返回值的方式,我们更推荐第二种方式。第一种方式中,直接使用待调整空间的地址接收返值,若调整失败,则会返回空指针NULL,这样的话,原数据就会丢失。而第二种方式则是在确保了调整成功的情况下才将待调整空间的地址接收返回值,更为安全。

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

注:以下代码均为错误示范

7.1 对空指针解引用

void test1()
{int *p = (int*)malloc(40);//若开辟失败,则返回空指针,没有进行判断就直接解引用*p = 2;free(p);
}

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

void test2()
{int* p = (int*)malloc(20);if (p == NULL)return 1;//只能存放五个整型,所以越界访问了*(p + 5) = 3;free(p);
}

7.3 对非动态开辟内存使用free

void test3()
{int a = 0;int* p = &a;//非动态开辟内存free(p);
}

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

void test4()
{int *p = (int*)malloc(100);p++;//p不再是动态开辟内存的起点free(p);
}

7.5 对同一块动态内存重复释放

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

7.6 动态开辟内存未释放

void test()
{int *p = (int*)malloc(100);if(p == NULL)return 1;//申请完未释放//出了函数后使用者也不能再使用这一块空间//操作系统也没使用权限//造成了内存泄漏
}int main()
{test();while(1);
}

所以在使用动态内存的时候,要确保在哪个函数内申请的空间,就在哪个函数内正确释放掉,否则就会出现内存泄漏

8. 动态内存相关题目

8.1 题目1

#include <stdio.h>
#include <stdlib.h>
#include <string.h>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;
}

在主函数中调用了Test()函数,Test()函数中调用了GetMemory()函数,其中 str 作为参数传递进去,但 str 是一个指针,GetMemory()函数的参数是一个指针,所以将 str 传递进去就相当于传值调用,也就是说,GetMemory()函数并没有真正的改变 str 所指向的地址,它依旧为空指针,传进 strcpy 函数的第一个参数是一个空指针,这就导致了程序崩溃

可修改为:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>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;
}

8.2 题目2

#include <stdio.h>
#include <stdlib.h>
#include <string.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,并且在Test()中使用 str 接收,但char p[]变量在Test()中已经销毁了,使用权限已经还给操作系统了
也就是说,GetMemory()仅仅是将一个地址传递了出去,但地址所指向的内存已经没有使用权限了
那么 str 接收地址后,就变成了一个野指针,所指向的內容是不确定的

8.3 题目3

#include <stdio.h>
#include <stdlib.h>
#include <string.h>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;
}

上述代码唯一的问题就是,使用完动态内存后没有将动态内存释放

8.4 题目4

#include <stdio.h>
#include <stdlib.h>
#include <string.h>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;
}

上述代码中已经将 str 释放掉了,将使用权限还给了操作系统,后面却继续使用 str,导致非法访问内存空间

9. 柔性数组

当我们要保存相同类型的数据的时候,首先想到的肯定就是使用数组了,但是数组有着很大的限制,它并不能够根据使用者的需求来灵活的调整大小,尽管C99提供了变长数组的功能,但当他一旦确定了大小,后续的使用中同样也不可以改变
而柔性数组就可以完美地解决这个问题,下面我们一起学习一下

9.1 柔性数组的定义

C99中,结构中的最后一个元素允许是未知大小的数组,称为柔性数组成员

例如:

struct Sarr
{int i;int a[0];//或者int a[];
};

9.2 柔性数组的特点

柔性数组有以下特点:

  • 结构中的柔性数组成员前必须有一个或多个其他成员
  • 柔性数组成员的大小是未知的
  • sizeof 返回的这类结构的大小不包括柔性数组的内存

例如:

#include <stdio.h>struct Sarr
{int i;int a[0];//或者int a[];
};int main()
{printf("%zd\n", sizeof(struct Sarr));return 0;
}

运行结果:
在这里插入图片描述

9.3 柔性数组的使用

我们可以通过 malloc() 函数对柔性数组成员的结构进行动态内存分配,其中分配的内存应该大于结构的大小,以适应柔性数组的预期大小

例如:

#include <stdio.h>
#include <stdlib.h>struct Sarr
{int i;int a[0];
};int main()
{//柔性数组成员申请内存struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr) + 20 * sizeof(int));int i = 0;//柔性数组的使用p->i = 20;for (i = 0; i < 20; i++){p->a[i] = i;}for (i = 0; i < 20; i++){printf("%d ", p->a[i]);}//释放动态内存free(p);p = NULL;return 0;
}

运行结果:
在这里插入图片描述

9.4 柔性数组的优点

上述代码也能写成这样:

//代码2
#include <stdio.h>
#include <stdlib.h>struct Sarr
{int i;int* p_a;
};int main()
{//给变量p开辟结构体大小的空间struct Sarr* p = (struct Sarr*)malloc(sizeof(struct Sarr));//指定数组大小p->i = 20;//给数组开辟空间p->p_a = (struct Sarr*)malloc(p->i * sizeof(int));int i = 0;//使用数组for (i = 0; i < 20; i++){p->p_a[i] = i;}for (i = 0; i < 20; i++){printf("%d ", p->p_a[i]);}//释放动态内存free(p->p_a);p->p_a = NULL;free(p);p = NULL;return 0;
}

运行结果:
在这里插入图片描述

上述代码同样实现了柔性数组的功能,但使用柔性数组有两个好处:

  • 方便内存释放:在代码2中,我们首先对结构的内存进行了分配,然后再对结构中的成员进行了内存分配,这样当我们使用完毕后释放内存时,就需要释放两次内存;而使用柔性数组就需要释放一次,一步到位!
  • 访问速度快:连续的内存有益于提高访问速度,也有益于减少内存碎片(多块使用中的内存之间的部分)


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

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

相关文章

代码随想录算法训练营第十九天:二叉树go

代码随想录算法训练营第十九天&#xff1a;二叉树go 226.翻转二叉树 力扣题目链接(opens new window) 翻转一棵二叉树。 ​​ 这道题目背后有一个让程序员心酸的故事&#xff0c;听说 Homebrew的作者Max Howell&#xff0c;就是因为没在白板上写出翻转二叉树&#xff0c;最…

jmeter分布式集群压测

目的&#xff1a;通过多台机器同时运行 性能压测 脚本&#xff0c;模拟更好的并发压力 简单点&#xff1a;就是一个人&#xff08;控制机&#xff09;做一个项目的时候&#xff0c;压力有点大&#xff0c;会导致结果不理想&#xff0c;这时候找几个人&#xff08;执行机&#x…

Parts2Whole革新:多参照图定制人像,创新自定义肖像生成框架!

DeepVisionary 每日深度学习前沿科技推送&顶会论文分享&#xff0c;与你一起了解前沿深度学习信息&#xff01; Parts2Whole革新&#xff1a;多参照图定制人像&#xff0c;创新自定义肖像生成框架&#xff01; 引言&#xff1a;探索多条件人像生成的新篇章 在数字内容创作…

Autosar PNC网络管理配置-UserData的使用

文章目录 前言ComComSignalComIPdu CanNmSignal Mapping总结 前言 之前配置的网络管理报文中的data都由ComM管理&#xff0c;后面客户新增了需求&#xff0c;最后两个byte需要发送Wakeup Reason&#xff0c;本文记录一下相关配置的修改 Com ComSignal 之前配置的PN_TX&…

应用层协议——HTTP协议

1. 认识HTTP协议 HTTP&#xff08;Hyper Text Transfer Protocol&#xff09;协议又叫做超文本传输协议&#xff0c;是一个简单的请求-响应协议&#xff0c;HTTP通常运行在TCP之上。 超文本的意思就是超越普通的文本&#xff0c;http允许传送文字&#xff0c;图片&#xff0c…

XAMPP是什么?XAMPP好不好用?

XAMPP是一个免费且开源的软件套件&#xff0c;用于在个人计算机上轻松搭建和运行 Apache 服务器、MySQL 数据库、PHP 和 Perl&#xff0c;让用户可以在个人电脑上搭建服务器环境的平台。 XAMPP的由来是 X(表示跨平台)、Apache、MySQL、PHP 和 Perl 的首字母缩写。 它集成了这…

Autosar NvM配置-手动配置Nvblock及使用-基于ETAS软件

文章目录 前言NvDataInterfaceNvBlockNvM配置SWC配置RTE Mapping使用生成的接口操作NVM总结前言 NVM作为存储协议栈中最顶层的模块,是必须要掌握的。目前项目基本使用MCU带的Dflash模块,使用Fee模拟eeprom。在项目前期阶段,应该充分讨论需要存储的内容,包括应用数据,诊断…

《Fundamentals of Power Electronics》——隔离型CUK转换器、

以下是隔离型CUK转换器的相关知识点&#xff1a; Cuk电路的隔离型版本获得方式不同。基础非隔离型Cuk电路如下图所示。 将上图中电容C1分成两个串联的电容C1a和C1b&#xff0c;得到结果如下图所示。 在两个电容之间插入一个变压器&#xff0c;得到如下图所示电路。 变压器极性…

再议大模型微调之Zero策略

1. 引言 尽管关于使用Deepspeed的Zero策略的博客已经满天飞了&#xff0c;特别是有许多经典的结论都已经阐述了&#xff0c;今天仍然被问到说&#xff0c;如果我只有4块40G的A100&#xff0c;能否进行全量的7B的大模型微调呢&#xff1f; 正所谓“纸上得来终觉浅&#xff0c;…

C#知识|将选中的账号信息展示到控制台(小示例)

哈喽&#xff0c;你好啊&#xff0c;我是雷工&#xff01; 上篇学习了控件事件的统一关联&#xff0c; 本篇通过实例练习继续学习事件统一处理中Tag数据获取、对象的封装及泛型集合List的综合运用。 01 实现功能 在上篇的基础上实现&#xff0c;点击选中喜欢的账号&#xff0…

Day1| Java基础 | 1 面向对象特性

Day1 | Java基础 | 1 面向对象特性 基础补充版Java中的开闭原则面向对象继承实现继承this和super关键字修饰符Object类和转型子父类初始化顺序 多态一个简单应用在构造方法中调用多态方法多态与向下转型 问题回答版面向对象面向对象的三大特性是什么&#xff1f;多态特性你是怎…

Transformer详解:从放弃到入门(三)

上篇文章中我们了解了多头注意力和位置编码&#xff0c;本文我们继续了解Transformer中剩下的其他组件。 层归一化 层归一化想要解决一个问题&#xff0c;这个问题在Batch Normalization的论文中有详细的描述&#xff0c;即深层网络中内部结点在训练过程中分布的变化问题。  …

秘籍解锁 primegaming亚马逊免费游戏领取+下载安装教程秘籍解锁

秘籍解锁&#xff01;primegaming亚马逊免费游戏领取下载安装教程秘籍解锁&#xff01; 亚马逊作为几大游戏平台之一也是常常送出各种免费以供玩家们游玩&#xff0c;就在近日&#xff0c;亚马逊平台优势豪掷千金为玩家们送出了两款大作&#xff0c;分别是古墓丽影年度版与乐高…

《设计一款蓝牙热敏打印机》

主控芯片用易兆威蓝牙ic&#xff0c;通讯接口&#xff1a;蓝牙、串口、usb 安卓apk用java kotlin编写、上位机用Qt编写。

PX4二次开发快速入门(三):自定义串口驱动

文章目录 前言 前言 软件&#xff1a;PX4 1.14.0稳定版 硬件&#xff1a;纳雷NRA12&#xff0c;pixhawk4 仿照原生固件tfmini的驱动进行编写 源码地址&#xff1a; https://gitee.com/Mbot_admin/px4-1.14.0-csdn 修改 src/drivers/distance_sensor/CMakeLists.txt 添加 add…

uniapp 监听APP切换前台、后台插件 Ba-Lifecycle

监听APP切换前台、后台 Ba-Lifecycle 简介&#xff08;下载地址&#xff09; Ba-Lifecycle 是一款uniapp监听APP切换前台、后台的插件&#xff0c;简单易用。 截图展示 也可关注博客&#xff0c;实时更新最新插件&#xff1a; uniapp 常用原生插件大全 使用方法 在 script…

Python实现打砖块游戏

提供学习或者毕业设计使用&#xff0c;功能基本都有&#xff0c;不能和市场上正式游戏相提比论&#xff0c;请理性对待&#xff01; 在本文中&#xff0c;我们将使用 Pygame 和 Tkinter 创建一个简单的打砖块游戏。游戏的目标是通过控制挡板来击碎屏幕上的砖块&#xff0c;同时…

Mac虚拟机软件哪个好用 mac虚拟机parallels desktop有什么用 Mac装虚拟机的利与弊 mac装虚拟机对电脑有损害吗

随着多系统使用需求的升温&#xff0c;虚拟机的使用也变得越来越普遍。虚拟机可以用于创建各种不同的系统&#xff0c;并按照要求设定所需的系统环境。另外&#xff0c;虚拟机在Mac电脑的跨系统使用以及测试软件系统兼容性等领域应用也越来越广泛。 一、Mac系统和虚拟机的区别 …

【Pytorch】6.torch.nn.functional.conv2d的使用

阅读之前应该先了解基础的CNN网络的逻辑 conv2d的作用 是PyTorch中用于执行二维卷积操作的函数。它的作用是对输入数据进行二维卷积操作&#xff0c;通常用于图像处理和深度学习中的卷积神经网络&#xff08;CNN&#xff09;模型。 conv2d的使用 我们先查看一下官方文档 inpu…

LibTorch入坑记--续2

一、安装faiss 我的faiss&#xff0c;用的是曾经安装过的 pip install faiss-gpu1.7 当时搞得环境名称是pni 二、配置环境 三、例子代码 #include <faiss/IndexFlat.h> #include <faiss/Index.h> #include <faiss/VectorTransform.h> #include <faiss/…