【C语言】动态分配内存

embedded/2024/11/14 19:10:47/

内存的五大分区

1、堆区(heap)——由程序员分配和释放, 若程序员不释放,程序结束时一般由操作系统回收。注意它与数据结构中的堆是两回事

2、栈区(stack)——由编译器自动分配释放 ,存放函数的参数值,局部变量等。其操作方式类似于数据结构中的栈

3、静态全局区

1)未初始化静态全局区 —— 静态变量,全局变量,没有初始化的存在此区
2)初始化的静态全局区 —— 静态变量、全局变量,赋过初值的存放在此区

4、文字常量区——常量、字符串就是放在这里的。 程序结束后由系统释放

5、(程序)代码区——用于存放函数体的(二进制)代码

内存五大区

静态分配与动态分配

在数组一章中,介绍过数组的长度是预先定义好的,在整个程序中固定不变。但是在实际的编程中,往往会发生所需的内存空间取决于实际输入的数据,而无法预先确定 。为了解决上述问题,C语言提供了一些内存管理函数,这些内存管理函数可以按需要动态的分配内存空间,也可把不再使用的空间回收再次利用。而动态分配内存就是在堆区分配空间。

静态分配

  1. 在程序编译或运行过程中,按事先规定大小分配内存空间的分配方式。如:int a[10]

  2. 必须事先知道所需空间的大小。

  3. 一般以数组的形式,分配在栈区或静态全局区。

  4. 按计划分配。

动态分配

  1. 在程序运行过程中,根据需要大小自由分配所需空间。

  2. 分配在堆区,一般使用特定的函数进行分配。

  3. 堆区开辟空间,手动申请手动释放,更加灵活。

  4. 按需分配。

动态分配内存函数

#include <stdlib.h>

malloc

void *malloc(unsigned int size);

malloc的全称是memory allocation,中文叫动态内存分配,用于申请一块连续的指定大小(size)的内存块区域以void*类型返回分配的内存区域地址,当无法知道内存具体位置的时候,想要绑定真正的内存空间,就需要用到动态的分配内存,且分配的大小就是程序要求的大小。

这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。

  1. 如果开辟成功,则返回一个指向开辟好空间的指针;
  2. 如果开辟失败,则返回一个NULL指针,因此 malloc 的返回值一定要做检查;
  3. 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候使用者自己来决定(强制类型转换);
  4. 如果参数 size 为 0 ,malloc 的行为是标准是未定义的,取决于编译器。
  5. 如果多次malloc申请的内存,第1次和第2次申请的内存不一定是连续的。
//我们直接给出分配的大小是可以的
char* rec2=(char*) malloc(20);
//当然,我们一般会以如下这种形式给出分配的大小。因为不同的操作系统可能数据类型的大小不同,这样写更符合规范
//指针 = (指针类型*)malloc(数据数量 *sizeof(指针类型))
char* rec1=(char*) malloc(20*sizeof(char));

free

void free(void *ptr)

我们不能为所欲为的开辟空间,因为空间是有限的,所以应当有借有还。C语言为我们提供了free函数,专门用来做动态内存的释放和回收。

  1. ptr:开辟后使用完毕的堆区的空间的首地址

  2. 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的;

  3. 如果参数 ptr 是NULL指针,则函数什么操作都不进行。

  4. free函数只能释放堆区的空间,其他区域的空间无法使用free

  5. free释放空间必须释放malloc或者calloc或者realloc的返回值对应的空间,不能说只释放一部分。

  6. free§; 注意当free后,因为没有给p赋值,所以p还是指向原先动态申请的内存。但是内存已经不能再用了,p变成野指针了,所以一般为了防止野指针,会free完毕之后对p赋为NULL

  7. 一块动态申请的内存只能free一次,不能多次free

实例1:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main()
{// 向内存申请10个整型的空间int* p = (int*)malloc(10 * sizeof(int));if (p == NULL){// 打印错误原因printf("%s\n", strerror(errno));}else{// 正常使用空间int i = 0;for (i = 0; i < 10; i++){*(p + i) = i;}for (i = 0; i < 10; i++){printf("%d ", *(p + i));}}// 当动态申请的空间不再使用的时候应该还给操作系统free(p);// 将p置为NULL,防止野指针 p = NULL;return 0;
}

实例2:倒序输出一个字符串

#include <string.h>
#include <stdio.h>
#include <stdlib.h>
int main()
{char* src="hello,world"; char* dest=NULL;int len=strlen(src);dest=(char*)malloc(len+1);// 要为\0分配空间char* d=dest;char* s=src+len-1;// 指向最后一个字符while(len--!=0){ *(d++)=*(s--);// 注意不要丢掉*号*d ='\0';// 字符串的结尾不要忘记'\0'} printf("%s",dest);free(dest);// 使用完要释放空间,避免内存泄露dest = NULL; // 释放不等于安全,将其置为空指针的操作不可省略return 0;
}

