动态内存管理

文章目录

1. 为什么要有动态内存分配
2. malloc和free
3. calloc和realloc
4. 常⻅的动态内存的错误
5. 动态内存经典笔试题分析
6. 柔性数组
 


1. 为什么要有动态内存分配

在C语言中,内存分为几个不同的区域,包括栈区(Stack),堆区(Heap),静态区(Static)、常量区(Constant Area)和代码区(Code Area)。

栈区(Stack):

  • 存储局部变量和函数的调用信息。
  • 自动分配和释放内存,遵循"先进后出"的原则。
  • 生命周期与函数调用相关,当函数执行完成时,其栈上的局部变量会被自动销毁。
  • 通常用于存储函数的局部变量、函数参数和返回地址等。

堆区(Heap):

  • 用于动态内存分配,程序员手动分配和释放内存。
  • 生命周期不受函数调用的限制,需要手动释放内存以防止内存泄漏。
int val = 20;//在栈空间上开辟四个字节
char arr[10] = {0};//在栈空间上开辟10个字节的连续空间

了解完栈区后,上述的代码开辟空间的方式有两个特点:

  • 空间开辟⼤⼩是固定的。
  •  数组在申明的时候,必须指定数组的⻓度,数组空间⼀旦确定了⼤⼩不能调整。

但是对于空间的需求,有时候只有在程序运行起来的时候才知道需要多少,那么数组编译时开辟的空间方式就不能满足需求了。于是,C语言引入了动态内存开辟,将这些数据放入堆区中,让程序员自己申请和释放空间,就比较灵活了。

2. malloc和free

2.1 malloc

malloc函数的原型如下:

void* malloc (size_t size);

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

  • 如果开辟成功,则返回⼀个指向开辟好空间的指针。
  • 如果开辟失败,则返回⼀个 NULL 指针,因此malloc的返回值⼀定要做检查。
  • 返回值的类型是 void* ,所以malloc函数并不知道开辟空间的类型,具体在使⽤的时候使⽤者⾃⼰来决定。
  • 如果参数 size 为0,malloc的⾏为是标准是未定义的,取决于编译器。

2.2 free

C语言提供了一个专门用来做动态内存释放和回收的函数free,函数原型如下:

void free (void* ptr);

free函数⽤来释放动态开辟的内存。

  • 如果参数 ptr 指向的空间不是动态开辟的,那free函数的⾏为是未定义的。
  • 如果参数 ptr 是NULL指针,则函数什么事都不做。

malloc和free都声明在stdlib.h头文件中。

如何做到存放十个整型数据呢?看看下面的案例:

动态内存管理_第1张图片

因为malloc函数返回一个void类型的指针,所以需要强制转换成指定的指针类型。之后需要加入判断,因为malloc开辟空间时哟而有可能失败,会返回空指针。如果不加以判断,对空指针进行使用,这种情况十分危险。接着就可以正常使用该空间,用一个for循环初始化该40个字节的内容,接着还可以使用for循环打印初始化的内容。最后就是释放申请的空间,释放后ptr还是指向这块不能使用空间,ptr成为了野指针,必须重置为空指针(NULL)。还可以这样动态开辟空间:

动态内存管理_第2张图片

此时,malloc用法一致。到判断这一环节,使用了strerror字符串内置函数。strerror可以用来打印程序异常时发生了何种错误的信息,strerror内的参数时错误码,每个程序运行完后都会返回一个数值,但是我们无法解析错误码代表的错误的信息,就可以用strerror转换错误码为一串字符。而错误码会放在errno之中,errno是一个全局变量,用来记录发生错误的标识符。strerror和errno需要分别用string.h和errno.h来声明。

3. calloc和realloc

3.1 calloc

C语言还提供了一个函数交calloc,擦了咯从函数也用来动态内存分配。原型如下:

void* calloc (size_t num, size_t size);
  • 函数的功能是为 num 个⼤⼩为 size 的元素开辟⼀块空间,并把空间的每个字节初始化为0。
  • 与函数 malloc 的区别只在于 calloc 会在返回地址之前把申请的空间的每个字节初始化为全0。

举个例⼦:

动态内存管理_第3张图片

输出的结果:

所以如果我们对申请的内存空间的内容要求初始化,那么可以很⽅便的使⽤calloc函数来完成任务。

3.2 realloc

  • realloc函数的出现让动态内存管理更加灵活。
  • 有时会我们发现过去申请的空间太⼩了,有时候我们⼜会觉得申请的空间过⼤了,那为了合理的时候内存,我们⼀定会对内存的⼤⼩做灵活的调整。那 realloc 函数就可以做到对动态开辟内存⼤⼩的调整。

函数原型如下:

void* realloc (void* ptr, size_t size);
  • ptr 是要调整的内存地址,size 调整之后新⼤⼩。
  • 返回值为调整之后的内存起始位置。 这个函数调整原内存空间⼤⼩的基础上,还会将原来内存中的数据移动到 新 的空间。
  • realloc在调整内存空间的是存在两种情况:
  1. 情况1:原有空间之后有⾜够⼤的空间
  2. 情况2:原有空间之后没有⾜够⼤的空间

