动态内存管理、柔性数组

news2025/1/22 9:13:24

动态内存分配的由来

我们已经掌握的内存开辟的方式:

int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间
  1. 上面开辟的空间大小是固定不可变的
  2. 数组申请的大小也是确定了之后就不能改变

这里就引入了动态内存的开辟,可以根据内存的需要进行更改 

动态内存函数的介绍

malloc和free

 void* malloc (size_t size);
  1. malloc的功能是申请size个连续可用size_t类型的字节空间,并返回指向这块空间的void*类型的指针 
  2. 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
  3. 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  4.  如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

在稍微了解了malloc后还不能直接上代码,因为动态内存申请后需要进行释放,我们先学习一下free 

free

void free(void* ptr);
  1.  free函数⽤来释放动态开辟的内存。
  2. 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
  3. 如果参数 ptr 是NULL指针,则函数什么事都不做。
  4. malloc和free都声明在 stdlib.h 头⽂件中。

 看一段代码

#include<stdio.h>
#include<stdlib.h>
int main()
{
	//开辟40个字节/10个整形大小空间
	//int* p = (int *)malloc(40);
	int* p = (int*)malloc(10*sizeof(int));

	//检查空间是否开辟成功
	if (p == NULL)
	{
		perror("malloc");
		return 1;//异常退出
	}

	//打印出来
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}

    free(p);//释放p指向的动态内存
    p=NULL;//内存释放掉了但是还是指向这块空间

	return 0;
}

结果如下 

  1. 发现结果是随机值
  2. 调试查看p的地址,发现free前后p的地址并没有发生改变,p还是指向那个地址,所以释放后这里p就是野指针,在进行解引用就是非法访问了,结论就是free之后我们还需要手动将p置为空指针

对于melloc和free总结为

1.malloc为你申请一定大小的空间,成功则返回void*的指针,失败则返回空指针;

2.由于返回的是void*,所以需要强制转换一下才可以使用;

3.在使用完之后要使用free手动释放空间;

4.释放后要将旧指针置为空指针;

calloc

void* calloc (size_t num, size_t size);
  1. 函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并且把空间的每个字节初始化为0。malloc是直接申请所有字节。
  2. calloc与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0,所以如果申请内存空间要求初始化,那么使用calloc是很好的选择。

举个例子

#include <stdio.h>
#include <stdlib.h>
int main()
{
	int* p = (int*)calloc(10, 4);
	if (p == NULL)
	{
		perror("calloc");
		return 1;
	}
	for (int i = 0; i < 10; i++)
	{
		printf("%d\n", *(p + i));
	}
	free(p);
	p = NULL;
	return 0;
}

 程序结果如下

realloc

void* realloc (void* ptr, size_t size);
  1.  如果在用malloc或者calloc时,发现空间大了或者小了,为了合理分配内存会使用realloc来修改内存大小。
  2. ptr是需要调整的动态内存首元素地址,size是调整后新的大小。
  3. 返回是void*类型,会返回修改后的内存起始地址。
  4. 需要注意接受修改后的地址不能用原来的ptr接受,如果申请失败会把修改之前的数据也丢失变成NULL。需要创建一个新的指针变量
  5. realloc在调整内存空间的是存在两种情况:
    ◦ 情况1:原有空间之后有⾜够⼤的空间,直接在后面加上size个字节,返回指向原有空间起始位置的指针。
    ◦ 情况2:原有空间之后没有⾜够⼤的空间,会在内存中申请一块新的地方,将旧空间里的数据复制到新的空间,并在新空间的末尾加上size个字节,返回指向新空间起始位置的指针。(可以通过调试修改size大小对比发现区别)

 举个例子

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

int main()
{
	//开辟10个整型空间
	int* p = (int*)calloc(10, sizeof(int));
	//如果开辟失败打印错误信息
	if (p == NULL)
	{
		perror("calloc");
        return 1;
	}
	//假设我们要扩容到20个整型空间
	int* str = (int*)realloc(p, 20 * sizeof(int));
    //执行任务
	if (str != NULL)
	{
    //不为空指针才使用,不然会内存泄漏
		p = str;
		str = NULL;

	}
	free(p);
	p = NULL;
	return 0;
}

