聊聊c语言的flexible array member

本文将flexible array member翻译为弹性数组(成员),将介绍弹性数组的语法,好处与代价,以及扩展聊聊关于c语言操作内存灵活性方面的思考。

语法

1
2
3
4
struct Foo {
int a;
char b[]; // 有时也写成 char b[0];
};

如上面例子展示,将结构体的最后一个数据成员定义为不写长度(或长度为0)的数组即为弹性数组。

这种写法,b数据成员不占用大小,看如下代码:

1
2
struct Foo foo;
printf("%d %d\n", sizeof(foo), sizeof(foo.a));

在我的mac电脑上使用clang-1100.0.33.12编译,打印结果为4 4,说明foo变量大小等于foo变量中a数据成员的大小。

使用如下方法为弹性数组分配内存会产生编译错误:

1
foo.b = malloc(128);

编译错误信息: error: array type 'char []' is not assignable

正确的使用方式是:

1
struct Foo *foo = malloc(sizeof(struct Foo) + 128);

该方式共申请了4+128字节大小的内存,该132字节内存是连续的,前4个字节分配给foo->a,后128字节可以通过foo->b访问。

好处与代价

一般来说,弹性数组用于元素个数在运行期动态决定的场景。你可能会说,为什么不直接用指针呢,就像下面这样:

1
2
3
4
struct Foo {
int a;
char *b;
};

上面这种写法确实可以实现同样的功能,但是存储相同大小的数据时,两种方式存在一些区别:

第一,使用这种写法,b指针变量本身要占用内存,注意,不管你是否为b指针分配内存,即使b==NULL,变量自身都需要占用内存。在64位系统,一个指针变量是8个字节,别小看这8个字节,在内存总量比较小的场景,或结构体变量非常多的场景还是很客观的。

第二,使用弹性数组,弹性数组的内存地址和它之前的数据成员的地址是连续的。访问时内存的空间局部性也更好些。

但是话说回来,使用弹性数组并不只有好处,它也有代价。

弹性数组的方式,由于结构体中的数据成员和后面挂着的这个数据成员是通过一个malloc申请的内存,这也意味:

第一,整个结构体都要分配在堆上。

第二,当需要对数组内存进行扩容时,你需要对整块内存realloc。

弹性数组是语法糖,有威力的是c语言操作内存的自由度

其实,我们不使用弹性数组也可以达到弹性数组的效果,如下面代码:

1
2
3
4
5
6
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 []'

第二,弹性数组一般作为结构体的最后一个数据成员出现,如下代码会产生编译错误:

1
2
3
4
struct Foo {
char b[];
int a;
};

编译错误信息: error: flexible array member 'b' with type 'char []' is not at the end of struct

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/20013/

0%