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