本文将 flexible array member 翻译为弹性数组(成员),将介绍弹性数组的语法,好处与代价,以及扩展聊聊关于 c 语言操作内存灵活性方面的思考。
语法
struct Foo { int a; char b[]; // 有时也写成 char b[0];};
如上面例子展示,将结构体的最后一个数据成员定义为不写长度(或长度为 0)的数组即为弹性数组。
这种写法,b 数据成员不占用大小,看如下代码:
struct Foo foo;printf("%d %d", sizeof(foo), sizeof(foo.a));
在我的 mac 电脑上使用clang-1100.0.33.12编译,打印结果为4 4,说明 foo 变量大小等于 foo 变量中 a 数据成员的大小。
使用如下方法为弹性数组分配内存会产生编译错误:
foo.b = malloc(128);
编译错误信息: error: array type 'char []' is not assignable
正确的使用方式是:
struct Foo *foo = malloc(sizeof(struct Foo) + 128);
该方式共申请了4+128字节大小的内存,该 132 字节内存是连续的,前 4 个字节分配给foo->a,后 128 字节可以通过foo->b访问。
好处与代价
一般来说,弹性数组用于元素个数在运行期动态决定的场景。你可能会说,为什么不直接用指针呢,就像下面这样:
struct Foo { int a; char *b;};
上面这种写法确实可以实现同样的功能,但是存储相同大小的数据时,两种方式存在一些区别:
第一,使用这种写法,b 指针变量本身要占用内存,注意,不管你是否为 b 指针分配内存,即使b==NULL,变量自身都需要占用内存。在 64 位系统,一个指针变量是 8 个字节,别小看这 8 个字节,在内存总量比较小的场景,或结构体变量非常多的场景还是很客观的。
第二,使用弹性数组,弹性数组的内存地址和它之前的数据成员的地址是连续的。访问时内存的空间局部性也更好些。
但是话说回来,使用弹性数组并不只有好处,它也有代价。
弹性数组的方式,由于结构体中的数据成员和后面挂着的这个数据成员是通过一个 malloc 申请的内存,这也意味:
第一,整个结构体都要分配在堆上。
第二,当需要对数组内存进行扩容时,你需要对整块内存 realloc。
弹性数组是语法糖,有威力的是 c 语言操作内存的自由度
其实,我们不使用弹性数组也可以达到弹性数组的效果,如下面代码:
struct Foo { int a;};struct Foo *foo = malloc(sizeof(struct Foo) + 128);char *b = (char *)foo + sizeof(struct Foo);
弹性数组只是一个语法糖,它在结构体最后增加一个成员变量,让我们可以使用foo->b这种方式,直接访问结构体之后的内存。事实上,你如果自己计算偏移量,也可以到达一样的效果。
这里要撇开弹性数组,聊聊闲篇。
在操作内存方面,c 语言给它的使用者提供了非常高的自由度,它自身并不标记内存中存储的是什么类型的数据,使用者可以对内存地址做任意前后偏移,通过指针类型强转,解引用,可以把内存按任意类型解析,写入,读取。当然,前提是不要发生越界,并且读写一致,逻辑符合使用者的预期。
自由度越高,就越可以在更多的场景做更多的优化。但带来的代价,则是和高级语言相比,可读性差些,也容易写出 bug。当然,这句话只对于同等水平的初中级程序员有效哈,高手可以无视。
语法补充
最后,对语法做些补充。
第一,在我的环境,如果使用char b[]这种写法,再使用sizeof(foo.b)获取 b 数据成员大小,将产生编译错误:
error: invalid application of 'sizeof' to an incomplete type 'char []'
第二,弹性数组一般作为结构体的最后一个数据成员出现,如下代码会产生编译错误:
struct Foo { char b[]; int a;};
编译错误信息: error: flexible array member 'b' with type 'char []' is not at the end of struct