calloc

void * calloc(size_t nmemb,size_t size);
  • size_t :无符号整型,它是在头文件中,是用typedef定义出来的
  • nmemb:要申请的空间的块数
  • size:每块的字节数

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

realloc

void* realloc(void *s,unsigned int newsize);

如果我们在使用内存的过程中需要对内存的大小进行调整怎么办呢?C语言同样为我们提供了一个函数叫 realloc

  • s:原本开辟好的空间的首地址
  • newsize:重新开辟的空间的大小
  • 返回值:新的空间的首地址

在原本申请好的堆区空间的基础上重新申请内存,新的空间大小为函数的第二个参数
如果原本申请好的空间的后面不足以增加指定的大小,系统会重新找一个足够大的位置开辟指定的空间,然后将原本空间中的数据拷贝过来,然后释放原本的空间
如果newsize比原先的内存小,则会释放原先内存的后面的存储空间,只留前面的newsize个字节

char* p1=(char*)malloc(80*sizeof(char));//申请80个字节的内存
p1=(char*)realloc(p1,100);//将内存重新开辟为100个字节,可以认为是增加了20个字节 
p1=(char*)realloc(p1,50);//将内存重新开辟为50个字节,可以认为是减少了30个字节 

常见的动态内存易错警示

1、不能对NULL指针的解引用操作

因为NULL是一个特殊的指针值,表示指针没有指向任何有效的对象或地址。对NULL指针解引用会导致程序崩溃或未定义的行为,因为程序在试图访问一个不存在的内存地址。

因此,在使用指针之前,应检查其是否为NULL,并确保指向有效的内存地址。

2、不能对动态开辟空间的越界访问

对动态内存的越界访问可能会导致程序崩溃或产生未定义的行为。

这是因为动态内存分配需要在运行时进行,并且程序员需要手动管理内存的分配和释放。如果程序员在访问动态内存时越界,就会导致访问到未分配的内存或者已经释放的内存,从而可能导致程序崩溃或出现未定义的行为。

此外,动态内存的越界访问还可能会导致数据损坏、安全漏洞等问题。因此,程序员需要注意动态内存的边界,并且避免越界访问。

3、不能对非动态内存使用free

因为非动态开辟的内存是在程序运行时从栈上分配的,而不是从堆上分配的。栈上分配的内存是由系统自动管理的,程序员无法控制其释放。因此,如果试图使用free函数来释放栈上的内存,会导致程序崩溃或不可预测的行为。所以只有动态开辟的内存才能使用free函数进行释放。

4、不能对同一块动态内存free多次

对同一块动态内存多次释放会导致程序崩溃或出现未定义的行为。因为在第一次释放后,操作系统会将该内存块标记为可用,此时这块内存空间就可以被其他变量所占用。所以再次释放时该内存块由于已经被标记为可用,所以释放操作将无法成功,从而导致程序出现异常。

此外,多次释放同一块内存还会导致内存泄漏和程序性能下降的风险。因此,程序员需要确保只释放已经分配的内存,且只释放一次。

其中需要注意的是,free释放的是free释放的是内存空间,而不是指针。free之后,指针仍然存在,指针指向也不变,而指针指向的内容要视情况而定,可能存在也可能不存在,具体还要看环境和编译器(VS2022是将其置为随机值的)。所以释放后的输出可能和原来的内容一样,也可能是乱码。但是综合考虑,为了安全起见还是不要有对同一块动态内存多次释放这种操作。

5、不能使用free释放动态开辟内存的一部分

错误示例:

#include<stdio.h>
#include<stdlib.h>
int main()
{// 使用free释放动态开辟内存的一部分int* p = (int*)malloc(40);if (p == NULL){return 0;}int i = 0;for (i = 0; i < 10; i++){*p++ = i;}// 回收空间free(p);p = NULL;return 0;
}

我们有这样一个操作 “*p++ = i;” ,当这个操作结束的时候,我们的指针p指向的空间已经不是我们动态开辟的完整空间了,不仅仅局限指向末尾,只要这里的p不再指向空间的初始位置,都会导致程序的崩溃。

6、不能忘记释放动态开辟的内存(内存泄漏)

动态分配的内存是由程序员手动分配的,而不是由系统自动管理的。如果程序员忘记释放动态分配的内存,那么这些内存将一直占据系统资源,导致内存泄漏和程序性能下降。此外,如果程序员在使用未初始化的动态分配内存时发生访问错误,会导致程序崩溃或出现不可预测的行为。因此,释放动态分配的内存是程序员的责任,必须确保释放内存以避免这些问题。

