动态内存管理——详细解读柔性数组
- 导读
- 一、什么是柔性数组
- 二、柔性数组的特点
- 三、柔性数组的使用
- 四、柔性数组的优势
- 结语
导读
大家好,很高兴又和大家见面啦!!!
在上一篇内容中我们介绍了C/C++程序中的内存分区,在C/C++程序中内存从高地址到低地址的分区依次为:
- 内核空间:存储内核代码,内核代码不能进行读/写操作
- 栈区:存储函数体内创建的对象,如变量、函数返回值、数组、指针等,空间从高地址向低地址增长;
- 内存映射段:存储文件映射、动态库、匿名映射等数据
- 堆区:由动态函数进行管理的空间,空间从低地址向高地址增长
- 数据段:存储全局数据、静态数据
- 代码段:存储可执行代码、只读常量等数据
正因为动态函数管理的内存空间与指针所在的空间不是同一个空间,因此 free
函数不能够释放与指针所在空间相同的内存空间,即指针指向的栈区空间,如数组空间。
malloc
函数在进行空间申请时,会执行三步操作:
- 在堆区空间中查找空间
- 判断空间是否可用
- 判断空间大小是否为指定大小
因此free函数在进行空间释放时,在确认该空间是否为有效空间时,同样是进行的这三部判断:
- 空间地址是否在堆区
- 空间是否被使用
- 空间大小是否为指定大小
而我们在使用动态函数时,如果不注意对应函数的使用规则,就会导致一系列的问题:
- 空间申请失败时,对空指针进行解引用操作
- 解决方案:在完成空间申请后,及时对接收返回值的指针进行判空操作
- 使用空间时,对已开辟好的空间进行越界访问
- 解决方案:对开辟好的空间进行访问时,边界不能够超过申请的空间大小
- 使用空间后,未及时释放堆区空间导致内存泄漏
- 解决方案:
- 进行扩容时,通过临时指针接收扩容后的空间地址,并对其进行及时的判空操作
- 函数返回时,检查不再使用的堆区空间是否被释放
- 解决方案:
- 释放空间时,指针指向的空间不是堆区的空间
- 解决方案:在进行空间释放前,检查指针名是否有误,指针指向的空间是否为堆区空间
- 释放空间时,指针指向的地址不是该空间的起始地址
- 解决方案:在进行空间访问时,不要改变指向空间起始地址的指针
- 释放空间时,对同一块空间进行多次释放
- 解决方案:
- 空间申请时,确保
calloc
/malloc
与free
是一一对应的关系: - 空间扩容后,及时的改变原指针的指向,确保指针指向的是扩容好的内存空间地址
- 空间申请时,确保
- 解决方案:
为了避免这些问题的出现,所以我们需要在使用动态函数前,先了解一个各个函数的用法,避免对函数使用不当而导致出现上述错误。
经过前面的内容学习,形象大家都对动态内存管理有了一个基础的认知,接下来我们就看来看一下动态内存管理的一种实际的应用——柔性数组。
一、什么是柔性数组
柔性数组(flexible array
),相信大部分朋友都和我一样,在这之前完全没有听说过,甚至连使用都很少,但是,它是确实存在的一个概念。
C99 中,结构中的最后一个元素允许是未知大小的数组,这就叫做【柔性数组】成员。
typedef struct SoftArr {int* pa;//整型指针int pb[0];//柔性数组
}SfA;
上面展示的就是一种柔性数组的例子,对于有些编译器而言,上述的这种编译方式是编译器会报错,因此我们也可以将其改写为:
typedef struct SoftArr {int* pa;//整型指针int pb[];//柔性数组
}SfA;
柔性数组只存在于结构体中,但是它本身的数组大小为0,即不占内存空间,如下所示:
柔性数组的具体大小会在在结构体变量创建时,通过malloc
来进行分配,因此,我们可以将其看做是一个大小可以改变的数组。
二、柔性数组的特点
柔性数组存在以下特点:
- 结构中的柔性数组成员前面必须至少一个其他成员
sizeof
返回的这种结构大小不包括柔性数组的内存- 包含柔性数组成员的结构用
malloc()
函数进行内存的动态分配,并且分配的内存应该大于结构的大
小,以适应柔性数组的预期大小
我们应该如何来理解这些特点呢?
首先我们要知道,结构体是一些值的集合,这些值可以是不同的数据类型,它们被称为结构体成员。
在包含柔性数组的结构体中,成员数量至少是有2个,如上例所示的整型指针与柔性数组,而且柔性数组的位置一定是结构体中的最后一个成员。
结构体在进行内存分配时,根据内存对齐的规则,每个成员都会分配到特定的位置,但是柔性数组的大小未知的,如果我们将放在了其它成员的中间,那么位于柔性数组之后的成员要被分配的位置就不可预测了,因此我们需要将大小确定的成员优先放入内存中,这样柔性数组的大小就可以随着实际的情况来进行确定。
其次正因为柔性数组的大小是不确定的,我们就可以认为柔性数组的初始大小为0,这样当我们通过sizeof
来计算结构体大小时,会将柔性数组的大小认定为0,即sizeof计算的只是柔性数组之前的所有成员组成的结构体的总大小,并不包括柔性数组的大小;
最后包含柔性数组的结构体,在创建变量时,只能够创建对应类型的指针变量,该指针指向的空间,是可以改变的堆区空间,即通过malloc
、calloc
或者realloc
申请的空间。
对柔性数组的特点有了一个初步的理解后,接下来我们就来看一下我们应该如何使用柔性数组;
三、柔性数组的使用
使用柔性数组时,实际上就是创建一个该结构类型的指针变量,并在堆区申请空间,如下所示:
typedef struct SoftArr {int* pa;//整型指针int pb[0];//柔性数组
}SfA;//柔性数组
void test1() {printf("sizeof(SfA) = %d\n", sizeof(SfA));SfA* p = (SfA*)calloc(sizeof(SfA) + 5 * sizeof(int), 1);assert(p);printf("p->pa = %p\n", p->pa);for (int i = 0; i < 5; i++) {printf("p->pb[%d] = %d\n", i, p->pb[i]);}free(p);
}
这里我们要注意的是我们在申请空间时,sizeof(SfA)
表示的是柔性数组之前的成员需要分配的空间大小,之后的5 * sizeof(int)
才是为柔性数组分配的空间大小。因此我们在申请空间时,是按字节进行申请的,而不是元素所占空间的大小,所以,使用malloc
的话会更加容易理解一点,如果和我一样使用calloc
,那就需要注意第二个参数的大小为1。
下面我们就来运行一下,如下所示:
可以看到,此时我们成功的为该结构体申请了空间,并对结构体中的成员进行了访问。当然,我们还可以通过realloc
来为柔性数组进行扩容,如下所示:
相信大家应该知道如何来使用柔性数组了。
四、柔性数组的优势
在上例中,我创建的结构体是由整型指针与柔性数组组成的集合,之所以选择这两个作为结构体成员,是因为它们都能够通过动态函数来申请空间,如下所示:
可以看到,指针成本变量与柔性数组是可以完成同样的工作的,而且指针变量还没有一定是结构体最后一个成员的限制,这样看起来似乎比柔性数组方便多了,为什么我们不直接采用指针变量的方式来实现呢?
实际上我们从最后的空间释放就可以到,如果采用指针变量的话,在进行空间释放时,我们需要进行两次空间释放,一次是指针变量成员指向的内存空间,一个只是结构体指针变量指向的内存空间,且它们释放的顺序还不能改变。因此就空间释放这一点来说,采用柔性数组会比采用指针变量要方便很多。
当我们通过柔性数组实现时,我们像内存空间申请的是一块连续的空间,而通过指针变量实现时,我们则是像内存空间申请了两块空间——结构体指针变量指向的内存空间与成员指针变量指向的内存空间。
在前面的内容中我们有介绍过,对于结构体而言,其成员在内存空间中都是会对齐到对应的边界上,因此我们要找到结构体中的每一个成员时,就需要根据成员的偏移量进行查找。
- 当结构体中的最后一个成员是柔性数组时,我们只需要找到了柔性数组的起始地址,就可以开始正常的访问数组中的元素;
- 而当结构体中的最后一个成员是指针变量时,我们需要先找到该成员的地址,再由该成员的空间中存储的地址找到其所指向的内存空间,才能访问空间内的元素。
就访问速度上来看,使用柔性数组的内存访问速度要优于使用指针变量的内存访问速度。因此当我们要在结构体中存储一些同类型的值时,我们使用柔性数组会优于指针变量。
其实这里的问题就是在比较结构体中的数组与指针,给大家推荐一篇文章:【C语言结构体里的成员数组和指针】。这篇文章是由陈皓大佬写的,文章内容深度解析了结构体中的数组和指针的问题。
结语
今天的内容到这里就全部结束了,如果大家喜欢博主的内容,可以点赞、收藏加评论支持一下博主,当然也可以将博主的内容转发给你身边需要的朋友。
【C语言必学知识点】这一专栏的全部内容也在今天就全部完结了,这个专栏的内容主要是以C语言中我们需要学习的基础语法与基础知识体系为主,是各位和我一样的初学者必须要学习的知识点。
之后,博主将会在【C语言加油站】专栏中继续为大家带来C语言中的相关知识解读,感兴趣的朋友可以关注以下该专栏。
对于计算机的学习,只了解计算机语言的基础语法是完全不够用的,因此后续我也会在学习408的过程中同步开设相应的博客专栏来分享我对已学知识点的记录以及我对这些知识点的个人见解。大家有和我一样要开始学习408的朋友,或者正在学习408的朋友可以关注一下对应的专栏哦!
最后感谢各位朋友的支持,咱们下一篇再见!!!