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://www.hqwc.cn/news/674868.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

软件设计师-应用技术-UML建模题3

基础知识及技巧&#xff1a; 1. 用例图&#xff1a; 1.1 考点&#xff1a; 题干里面有关项目的详细描述&#xff0c;完整用例图中的某些参与者和某些用来扣掉&#xff0c;根据题干内容和已有用例图补充。根据题干&#xff0c;分析用例图之间的关系。 1.2 基础知识&#xff…

力扣:62. 不同路径

62. 不同路径 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在下图中标记为 “Finish” &#xff09;。 问总共有多少条不同的路径&…

仓库管理员如何入门?仓库管理六大步骤教会你!

新手菜鸟入行&#xff0c;如何做好一个仓库管理员&#xff1f;仓库运营对于许多行业至关重要&#xff0c;例如制造、零售和物流。它们涉及高效、安全地接收、仓储、拣选、包装和运输货物。 跟着这6个步骤做&#xff0c;最慢一个月&#xff0c;最快一周&#xff0c;就能轻松做好…

【Unity】使用Resources.LoadAll读取文件的顺序问题

最近在做客户的一个项目&#xff0c;其中的一个模块使用到了照片&#xff0c;但是发现了一个很严重的问题。当你在使用Unity的时候&#xff0c;它竟然不按照顺序读取&#xff1f;这个机器人是不是逻辑有问题&#xff1f;如下图&#xff1a; 名字脱敏了哈。。。 照片比较多&…

【计组OS】访存过程以及存储层次化结构

苏泽 本专栏纯个人笔记作用 用于记录408 学习的笔记记录&#xff08;敲了两年码实在不习惯手写笔记了&#xff09; 如果能帮助到大家当然最好 但由于是工作后退下来备考 很多说法和想法都会结合实际开发的思想 可能不是那么的纯粹应试哈 希望大家挑选自己喜欢的口味食用…

神经网络怎么把隐含层变量融合到损失函数中?

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

STM32F10x移植FreeRTOS

一、获取FreeRTOS源码 &#xff08;1&#xff09;登录FreeRTOS官网&#xff1a;www.freertos.org&#xff0c;下载第一个压缩包 &#xff08;2&#xff09;通过GitHub网站&#xff1a;github.com/FreeRTOS/FreeRTOS下载&#xff0c;由于该网站服务器在国外&#xff0c;所以访问…

2-5 任务:打印九九表

本次实战的目标是通过编写程序实现打印九九乘法表、字符矩形、字符平行四边形和字符菱形等图形&#xff0c;以及解决百钱买百鸡问题和输出素数等实际问题。在实战过程中&#xff0c;我们将学习并掌握以下知识点。 双重循环的使用&#xff1a;通过双重循环实现九九乘法表的打印&…

[笔记] Win11 Microsoft Store App 离线下载

微软应用商店无法下载或下载缓慢解决方法 在一些环境下 Microsoft Store 下载速度缓慢&#xff0c;或者需要账号登录才能安装的场景&#xff0c;可以通过找到对应的离线安装包的形式进行安装。 Micorsoft Store 中的离线安装包一般后缀为 AppxBundle 和 Appx。以 Ubuntu 为例…

【three.js】23. Raycaster and Mouse Events 投射射线(碰撞检测)和鼠标事件

介绍 顾名思义&#xff0c;Raycaster 可以向特定方向投射&#xff08;或发射&#xff09;一条射线&#xff0c;并测试与它相交的对象。 您可以使用该技术来检测玩家前面是否有墙&#xff0c;测试激光枪是否击中了什么东西&#xff0c;测试当前鼠标下方是否有东西来模拟鼠标事…

Kansformer?变形金刚来自过去的新敌人

​1.前言 多层感知器(MLPs),也被称为全连接前馈神经网络,是当今深度学习模型的基础组成部分。 MLPs在机器学习中扮演着至关重要的角色,因为它们是用于近似非线性函数的默认模型,这得益于通用近似定理所保证的表达能力。然而,MLPs真的是我们能构建的最佳非线性回归器吗?尽管ML…

如何去官网下载windows10操作系统iso镜像

文章目录 一、先从微软中国官网https://www.microsoft.com/zh-cn/进去二、然后按图示一步步点进去三、点击下载工具这个工具会帮你生成windows操作系统iso文件四、下载好后一步步按图示要求成功操作一、先从微软中国官网https://www.microsoft.com/zh-cn/进去 二、然后按图示一…