错误案例一:

char* p=(char*)malloc(100);
p="hellow world!";

案例分析:开始定义了一个指针型变量p在堆区开辟了100个字节的空间,而 p=“hellow world!” 之后,p指向了 hellow world! 的文字常量区,p指向的地址内存分区发生变化,那么p在堆区申请的100个字节的内存(的首地址)就丢了,即发生了内存泄漏。

错误案例二:

void fun()
{char* p=(char*)malloc(80);
}
int main()
{fun(); //第一次调用fun(); //第二次调用
//每调用一次则内存泄漏一次(80字节)return 0;
}

案例分析:fun函数每调用一次内存就会泄漏一次。因为fun函数中定义了一个指针型变量p在堆区开辟了80个字节的空间,而主函数调用完fun函数之后,既没释放也没返回,所以调用完之后开辟的空间就丢了,就会发生内存泄漏。

解决方案:可以设置一个函数的返回值,主调函数接收这个返回值并对其使用、处理或者释放。

两个问题

free(NULL)的问题

在C语言中free(NULL)的操作是合法的,C语言标准规定:如果free的参数是NULL,那么这个函数就什么也不做。

malloc(0)的问题

在C语言中malloc(0)的语法也是对的,而且确实也分配了内存,但是内存空间是0,这个看起来说法很奇怪,但是从操作系统的原理来解释就不奇怪了。

在内存管理中,内存中有栈和堆两个部分,栈有自己的机器指令,是一种先进后出的数据结构。而malloc分配的内存是堆内存,由于堆没有自己的机器指令,所以要由自己编写算法来管理这片内存,通常的做法是用链表在每片被分配的内存前加个表头,里面存储了被分配内存的起始地址和大小。malloc等函数返回的就是表头里的起始指针(这个地址是由一系列的算法得来的,而这些操作又是由编译器的底层为我们做的,我们并不需要关心如何操作)

动态分配内存成功之后,就会返回一个有效的指针。而对于分配0空间来说,算法会得出一个可用内存的起始地址,但可用的空间为0,而操作系统一般不知道其终止地址,一般是根据占用大小来推出终止地址的。所以对malloc(0)返回的指针进行操作就是错误的。

但需要注意,即使malloc(0)也要记得free掉,因为malloc还会额外分配内存来维护申请的空间,malloc(0)时并不是什么也不做。

四道试题

题目1

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

请问:运行 Test 函数会有什么样的结果?
答案为:程序崩溃

对于本题,很多人的注意力会集中于 “printf(str);” ,实际上这里并没有问题,它等价于 “printf(“%s\n”,str);” 。

解析代码
看到 “GetMemory(str);” ,我们在这里传递的是 str 本身的值,而不是 str 的地址,进入 GetMemory 函数以后,我们在堆上开辟了100个空间,我们将这些空间放置在 p 中,这里的 p 作为一个形参变量,在 GetMemory 函数结束以后,这个 p 就销毁了, 实际上 str 仍然是NULL,而接下来我们想要将 “hello world” copy 到 str 中去,但是 str 作为NULL,它并没有指向一个有效的空间,进行操作的时候,无法避免的进行了非法访问,虽然后边的 printf 操作没有问题,但是程序在 strcpy 操作时就已经崩溃了。

总结
(1)运行代码程序会出现崩溃现象;
(2)程序存在内存泄漏问题:

str 以值传递的形式给 p
p 是 GetMemory 函数的形参,只在函数内有效
等 GetMemory 函数返回之后,动态开辟内存尚未释放
并且无法找到,所以会造成内存泄漏

题目2

“返回栈空间地址问题”

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

请问:运行 Test 函数会有什么样的结果?
答案为:随机值(或者崩溃)

解析代码
看到 “str = GetMemory();” ,进入 GetMemory 函数的时候,p[] 这个数组是GetMemory 函数内的形参,它申请了一个空间,这个空间只在 GetMemory 函数内存在,在 GetMemory 函数结束的时候,的确将 p 的地址返回了,放置在 str 中,但是当 GetMemory 调用完成之后,p 这个数组开辟的空间返还给操作系统了,这个空间里存放的值,我们是不清楚的,接下来 “printf(str);”
打印出来的值我们不清楚,所以结果为随机值。

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

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)输出hello
(2)但是有内存泄漏

解析代码
看到 “GetMemory(&str, 100);” ,将 str的地址传入 GetMemory 函数,用二级指针p 来接收,那么 *p 指向的地址即为 str ,然后将 “hello” copy 到 str 当中,再打印出来,这些操作都没有问题,但是当我们使用完 str 以后,忘记释放动态开辟的内存,导致了内存泄漏。