动态内存管理_第4张图片

情况1
当是情况1 的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发⽣变化。
情况2
当是情况2 的时候,原有空间之后没有⾜够多的空间时,扩展的⽅法是:在堆空间上另找⼀个合适⼤⼩的连续空间来使⽤。这样函数返回的是⼀个新的内存地址。

由于上述的两种情况,realloc函数的使⽤就要注意⼀些。

动态内存管理_第5张图片

代码一错误,当发生情况2的时候,如果realloc无法分配所需的内存空间,它将返回NULL,并且原始的指针ptr将丢失,这可能导致内存泄漏。因为ptr直接指向realloc返回的新内存,如果realloc失败并返回NULL,原来的内存块将丢失,无法再次释放。所以代码2的做法更加稳妥。


4. 常见的动态内存的错误

4.1 对NULL指针的解引用操作 

动态内存管理_第6张图片

4.2 对动态开辟空间的越界访问

动态内存管理_第7张图片

4.3 对非动态开辟内存使用free释放

动态内存管理_第8张图片

4.4 使用free释放⼀块动态开辟内存的⼀部分

动态内存管理_第9张图片

4.5 对同⼀块动态内存多次释放

动态内存管理_第10张图片

4.6 动态开辟内存忘记释放(内存泄漏)

动态内存管理_第11张图片

忘记释放不再使⽤的动态开辟的空间会造成内存泄漏。
谨记:动态开辟的空间⼀定要释放,并且正确释放。


5. 动态内存经典笔试题分析

分析一下四个Test函数运行之后的结果分别是什么。

5.1题目1:

动态内存管理_第12张图片

5.2题目2:

动态内存管理_第13张图片

5.3题目3:

动态内存管理_第14张图片

5.4题目4:

动态内存管理_第15张图片

5.5 题目解析:

  1. 题目1中,函数中代码运行完,因为存放在栈区上,出函数作用域之后,会销毁其中的变量。所以其中p存放的地址给销毁,但是malloc开辟的空间因为在堆区上,不会被销毁,需要程序员自己手动管理,而操作系统分配给的内存没有指针变量指向,造成内存泄漏。
  2. 题目2中,函数GetMemory中返回了一个局部变量的地址。局部变量p是在栈上分配的,当函数执行完毕后,该内存空间将被释放,所以无法打印任何东西,。而printf函数内的参数是“const char * format, ...”,就是一个字符指针,代表打印字符的首地址,str'就是一个字符指针,这样写没有问题。所以解决方法是将p定义为静态变量或者使用动态内存分配函数分配内存。
  3. 题目3中,GetMemory函数传了一个str的地址的参数,使用二级指针接收参数,这样通过指针修改str的值,即使GetMemory函数销毁p变量,也不会影响到p变量,并且用动态内存来开辟空间,是在堆区上,不会被销毁。所以打印时是没有问题的,但是别忘了Test也是个函数,执行完后也会销毁其中的变量,str变量被销毁,但是其开辟在堆区上的内存没有释放,就会造成内存泄漏
  4. 题目4,在使用动态内存后,释放内存,这没有问题。但是释放完内存后,没有将str重置为空指针,str只想已被释放的空间,是个野指针,对野指针进行操作,是很危险的。


6. 柔性数组

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

typedef struct st_type
{
    int i;
    int a[0];//柔性数组成员
}type_a;

有些编译器会报错⽆法编译可以改成:

typedef struct st_type
{
    int i;
    int a[];//柔性数组成员
}type_a;

6.1 柔性数组的特点:

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

看看下面的例子:

动态内存管理_第16张图片

6.2 柔性数组的使⽤

动态内存管理_第17张图片

6.3 柔性数组的优势

上述的 type_a 结构也可以设计为下⾯的结构,也能完成同样的效果。

动态内存管理_第18张图片

上述的 type_a 结构也可以设计为下⾯的结构,也能完成同样的效果。

第⼀个好处是:⽅便内存释放

如果我们的代码是在⼀个给别⼈⽤的函数中,你在⾥⾯做了⼆次内存分配,并把整个结构体返回给⽤⼾。⽤⼾调⽤free可以释放结构体,但是⽤⼾并不知道这个结构体内的成员也需要free,所以你不能指望⽤⼾来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存⼀次性分配好了,并返回给⽤⼾⼀个结构体指针,⽤⼾做⼀次free就可以把所有的内存也给释放掉。

第⼆个好处是:这样有利于访问速度.

连续的内存有益于提⾼访问速度,也有益于减少内存碎⽚。(其实,我个⼈觉得也没多⾼了,反正你跑不了要⽤做偏移量的加法来寻址)


总结

动态内存管理是C语言给程序员自己手动分配内存的工具,在日后使用频率高。这是个很重要的知识点,也需要多多练习,分析代码,才能掌握。话不多说,学起来吧多多重复,百炼成钢!

创作不易,如果喜欢这篇文章的话,请留下你宝贵的三连,你的支持是我最的的动力!!!

动态内存管理_第19张图片

你可能感兴趣的:(c语言)