C语言柔性数组详解
Contents
起因
写 C 程序迟早遇到“定长头 + 变长体”的数据:通信报文(包头+变长 payload)、动态字符串、对象池里带附属缓冲的节点。C89 时代大家用一个 hack 解决,C99 把它扶正——柔性数组(flexible array member)。这篇把它是什么、为什么这么设计、用的时候注意什么讲清楚。
需求
- 结构体能表达“头部 + N 个同类型元素”,一次 malloc 分配到位
- 访问方式像普通数组成员,sizeof 行为可预期
技术实现原理
标准示例
C99及以上标准支持,标准示例如下:
|
|
从 a[1] hack 说起:柔性数组解决的是同一个老问题
C89 里的惯用写法是把最后一个成员声明成 int a[1];,然后 malloc(sizeof(type) + (n-1)*sizeof(int))。这个 hack 能跑,但有先天毛病:sizeof 把那 1 个元素算进结构体,n 的计算要减 1,很容易差一;而且 a[1] 在语法上是“真数组”,被嵌套、被拷贝时编译器不会拦你。GCC 为此开了 a[0] 的扩展。C99(C99 specification 6.7.2.1.16)把它正式化:结构体最后一个成员可以是不完整数组类型 int a[];,这就是柔性数组,规则明确、可移植。
为什么 sizeof(*p) 仍然只是 4
初始大小为sizeof(i),0个元素的数组没有占用空间,而后我们可以进行变长操作了。
通过如下表达式给结构体分配内存:
|
|
但是这时候我们再用 sizeof(*p) 测试结构体的大小,发现仍然为 4。因为在定义这个结构体的时候,模子的大小就已经确定不包含柔性数组的内存大小。
柔性数组只是编外人员,不占结构体的编制。 只是说在使用柔性数组时需要把它当作结构体的一个成员,仅此而已。 再说白点,柔性数组其实与结构体没什么关系,只是“挂羊头卖狗肉”而已,算不得结构体的正式成员。
从内存布局看更直白:malloc 返回的一整块里,前 sizeof(type_a) 字节是“编制内”成员,后面紧跟的就是柔性数组的地盘。p->a[i] 被编译成“从 p 出发偏移 offsetof(type_a,a)、再加 i*sizeof(int)”——与访问普通成员是同一套寻址,柔性数组没有引入任何新的布局规则,它只是给了“结构体后面紧跟一串元素”一个合法的成员访问语法。
对比指针成员:为什么推荐柔性数组
变长数据也可以写成 int *a; 再单独 malloc 一块。两者对比:
| 维度 | 柔性数组 int a[] |
指针成员 int *a |
|---|---|---|
| 分配次数 | 1 次 malloc | 2 次(结构体、数组各一次) |
| 释放 | 1 次 free | 2 次 free |
| 额外开销 | 无 | 指针本身 4/8 字节 |
| 内存局部性 | 头和数组连续,cache 友好 | 两块内存,可能相距很远 |
| 重新扩容 | realloc 整块 | 只 realloc 数组那块 |
规则边界(C99 怎么规定的)
- 柔性数组必须是结构体的最后一个成员;
- 结构体里必须还有至少一个其他具名成员(只有
int a[];一个成员的结构体不合法); - 含柔性数组的结构体不能给柔性数组初始化、不能作为数组元素类型、不能按值拷贝/赋值——编译器只认 sizeof 覆盖的那一段。
详细介绍请搜索:
c99(C99 specification 6.7.2.1.16)中引入的 flexible array member
踩坑记录与注意事项
- sizeof 不含柔性数组,拷贝同样不含:按值赋值、传参、memcpy 都只复制 sizeof 那一段,变长数据要用
sizeof(type_a)+n*sizeof(int)显式带上——“结构体赋值丢数据”的根因就在这; - 只 malloc(sizeof(type_a)) 就写 a[i] 是越界:柔性数组的地盘还没分配,越界写直接踩坏堆元数据;
- realloc 扩容后要回写指针:
p = realloc(p, ...)后旧指针作废,其它别名继续用就是未定义行为; - 有尾部 padding 的结构体要留心:比如
int i; char c; double a[];时 sizeof 含对齐填充,直接用sizeof + n*sizeof(elem)是保守安全的做法,自己拿 offsetof 算偏移时容易漏; - 二进制序列化别直接 fwrite(p):只会写出头部,payload 长度要单独维护并补写;
- C++ 标准里没有柔性数组:g++ 一般当扩展接受,写跨 C/C++ 的公共头文件时要么
#ifdef __cplusplus分路,要么改用指针成员方案。
Author 软件开发大郭
LastMod 2022-01-28