【C语言】动态分配内存

2024-05-07 14:36
文章标签 分配内存 动态 语言

本文主要是介绍【C语言】动态分配内存,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

内存的五大分区

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

这篇关于【C语言】动态分配内存的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/967629

相关文章

分享几个超级实用的C语言小技巧!

在嵌入式开发中,C语言的一些小技巧可以帮我们提高开发效率,事半功倍。 C语言常常让人觉得它所能表达的东西非常有限。它不具有类似第一级函数和模式匹配这样的高级功能。但是C非常简单,并且仍然有一些非常有用的语法技巧和功能,只是没有多少人知道罢了。 一、指定的初始化 很多人都知道像这样来静态地初始化数组: int fibs[] = {1, 1, 2, 3, 5}; C99标准实际上支持一种更为直观简单

面试常问的16个C语言问题,你能答上来几个?

大家好,我是小麦。最近不少小伙伴在找工作,这里我给大家分享一下面试中经常会遇到的一些嵌入式C语言问题,你看看能答上来几个呢? 1 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题) #define SEC_YEAR  (365*24*60*60)UL 考察点: #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)懂得预处理器将为你计算常数表达式

C语言指针的正确打开方式!

点击上方“小麦大叔”,选择“置顶/星标公众号” 福利干货,第一时间送达 摘要:一直觉得C语言较其他语言最伟大的地方就是C语言中的指针,有些人认为指针很简单,而有些人认为指针很难,当然这里的对简单和难并不是等价于对指针的理解程度。为此作者在这里对C语言中的指针进行全面的总结,从底层的内存分析,彻底让读者明白指针的本质。 小编认为C指针应该和C语言中的变量放在一起,因为C指针本质上还是一个变量,

C语言还可以这么玩,你绝对想不到!

点击上方“小麦大叔”,选择“置顶/星标公众号” 福利干货,第一时间送达 大家好,我是小麦,又到了愉快的周五,有人说程序员就是艺术家,那下面我们看看用c语言能画出什么作品吧。 我们知道,在计算机中要显示颜色,一般都是用R、G、B三个0-255范围内的整数来描述。 这一点,即便你不是从事前端、客户端这些与界面交互相关的开发工作,也应该知道。 也就是说,你现在在屏幕上看到的任何一个像素点的颜色,都

难倒高手了,c语言枚举end的作用是什么?

点击上方 “小麦大叔” ,选择 “置顶/星标公众号” 福利干货,第一时间送达大家好,我是小麦。 我在知乎上看到这个问题,一开始,也有一些疑惑,后面查了一些资料,对于这个问题,简单的说一下我的看法。 枚举有多大 枚举类型到底有多大,占多少空间呢?这个要具体情况具体分析,编译器会视情况而定。 下面是我测试用的编译器版本。 gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5

springboot 2.1.1动态切换多个数据源

一.前言 springboot1.x与springboot2.x的不同版本还是有不少区别的,本文主要介绍在springboot2.1.1动态切换数据源的案例. 二.配置 1.引入依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId

vue多语言插件vue-i18n

安装vue-i18n npm install vue-i18n -S 使用 1.在main.js中引入vue-i18n import VueI18n from 'vue-i18n'Vue.use(VueI18n) 2.vue-i18n初始化 const i18n = new VueI18n({locale: 'cn', // 默认语言messages}) 3.vue-i

研究实验1_搭建一个精简的C语言开发环境(包含部分经典的前言)

综合研究:      在这部分内容中,将启示我们如何进行独立研究和深度思考(一定要注意这一点,相应的调整自己的学习思想)。同时使我们:          (1)认识到汇编语言对于深入理解其他领域知识的重要性。          (2)对前面所学习的汇编语言知识进行融会。          (3)对用研究的方法进行学习进行体验。  研究实验1_搭建一个精简的C语言开发环境:  在运行过

反射之动态加载类

源码请见共享目录CSDN/JavaSE基础/1. InvokeDemo.zip 一、目标 通过反射加载类类型的对象,然后通过类类型的对象实例化对象。 二、说明 反射:一种计算机处理方式。是程序可以访问、检测和修改它本身状态或行为的一种能力。 类类型:一种Class类型;如Class s = String.class,这里的s引用的就是String类类型的对象。 三、代码 使用反射加载

第6章 动态响应页面内容

1 什么是动态页面         之前案例虽然收到了表单提交的参数但是收到了404错误,因为确实没有页面与请求URI对应,解决这个办法就可以使用动态页面技术,就是动态生成页面内容。         动态响应页面(Dynamic Response Page)是一种动态生成的Web页面,其内容可以根据用户的请求和其他动态变化的数据进行实时生成和更新。与静态页面不同,动态响应页面具有一定的交互性和