关于realloc还有一点需要补充的是如果给realloc传递的指针为NULL,那么此时realloc的功能和malloc一样。此时realloc相当于malloc。

例如:

int* p=(int* )realloc(NULL,10*sizeof(int));

 

典型的动态内存错误

对NULL指针解应用

void test()
{
    int *p = (int *)malloc(INT_MAX/4);
    *p = 20;//如果p的值是NULL,就会有问题
    free(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);//error
}

使用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);
}

动态内存经典笔试题分析

题目1

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

这段代码中的问题是在GetMemory函数中,试图分配内存给指针p,但它实际上并未改变传入的指针str的值。因为C语言中的函数参数传递是按值传递的,所以当你传递一个指针给函数时,实际上是传递了这个指针的拷贝。这意味着在GetMemory函数内部,你修改的是拷贝的指针,而不是原始的str指针

为了解决这个问题,你可以使用指针的指针。这样,你就可以在函数内部修改指针的值,从而影响到函数外部的指针。以下是修改后的代码:

#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); // 注意这里使用了 & 运算符来传递 str 的地址
    strcpy(str, "hello world");
    printf("%s\n", str);
    free(str); // 不要忘记释放内存!
}

int main() 
{
    Test();
    return 0;
}
 

题目2

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

 

GetMemory函数中,创建了一个字符数组p,并试图返回其地址。然而这个数组是在函数内部创建的,所以当函数返回时,该数组的内存将被释放,返回的指针将变得无效。这会导致未定义的行为。所以str接收到的是野指针修改就是在static char p[]="hello world";延长生命周期。

题目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);
}

题目4

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

柔性数组

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

typedef struct st_type
{
    int i;
    int a[];//或者a[0] 柔性数组成员
}type_a;
int main()
{
    printf("%d\n", sizeof(type_a));//输出的是4
    //sizeof返回的这种结构⼤⼩不包括柔性数组的内存。
    return 0;
}

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

柔性数组的使用

int main()
{
	type_a* p = (type_a*)malloc(sizeof(type_a) + sizeof(int) * 10);
//这样相当于在原来基础上加了40个字节在柔性数组上
	p->a = 10;
    //业务处理
	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}
	printf("%d \n", p->a);
    
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
	return 0;
}

 柔性数组的优势

typedef struct st_type
{
	int a;
	int * arr;
}type_a;

int main()
{
	type_a* p = (type_a*)malloc(sizeof(int));
	p->a = 10;
	p->arr = (int*)malloc(10*sizeof(int));

	for (int i = 0; i < 10; i++)
	{
		p->arr[i] = i;
	}
	printf("%d \n", p->a);
	for (int i = 0; i < 10; i++)
	{
		printf("%d ", p->arr[i]);
	}
    //释放空间
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
	return 0;
}

 第⼀个好处是:⽅便内存释放
如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。
第⼆个好处是:这样有利于访问速度
连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址

c/c++中内存函数的分配

C/C++程序内存分配的⼏个区域:
1. 栈区(stack):在执⾏函数时,函数内局部变量的存储单元都可以在栈上创建,函数执⾏结束时这些存储单元⾃动被释放。栈内存分配运算内置于处理器的指令集中,效率很⾼,但是分配的内存容量有限。栈区主要存放运⾏函数⽽分配的局部变量、函数参数、返回数据、返回地址等。
2. 堆区(heap):⼀般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。分配⽅式类似于链表。
3. 数据段(静态区)(static)存放全局变量、静态数据。程序结束后由系统释放。
4. 代码段:存放函数体(类成员函数和全局函数)的⼆进制代码。 

 

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

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

相关文章

FT2232调试记录(1)

