C语言 - 动态内存分配

目录

内存的五大分区

动态内存分配的概述

认识动态分配内存

静态分配和动态分配

动态内存分配函数

malloc - 动态申请内存

free - 释放堆区内存

calloc - 按块动态申请内存

realloc - 动态追加申请内存

常见的动态内存易错警示

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

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

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

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

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

6、free(NULL)的问题

7、malloc(0)的问题


内存的五大分区

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

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

3、静态全局区

        1)未初始化静态全局区 —— 静态变量,全局变量,没有初始化的存在此区

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

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

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

图例如下:

动态内存分配的概述

认识动态分配内存

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

静态分配和动态分配

  • 静态分配

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

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

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

4、按计划分配。

  • 动态分配

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

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

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

4、按需分配。

动态内存分配函数

注意:malloc calloc relloc 动态申请的内存,只有在free或程序结束的时候才释放。

malloc - 动态申请内存

  • 头文件:stdlib.h
  • 函数原型: void *malloc(unsigned int size);

        size:指要开辟的空间的大小

  • 函数用法

        指针 = (指针类型*)malloc(数据数量 *sizeof(指针类型))

  • 用法示例
char* rec1=(char*) malloc(20*sizeof(char));//这种方法默认计算字节数
char* rec2=(char*) malloc(20);//这种方法也可以
  • 函数说明

在内存的动态存储区(堆区)中分配一块长度为size字节的连续区域,用来存放类型说明符指定的类型。即在堆区开辟指定长度的连续空间。其中计算字节数是为了确定到底需要分配多少空间。

  • 注意事项
  1. 在调用malloc之后,要判断是否申请内存成功。如果返回值是所开辟空间的首地址,说明申请空间成功;如果返回值是NULL,则说明申请空间失败
  2. 调用malloc之后,要及时释放,否则很有可能会引起一些很麻烦的问题
  3. 如果函数返回值为void*型,使用时要做相应的强制类型转换。因为使用malloc开辟空间时需要保存所开辟空间的首地址,但是由于不确定空间用于做什么,所以本身返回值类型为void *型 ,因此在调用函数时根据接收者的类型要对其进行强制类型转换
  4. 如果多次malloc 申请的内存,第1次和第2 次申请的内存不一定是连续的
  • 返回值

如果返回值是所开辟空间的首地址,说明申请空间成功;如果返回值是NULL,则说明申请空间失败

free - 释放堆区内存

free函数是释放内存函数

  • 头文件:stdlib.h
  • 函数原型:void free(void *ptr)
  • 函数用法free(函数名)

案例如下:

  • 函数说明

free函数释放ptr指向的内存(默认指的是堆区的内存)

  • 注意

1.free只能释放堆区的空间,即必须是malloc calloc relloc动态申请的内存。且无法部分释放空间,只能释放全部空间

2.free后,原指针还是指向原先动态申请的内存,但是内存已经不能再用了,就变成野指针了。所以一般为了防止野指针,会free完毕之后对其赋为NULL,例如:

char* rec=(char*) malloc(20*sizeof(char));//申请内存

free(rec);//释放内存
rec=NULL;//定义为空指针,防止出现野指针

3.野指针就是总是占其它指针的内存(地址)或者随机出现一个地址的指针

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

calloc - 按块动态申请内存

  • 头文件:stdlib.h
  • 函数原型:void * calloc(size_t nmemb,size_t size);

        size_t :无符号整型,它是在头文件中,是用typedef定义出来的

        nmemb:要申请的空间的块数

        size:每块的字节数

  • 函数用法

        calloc(nmemb,size);

函数用法与malloc类似,具体示例如下:

char* p2=(char*)calloc(2,10);在堆中申请了2块,每块大小为10个字节,即20个字节连续的区域
char* p2=(char*)calloc(2*sizeof(char),10);
char* p2=(char*)calloc(2,10*sizeof(char));
char* p2=(char*)calloc(2*sizeof(char),10*sizeof(char));
//以上四种方法都可行
  • 函数说明

在堆区申请nmemb块,每块的大小为size个字节的连续区域,即总nmemb*size个字节连续的区域

  • malloc和calloc 的区别

malloc 申请的内存中存放的内容是随机的。而calloc 函数申请的内存中的内容为0。所以calloc消除了野指针存在的风险,案例如下:

  • 返回值

如果返回值是所开辟空间的首地址,说明申请空间成功。如果返回值是NULL,则说明申请空间失败

realloc - 动态追加申请内存

  • 头文件:stdlib.h
  • 函数原型:void* realloc(void *s,unsigned int newsize);

        s:原本开辟好的空间的首地址

        newsize:重新开辟的空间的大小

  • 函数用法

realloc(s,newsize);

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

p1=(char*)realloc(p1,50);//将内存重新开辟为50个字节,可以认为是减少了30个字节 
  • 函数说明

        在原先 s 指向的内存基础上重新申请内存,新内存的大小为 newsize个字节。

        当newsize比原先的内存大时,如果原先内存后面有足够大的空间,就追加。如果后边的内存不够大,则relloc函数会在堆区找一个newsize个字节大小的内存申请,将原先内存中的内容拷贝过来,并释放原先的内存,最后返回新内存的地址。

        当newsize 比原先的内存小时,则会释放原先内存的后面的存储空间,只留前面的newsize个字节。

  • 返回值

如果调整成功则返回值为新申请内存的首地址。失败则返回NULL

常见的动态内存易错警示

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


案例一:

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函数之后,既没释放也没返回,所以调用完之后开辟的空间就丢了,就会发生内存泄漏

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

6、free(NULL)的问题

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

7、malloc(0)的问题

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

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

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值