64、柔性数组:结构体最后一个成员为什么可以是未知大小的数组?

这个问题,我当年刚学C语言时也困惑过。

结构体里的数组,居然可以不指定大小?编译器还认?这听起来有点反直觉。但等你真正做嵌入式开发,处理网络协议、数据包缓冲区的时候,你会发现这玩意儿简直是救星。

什么是柔性数组?

柔性数组(Flexible Array Member)是C99标准引入的特性。说白了,就是结构体的最后一个成员可以是一个没有指定长度的数组。

// 普通结构体
struct packet_fixed {
    int len;
    char data[128];  // 固定大小,浪费或不够用
};

// 柔性数组结构体
struct packet_flex {
    int len;
    char data[];     // 柔性数组,不占结构体空间
};

注意看,data[] 没有写大小。这个结构体 sizeof(struct packet_flex) 只算 int len 的大小,通常是4字节。数组本身不占空间。

核心要点:柔性数组不占用结构体的内存空间,它只是一个占位符。实际内存需要你手动分配。

为什么需要柔性数组?

我做过一个网络协议解析的项目。数据包长度是动态的,从几十字节到几千字节不等。

如果用固定数组:

  • 数组设大了,内存浪费严重。嵌入式设备内存金贵啊。
  • 数组设小了,大包直接截断,数据丢失。

用指针呢?

struct packet_ptr {
    int len;
    char *data;  // 指针,需要额外malloc
};

这也能解决问题。但指针有它的麻烦:

  • 结构体和数据内存不连续,多一次寻址。
  • 拷贝结构体时,指针指向的数据不会自动拷贝,得手动处理深拷贝。
  • 序列化/反序列化时,指针地址在网络上毫无意义。

柔性数组完美解决了这些问题。数据紧跟在结构体后面,内存连续,一次malloc搞定。

柔性数组的内存布局

我画个图你就明白了:

柔性数组内存布局对比 指针方案(内存不连续) struct data指针 实际数据(堆上) 指针指向 柔性数组方案(内存连续) struct 柔性数组数据(紧跟在后面) 一次 malloc 分配,整块内存连续

看到了吗?指针方案需要两次malloc,内存东一块西一块。柔性数组一次malloc搞定,所有数据都在一个连续内存块里。

怎么用柔性数组?

用法其实很简单,记住三步走:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 1. 定义结构体
struct packet {
    int len;
    char data[];  // 柔性数组
};

int main() {
    int payload_len = 100;
    
    // 2. 分配内存:结构体大小 + 柔性数组大小
    struct packet *pkt = malloc(sizeof(struct packet) + payload_len);
    if (!pkt) {
        perror("malloc failed");
        return -1;
    }
    
    // 3. 使用
    pkt->len = payload_len;
    memcpy(pkt->data, "Hello, Flexible Array!", 22);
    
    printf("结构体大小: %zu\n", sizeof(struct packet));
    printf("数据长度: %d\n", pkt->len);
    printf("数据内容: %s\n", pkt->data);
    
    free(pkt);
    return 0;
}

输出结果:

结构体大小: 4
数据长度: 100
数据内容: Hello, Flexible Array!

个人习惯:我一般会在结构体里加一个 capacity 字段,记录柔性数组实际分配的大小,方便做边界检查。别问我为什么,问就是吃过越界的亏。

避坑指南

柔性数组虽好,坑也不少。我一个个说:

我曾经踩过的坑:

  • 柔性数组必须在最后一个成员。 这是硬性规定。放中间?编译器直接报错。
  • 不能有多个柔性数组。 一个结构体只能有一个,而且必须是最后一个。
  • 不能直接声明结构体变量。 比如 struct packet pkt; 这样不行。因为编译器不知道数组多大,没法分配栈空间。只能用指针 + malloc。
  • sizeof 不计算柔性数组大小。 这个前面说过,但容易忘。分配内存时一定要手动加上数组大小。
  • 不要用 memcpy 拷贝整个结构体。 柔性数组的数据不会被拷贝,只会拷贝前面的固定成员。要拷贝就得自己算好总长度。

柔性数组 vs 指针:怎么选?

我整理了个对比表,方便你决策:

对比项 柔性数组 指针方案
内存连续性 连续,一次malloc 不连续,多次malloc
内存碎片
拷贝结构体 需手动计算总长度 需深拷贝,更麻烦
序列化/网络传输 直接整块发送 需拆解指针
sizeof 结果 只算固定成员 只算固定成员
适用场景 网络协议、数据缓冲区 链表、树等动态结构

我个人建议:如果你做的是数据包、消息缓冲区这类东西,无脑选柔性数组。如果是链表、树这种需要频繁增删节点的,用指针更灵活。

一个真实案例

我记得有一次做物联网网关,需要解析各种传感器上报的数据包。数据包格式是:

| 设备ID (4字节) | 数据长度 (2字节) | 传感器数据 (变长) |

用柔性数组实现:

struct sensor_packet {
    uint32_t device_id;
    uint16_t data_len;
    uint8_t  data[];  // 柔性数组
};

// 接收数据时
void on_data_received(uint8_t *buffer, size_t total_len) {
    struct sensor_packet *pkt = (struct sensor_packet *)buffer;
    // 直接访问,无需解析指针
    printf("设备ID: %u, 数据长度: %u\n", pkt->device_id, pkt->data_len);
    process_sensor_data(pkt->data, pkt->data_len);
}

你看,从网络收来的数据直接强转成结构体指针就能用。如果是指针方案,还得先解析出指针地址,再去找实际数据,多了一步不说,还容易出错。

总结一下

柔性数组不是什么黑魔法。它就是C语言给开发者提供的一个便利工具,让你能更优雅地处理变长数据。

记住三点:

  • 只能放在结构体末尾
  • 不占结构体空间,要手动分配
  • 内存连续,适合网络协议和缓冲区

嗯,就这些。下次你写协议解析代码的时候,试试柔性数组,你会爱上它的。

一句话记住:柔性数组 = 结构体尾巴上挂了个可变长的数据区,一次malloc,整块内存,省心省力。


公众号:蓝海资料掘金营,微信deep3321