题目4

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

请问:运行 Test 函数会有什么样的结果?
答案为:
(1)world
(2)非法访问内存(篡改动态内存区的内容,后果难以预测,非常危险)

解析代码
首先,我们向系统申请了100个字节,地址存放在 str 中;然后,我们把 “hello” copy 到 str 当中去;接下来,我们释放了这块空间,之后, str 指向的这块空间已经还给操作系统了;然后,进行判断: str 是否为空指针,虽然之前我们对申请的动态内存进行了释放,但是 str 的值并没有改变,仍然是 “hello”,所以它不为空指针;进入if语句后,将 “world” copy 到 str 当中,world 就把 hello 给覆盖了;所以打印 str 以后结果为 world。

虽然打印了world,但是这个程序依然出了问题,对于 “free(str);” 操作:已经把空间释放掉了,这表明这块空间已经不属于我们了,我们已经不能再使用这块空间了,但是接下来我们还将 world 放进去,并且打印,这就属于非法访问内存了。

参考博文:

https://blog.csdn.net/m0_73759312/article/details/128763422

https://blog.csdn.net/qq_61672347/article/details/125904571

https://blog.csdn.net/WZRbeliever/article/details/121461425


http://www.ppmy.cn/embedded/37993.html

相关文章

VM 安装Ubuntu20

1、VM 新建虚拟机 类型配置 - 典型 安装源选择 &#xff08;安装包获取&#xff1a;Ubuntu桌面系统 | Ubuntu&#xff09; 设置计算机名与用户账号密码 为虚拟机命一个名&#xff0c;设置虚拟机文件保存的位置 设置磁盘相关信息 最后一步&#xff0c;确定虚拟机的相关参数 设置…

如何安全的使用密码登录账号(在不知道密码的情况下)

首先&#xff0c;需要用到的这个工具&#xff1a; 度娘网盘 提取码&#xff1a;qwu2 蓝奏云 提取码&#xff1a;2r1z 1、打开工具&#xff0c;进入账号密码模块&#xff0c;如图 2、看到鼠标移动到密码那一栏有提示&#xff0c;按住Ctrl或者Alt点击或者双击就能复制内容&…

如何绘制厂区地图?厂区地图路线规划图怎么做的?

随着工业化的快速发展&#xff0c;工厂规模越来越大&#xff0c;厂内货车往往因路线不明兜转&#xff0c;造成物流效率低&#xff0c;甚至路线拥堵&#xff1b;其他也存在基于安全管理的人员定位&#xff0c;访客指引&#xff0c;厂区设备可视化管理等需求。这些需求都与空间位…

Stable Diffusion:AI绘画的新纪元

摘要&#xff1a; Stable Diffusion&#xff08;SD&#xff09;作为AI绘画领域的新星&#xff0c;以其开源免费、强大的生成能力和高度的自定义性&#xff0c;正在引领一场艺术与技术的革命。本文旨在为读者提供Stable Diffusion的全面介绍&#xff0c;包括其原理、核心组件、安…

模型剪枝——RETHINKING THE VALUE OF NETWORK PRUNING

1.概述 神经网络的过度参数化是众所周知的,导致在推理时计算成本高,内存占用大。作为解决办法,网络剪枝被认为是提高有限计算预算应用中深度网络效率的有效技术。典型的剪枝算法包括三个阶段:训练(一个大型模型)、剪枝和微调。 普遍信念的挑战: 大模型训练的必要性:普遍…

leetCode72. 编辑距离

leetCode72. 编辑距离 基本思路&#xff1a; 代码 class Solution { public:int minDistance(string a, string b) {// a,b的0不做表示&#xff0c;所以从1开始&#xff0c;dp状态表示&#xff0c;这种办法会很方便a a, b b;int n a.size();int m b.size(); // 定…

深入探索 Vue 中的 createVNode 与 resolveComponent

在 Vue 开发中&#xff0c;createVNode和resolveComponent是两个至关重要的工具&#xff0c;它们为我们提供了强大的能力来灵活地创建和操控组件。 一、首先&#xff0c;让我们深入了解一下createVNode。 这是一个用于创建虚拟节点的关键函数&#xff0c;通过它&#xff0c;我…

Pytorch学习笔记——卷积操作

一、认识卷积操作 卷积操作是一种数学运算&#xff0c;它涉及两个函数&#xff1a;输入函数&#xff08;通常是图像&#xff09;和卷积核&#xff08;也称为滤波器或特征检测器&#xff09;。卷积核在输入函数上滑动&#xff0c;将核中的每个元素与其覆盖的输入函数区域中的对应…