FT2232调试记录 &#xff08;1&#xff09;获取当前连接的FTDI设备通道个数:&#xff08;2&#xff09;获取当前连接的设备通道的信息:&#xff08;3&#xff09;配置SPI的通道:&#xff08;4&#xff09;如何设置GPIO:&#xff08;5&#xff09;DEMO测试&#xff1a; #参考文档…

电路设计(15)——篮球赛24秒违例倒计时报警器的proteus仿真

1.设计要求 设计、制作一个篮球赛24秒违例倒计时报警器。要求&#xff1a; &#xff08;1&#xff09;具有倒计时功能。可完整实现从“24”秒开始依序倒计时并显示倒计时过程&#xff0c;显示时间间隔为1秒。 &#xff08;2&#xff09;具有消隐功能。当“24”秒倒计时…

网络安全的今年:量子、生成人工智能以及 LLM 和密码

尽管世界总是难以预测&#xff0c;但网络安全的几个强劲趋势表明未来几个月的发展充满希望和令人担忧。有一点是肯定的&#xff1a;2024 年将是非常重要且有趣的一年。 近年来&#xff0c;人工智能&#xff08;AI&#xff09;以令人难以置信的速度发展&#xff0c;其在网络安全…

LC 987. 二叉树的垂序遍历

987. 二叉树的垂序遍历 难度 : 困难 题目大意&#xff1a; 给你二叉树的根结点 root &#xff0c;请你设计算法计算二叉树的 垂序遍历 序列。 对位于 (row, col) 的每个结点而言&#xff0c;其左右子结点分别位于 (row 1, col - 1) 和 (row 1, col 1) 。树的根结点位于 …

C++ :内存管理 newdelete

目录 内存区域划分 C的动态内存的管理方式 new new的基本使用方法 【注意事项】 delete 【注意】 new和delete操作自定义类型 operator new 和 operator delete 【关于自定义类型new申请内存】 【原理】 【调用顺序】 【连续开辟空间问题】 malloc/free和…

在面试中如何回复擅长vue还是react

当面试官问及这个问题的时候&#xff0c;我们需要思考面试官是否是在乎你是掌握vue还是react吗&#xff1f;&#xff1f;&#xff1f; 在大前端的一个环境下&#xff0c;当前又有AI人工智能的加持辅助&#xff0c;我们是不是要去思考企业在进行前端岗位人员需求的时候&#xf…

哈夫曼树的学习以及实践

哈夫曼树 哈夫曼树的基本了解哈夫曼树的基本概念创建霍夫曼树的思路编码构建的思路代码实现创建HuffmanTree结点初始化HuffmanTree创建霍夫曼树霍夫曼树编码 哈夫曼树的基本了解 给定 n 个 权值 作为 n 个 叶子节点&#xff0c;构造一颗二叉树&#xff0c;若该树的 带权路径长…

Vue源码系列讲解——模板编译篇【二】(模板解析阶段)

目录 1. 整体流程 2. 回到源码 3. 总结 1. 整体流程 上篇文章中我们说了&#xff0c;在模板解析阶段主要做的工作是把用户在<template></template>标签内写的模板使用正则等方式解析成抽象语法树&#xff08;AST&#xff09;。而这一阶段在源码中对应解析器&…

读千脑智能笔记12_阻止人类灭绝

1. 阻止人类灭绝 1.1. 宇宙中唯一知道这些的物体&#xff0c;唯一知道宇宙存在的物体&#xff0c;是我们的大脑 1.2. 如果没有关于某个事物的知识&#xff0c;我们能说这个事物就一定存在吗&#xff1f; 1.2.1. 我们的大脑扮演着这样一个独特的角色&#xff0c;这很令人着迷…

宽带高效非对称连续J/F-1模式Doherty 功率放大器设计(2023.11 MTT)-从理论到ADS版图

宽带高效非对称连续J/F-1模式Doherty 功率放大器设计(2023.11 MTT)-从理论到ADS版图 这个文章实现的效果非常好&#xff0c;非常值得大家去阅读复现&#xff08;见前言介绍&#xff09;&#xff0c;但是我复现出现了一点困难&#xff0c;效果调不到那么好&#xff08;带宽只是…

