本篇博客主要讲解C99中的新语法:柔性数组。

1.什么是柔性数组?

柔性数组就是大小可以变化的数组。

注意跟C99中的变长数组区分开来,变长数组指的是可以使用变量来指定大小,并且不能初始化的数组,比如:

int n = 0;scanf("%d", &n);int arr[n]; // 变长数组

柔性数组是在结构体中声明的。它满足:

  1. 是结构体的最后一个成员变量。
  2. 大小不确定,一般空出来或者用0来填充。

比如:

struct S{char ch;double d;int arr[];};

或者:

struct S{char ch;double d;int arr[0];};

上面的2中写法是等价的,但是有些编译器只支持其中的一种写法。

在进行如何使用的讲解之前,先来思考一个问题:结构体S的大小是多少?也就是说,sizeof(struct S)是多大?

不同的编译器结果不一定一样。事实上,sizeof(struct S)计算的是结构体中除了变长数组之外的大小,在VS2022,X64环境下,考虑内存对齐,计算出来的结果是16。

2.柔性数组应该如何使用?

为了给柔性数组分配空间,应该使用动态内存管理。假设使用malloc来申请空间,我们应该考虑柔性数组的大小,算出总的大小。

以上面的struct S为例。假设我想要数组arr的大小是10个int,那么要开辟的总大小就是除了柔性数组之外的大小+柔性数组的大小。前面提到了,除了柔性数组之外的大小就是sizeof(struct S),而柔性数组的大小是10*sizeof(int),就能如下开辟空间:

struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));if (ps == NULL){// ...}// ...

这样,结构体中的成员数组arr就有了10个int的空间了,我们可以正常的使用这个结构体,只需把成员数组arr当成int arr[10];这样的数组即可。

当然,之所以叫“柔性数组”,这个数组不仅可以指定初始化的大小,也可以改变大小,毕竟是动态内存开辟出来的。只需要使用realloc即可,新的大小的计算方式和前面一样,也是sizeof(struct S)+柔性数组新的大小。比如,如果我想把数组的大小扩大成原来的2倍,可以这么写:

struct S* tmp = (struct S*)realloc(ps, sizeof(struct S) + 20 * sizeof(int));if (tmp == NULL){// ...}else{ps = tmp;}// ...

这样,ps指向的结构体的最后一个成员数组的容量就变成了20个int。

记住,所有动态申请的内存,在使用结束后,都需要调用free释放。比如以上的结构体s使用完后,需要free(ps);

3.柔性数组的替代方案及内存分布对比

其实,我们可以不使用柔性数组实现类似的效果。分析一下,以上的结构体struct S的特点是:

  1. 使用动态内存管理,在堆区上申请空间。
  2. 有一个成员数组,可以动态的改变大小。

如果不使用柔性数组,而是直接使用动态内存管理的思路,也是可以的。比如:

struct S{char ch;double d;int* arr;};

先malloc出一个结构体出来。

struct S* ps = (struct S*)malloc(sizeof(struct S));if (ps == NULL){// ...}// ...

接着再malloc出一块空间,交给arr管理。假设开辟出10个int的空间。

ps->arr = (int*)malloc(10 * sizeof(int));if (ps->arr == NULL){// ...}// ...

如果想要扩容,直接对ps->arr进行realloc即可。比如,把arr指向的空间扩容成原来的2倍。

int* tmp = (int*)realloc(ps->arr, 20 * sizeof(int));if (tmp == NULL){// ...}else{ps->arr = tmp;}

这块空间使用结束后,记得释放空间。注意一定要先释放arr,再释放ps,因为如果先释放ps,结构体内的arr就成了野指针,就找不到原来arr指向的空间了,形成了内存泄漏。

free(ps->arr);ps->arr = NULL;free(ps);pf = NULL;

使用和不使用柔性数组的内存分布对比:

  1. 对于柔性数组,动态申请的内存只存放了一个完整的结构体,所有的成员变量(数组)整体上是连续的空间。
  2. 不使用柔性数组实现类似的效果,需要先动态开辟出一个结构体,再开辟出另外一块空间,用结构体的一个成员变量(一个指针)来管理。相当于malloc了2次,有2块独立的空间。

4.柔性数组有哪些优点?

既然不使用柔性数组也能实现类似的效果,为什么还要使用柔性数组呢?因为柔性数组有以下的2个优点:

  1. 使用柔性数组只用malloc一次,free一次。如果不使用柔性数组,要malloc两次,free两次。在使用上,柔性数组更方便、更简单。
  2. 由于柔性数组在整体上只开辟了一块连续的空间,根据局部性原理,缓存的命中率更高,增加了效率(虽然其实也没增加多少)。

总结

  1. 柔性数组指的是结构体的最后一个成员变量是一个大小不确定的数组,数组的大小会空出来或者给0。
  2. 柔性数组的使用需要进行动态内存管理,具体的大小计算是sizeof(结构体类型)+柔性数组大小
  3. 不使用柔性数组也可以实现类似的效果,但是柔性数组是有它独特的优势的。如果使用柔性数组,只需要malloc和free一次;如果不使用柔性数组,对于结构体需要malloc一次,结构体内的指针又要malloc一次,总共需要malloc两次,free两次。比较起来,使用柔性数组更简单、方便,而且整体是一块连续的空间,根据局部性原理,效率更高。

感谢大家的阅读!