家政小程序系统源码开发:引领智能生活新篇章

随着科技的飞速发展&#xff0c;小程序作为一种便捷的应用形态&#xff0c;已经深入到我们生活的方方面面。尤其在家庭服务领域&#xff0c;家政小程序的出现为人们带来了前所未有的便利。它不仅简化了家政服务的流程&#xff0c;提升了服务质量&#xff0c;还为家政服务行业注…

Linux--常用命令(详解)

详细目录 一、终端命令格式二、显示文件列表命令-ls2.1作用2.2格式2.3 ls常用选项2.3.1 ls -a2.3.2 ls -l(等价于 ll)2.3.2 ls -h 三、相对路径与绝对路径3.1绝对路径3.2相对路径 四、目录操作命令 -cd4.1作用4.2格式4.3案例4.3.1 cd -&#xff1a; 返回上一次所在目录4.3.2 cd…

Agile Initiative, Epic, and Story/Task

Stories, also called “user stories,” are short requirements or requests written from the perspective of an end user. stories are something the team can commit to finish within a one- or two-week sprint.Epics are large bodies of work that can be broken do…

Linux_进程间通信

管道 System V 共享内存 System V IPC 接口介绍 由于进程地址空间的存在&#xff0c;所以进程间有具有独立性&#xff0c;一个进程看不到另一个进程的数据。那么如果我们想让进程间通信&#xff0c;就必须先让它们先看到同一份资源。常见的进程间通信的方法有管道&#xff0c;…

ChatGpt报错:Your authentication token is no longer valid解决办法

今天打开ChatGpt突然提示Oops&#xff01;,Your authentication token is no longer valid.&#xff0c;之前还好好的&#xff0c;环境也没变啊&#xff0c;结果弄了好久终于解决&#xff0c;于是记录一下解决过程&#xff0c;顺便总结一下关于OpenAI各种报错的解决办法。 完整…

比较6*6范围内8个点425个结构的顺序

( A, B )---6*30*2---( 1, 0 )( 0, 1 ) 让网络的输入有6个节点&#xff0c;训练集AB各由6张二值化的图片组成&#xff0c;让A中有8个点&#xff0c;让B全是0&#xff0c;收敛误差7e-4&#xff0c;收敛199次&#xff0c;统计迭代次数平均值并排序。 假设这个6*6的结构的行和列都…

JavaWeb:SpingBoot原理 --黑马笔记

1. 配置优先级 在我们前面的课程当中&#xff0c;我们已经讲解了SpringBoot项目当中支持的三类配置文件&#xff1a; application.properties application.yml application.yaml 在SpringBoot项目当中&#xff0c;我们要想配置一个属性&#xff0c;可以通过这三种方式当中…

每日一练:LeeCode-617、合并二叉树【二叉树+DFS】

本文是力扣LeeCode-617、合并二叉树【二叉树DFS】 学习与理解过程&#xff0c;本文仅做学习之用&#xff0c;对本题感兴趣的小伙伴可以出门左拐LeeCode。 给你两棵二叉树&#xff1a; root1 和 root2 。 想象一下&#xff0c;当你将其中一棵覆盖到另一棵之上时&#xff0c;两…

力扣面试题 16.21. 交换和(哈希表)

Problem: 面试题 16.21. 交换和 文章目录 题目描述思路及解法复杂度Code 题目描述 思路及解法 1.分别求取array1与array2数组每一个元素的和&#xff08;sum1与sum2&#xff09;并同时将array2的元素存入一个set集合中&#xff1b; 2.如果sum1和sum2的和为奇数&#xff0c;则不…

深度学习基础之《深度学习介绍》

一、深度学习与机器学习的区别 1、特征提取方面 机器学习&#xff1a;人工特征提取 分类算法 深度学习&#xff1a;没有人工特征提取&#xff0c;直接将特征值传进去 &#xff08;1&#xff09;机器学习的特征工程步骤是要靠手工完成的&#xff0c;而且需要大量领域专业知识…