结构体的大小还能动态变化?你听过柔性数组吗?

本文介绍了C语言中处理动态数组的一种优雅方式——柔性数组成员(Flexible Array Members)。这种特性允许在结构体中定义一个大小为0的数组,以适应不同长度的数据存储需求。通过malloc()函数动态分配内存,并在结构体后面附加实际的数组大小。在释放内存时,只需像常规结构体那样使用free()。柔性数组的使用要求包括:支持C99标准,数组必须是结构体的最后一个成员,且结构体中至少还有一个其他成员。文章通过示例展示了如何创建和使用柔性数组,并提及了Linux内核中USB驱动代码中的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在刚开始写C代码的时候,我们可能会遇到这样一种情况:要存储一组同学的名字,可是每个同学的姓名长度不同,用固定大小的数组会很不方便而且会存在浪费内存的情况。这个时候我们引入了动态数组,通过malloc()函数申请内存按需分配大小。这时候问题来了,如果有一个学生的结构体需要存储姓名这样的属性我们该怎么做呢?同样的我们会在结构体中加入一个char*的指针成员.
struct Student {
    int id;
    int age;
    char sex;
    char* name;
}

这样做比较麻烦的有两点:

  1. name是一个指针,我们需要手动的创建和回收,防止内存泄漏;
  2. name在调试过程中是无法看到全貌的,只能看到第一个字节或者是通过表达式打印出整个名字.
    有没有什么比较漂亮的解决方案呢?那就是C语言从C99标准开始引入的一个"柔性数组"(Flexible Array Members)的功能,主要在结构体中增加一个可变大小的灵活数组.
    先看一下使用柔性数组的写法:
struct Student {
    int id;
    int age;
    int score;
    char name[0];
}

乍一看,好像和普通数组没有区别,但是数组的大小竟然为0,也可以写作char name[],这就是柔性数组.

柔性数组的使用

创建:如果Student结构体需要存储一个长度为len的人名,我们只需下面这样写即可:
struct Student *pStu = malloc(sizeof(Student)) + len * sizeof(char))
释放:当我们在释放内存的时候,和正常的结构体一样即可:
free(pStu);

柔性数组的使用要求

  1. 必须支持C99标准及以后的编译器;
  2. 结构体内部的数组声明必须是最后一个成员;
  3. 出了FAM数组成员,结构体中还必须包含一个其它命名成员.

柔性数组的解释

柔性数组本质上是把指针的动态数组申请的内存放在了结构体的尾部,从而使得结构体成员和动态申请的数组是串联在一起的,于是可以直接使用结构体的指针去访问这块内存.name成员只作为一个符号地址的存在指向结构体的地址,并没有占用实际的内存空间,因此sizeof(Student)的大小并不包括数组的内存大小.

柔性数组的使用案例

在linux内核中就有很多地方使用了0长数组,下面举一个内核中USB驱动代码的例子:

struct urb {
    struct kref kref;
    void *hcpriv;
    atomic_t use_count;
    atomic_t reject;
    int unlinked;
 
    struct list_head urb_list;
    struct list_head anchor_list;
    struct usb_anchor *anchor;
    struct usb_device *dev;
    struct usb_host_endpoint *ep;
    unsigned int pipe;
    unsigned int stream_id;
    int status;
    unsigned int transfer_flags;
    void *transfer_buffer;
    dma_addr_t transfer_dma;
    struct scatterlist *sg;
    int num_mapped_sgs;
    int num_sgs;
    u32 transfer_buffer_length;
    u32 actual_length;
    unsigned char *setup_packet;
    dma_addr_t setup_dma;
    int start_frame;
    int number_of_packets;
    int interval;
 
    int error_count;
    void *context;
    usb_complete_t complete;
    struct usb_iso_packet_descriptor iso_frame_desc[0];
};

usb_iso_packet_descriptor就是一个柔性数组,由于不同场景的缓冲大小是不确定的,如何能统一且更灵活,就是柔性数组.
可以用下面的代码来测试一下柔性数组

typedef struct tagStudent
{
	int id;
	int len;
	char name[];
}Student,*PtrStu;

PtrStu createStudent(int id, char name[]) 
{ 
	PtrStu pStu = (PtrStu)malloc( sizeof(Student) + sizeof(char) * (strlen(name)+1)); 
	pStu->id = id; 
	pStu->len = strlen(name); 
	strcpy(pStu->name, name);   
	return pStu; 
} 

int main(void)
{
	PtrStu pStu1 = createStudent(1001, "Tom");
	PtrStu pStu2 = createStudent(1002, "Martin");

	printf("name:%s,id:%d\n", pStu1->name, pStu1->id);
	printf("name:%s,id:%d\n", pStu2->name, pStu2->id);

	free(pStu1);
	free(pStu2);
}
<think>我们正在讨论abiencoderv2对结构体数组的编码逻辑。根据Solidity文档,abiencoderv2是默认的ABI编码器,从Solidity 0.8.0开始启用。它支持更复杂的类型,如嵌套数组和结构体。 对于结构体数组,abiencoderv2的编码规则如下: 1. 结构体数组被编码为动态数组,其编码分为两部分:头部(head)和尾部(tail)。 2. 头部:在数据部分中,数组的起始位置存储的是数组的长度(uint256),后面跟着每个元素在尾部的位置(偏移量)。注意,对于结构体数组,每个元素本身是一个结构体,而结构体是动态类型(如果包含动态类型)或静态类型。 3. 尾部:从头部中指定的偏移量开始,依次存储每个结构体元素。每个结构体元素按照其成员顺序进行编码。如果结构体包含动态类型(如字符串、动态数组等),那么结构体的编码也是分头部和尾部的。 具体步骤: - 首先,数组的编码以长度(uint256)开始,后面是每个元素的偏移量(uint256,表示该元素在尾部的位置相对于尾部开始处的偏移)。 - 然后,在尾部部分,依次放置每个结构体元素。每个结构体元素按照其成员顺序编码,如果成员是静态类型(如uint, address等),则直接存储;如果是动态类型,则存储一个指向实际数据位置的偏移量(相对于当前结构体开始位置的偏移?实际上,在ABI编码中,动态类型的数据会被放在尾部,而在结构体内部存储的是指向该动态数据在尾部位置的偏移量)。 但是,请注意:结构体数组的编码实际上与多维数组的编码类似。每个结构体元素在尾部连续存储,但结构体内部如果有动态类型,则结构体内部也会使用偏移量指向其动态成员的实际数据位置。 让我们用一个例子来说明: ```solidity pragma solidity ^0.8.0; contract Test { struct Person { string name; uint age; } function encode(Person[] memory people) public pure returns (bytes memory) { return abi.encode(people); } } ``` 假设我们编码一个包含两个Person的数组: - 第一个Person: name="Alice", age=30 - 第二个Person: name="Bob", age=25 编码过程: 1. 整个数组的编码(动态数组): - 头部: - 第一部分:数组长度,2(32字节,0x0000000000000000000000000000000000000000000000000000000000000002) - 第二部分:第一个元素的偏移量(相对于整个编码的尾部开始处的偏移),通常是0x20(因为头部有长度和两个元素的偏移量,所以第一个元素在头部之后32*3=96字节处,即0x60,但偏移量是相对于尾部的起始位置?实际上,偏移量是相对于整个编码的起始位置到该元素的位置) 注意:动态数组的编码布局: - 偏移量指向整个数组的编码开始处(即长度所在的位置)到数组元素部分开始处的偏移。实际上,数组的编码布局如下: [0]: 数组长度(32字节) [32]: 第一个元素的位置(这是一个偏移量,指向第一个元素实际开始的位置,这个位置是相对于整个编码的起始位置,即从0开始算) [64]: 第二个元素的位置(同样是指向第二个元素开始位置的偏移量) 然后,在偏移量指向的位置,开始存储第一个元素,第二个元素紧接着第一个元素存储(如果元素是静态大小)或者通过偏移量指向(如果是动态大小)。 但是,对于结构体数组,每个结构体元素的大小可能是动态的(如果结构体包含动态类型)。因此,每个元素的位置需要单独指定。 2. 对于每个结构体元素,由于结构体Person包含一个动态类型(string),所以每个结构体元素的编码也是动态的。因此,每个结构体元素的编码包括: - 头部:存储name字段的偏移量(相对于该结构体元素开始位置的偏移,由于结构体有两个字段,所以头部有两个32字节:第一个是name的偏移量,第二个是age的值。注意,name是动态类型,所以它的实际数据不在这个位置,而是需要根据偏移量去取) - 尾部:存储动态字段的实际数据(即字符串的编码:长度+字符串内容) 因此,整个数组的编码结构如下: ``` 0x0000000000000000000000000000000000000000000000000000000000000002 // 数组长度(2个元素) 0x0000000000000000000000000000000000000000000000000000000000000040 // 第一个元素的位置(从整个编码开始算的偏移量:64字节) 0x00000000000000000000000000000000000000000000000000000000000000c0 // 第二个元素的位置(192字节) // 第一个元素(从64字节开始): 0x0000000000000000000000000000000000000000000000000000000000000040 // name字段的偏移量(相对于当前结构体元素开始位置的偏移,64字节后偏移64字节,即128字节处) 0x000000000000000000000000000000000000000000000000000000000000001e // age=30 // 然后,在128字节处(即整个编码的128字节位置)存储第一个元素的name字段: 0x0000000000000000000000000000000000000000000000000000000000000005 // 字符串长度5 0x416c696365000000000000000000000000000000000000000000000000000000 // "Alice"的UTF8编码 // 第二个元素(从192字节开始): 0x0000000000000000000000000000000000000000000000000000000000000040 // name字段的偏移量(相对于当前结构体元素开始位置的偏移,192字节后偏移64字节,即256字节处) 0x0000000000000000000000000000000000000000000000000000000000000019 // age=25 // 在256字节处存储第二个元素的name字段: 0x0000000000000000000000000000000000000000000000000000000000000003 // 字符串长度3 0x426f620000000000000000000000000000000000000000000000000000000000 // "Bob"的UTF8编码 ``` 注意:上面的偏移量计算是基于整个编码的起始位置(0)的绝对偏移量。实际上,在ABI编码中,偏移量都是相对于整个编码的起始位置(即第一个字节)的。 总结:结构体数组的编码是动态数组的编码,每个结构体元素由于包含动态类型,所以每个结构体元素也是动态编码。因此,编码分为多层:数组层、结构体层、动态类型层。 引用[1]中提到了逻辑合约地址和函数名,但与我们讨论的编码逻辑无关。引用[2]提到区块链的模型,也与编码无关。因此,我们主要依据Solidity官方文档的ABI编码规范。 更多细节可参考Solidity文档:https://docs.soliditylang.org/en/latest/abi-spec.html §§相关问题§§ 1. 在Solidity中,abiencoderv2对嵌套结构体的编码逻辑是怎样的? 2. 如何处理包含动态数组的结构体数组的编码? 3. abiencoderv2与旧版编码器在结构体数组编码上的主要区别是什么?</think>### abiencoderv2 对结构体数组的编码逻辑 在 Solidity 的 ABI 编码规范中,`abiencoderv2`(即 ABI 编码器 V2)对结构体数组的编码遵循特定的分层规则,主要分为**头部区**和**尾部区**两部分: #### 1. **整体编码结构** - **头部区**:存储数组长度和每个结构体元素的偏移指针 - **尾部区**:实际存储结构体数据(含嵌套动态类型数据) - 编码公式: ``` 总编码 = 数组长度(32字节) + [偏移指针1, 偏移指针2, ...](每个32字节) + 结构体数据区 ``` #### 2. **结构体元素的编码规则** 每个结构体元素按成员顺序编码: - **静态类型成员**(如 `uint`, `address`)直接内联存储 - **动态类型成员**(如 `string`, `bytes`, 嵌套数组)存储指向尾部区的偏移量 - 结构体内部布局: ``` [成员1数据/偏移] (32字节) [成员2数据/偏移] (32字节) ... ``` #### 3. **动态类型数据存储** 动态类型数据(如结构体中的字符串)在尾部区按以下格式存储: ``` 数据长度(32字节) + 实际数据(按32字节对齐填充) ``` #### 4. 编码示例 考虑结构体和数组定义: ```solidity struct Person { string name; uint age; uint[] scores; } Person[] people; // 待编码数组 ``` 假设数组包含一个元素: ```javascript { name: "Alice", age: 30, scores: [90, 85] } ``` 编码结果(十六进制): ``` // 头部区 0000000000000000000000000000000000000000000000000000000000000001 // 数组长度=1 0000000000000000000000000000000000000000000000000000000000000020 // 元素1指针→32字节处 // 结构体数据区 (从0x20开始) 0000000000000000000000000000000000000000000000000000000000000060 // name偏移→96字节处 000000000000000000000000000000000000000000000000000000000000001e // age=30 00000000000000000000000000000000000000000000000000000000000000a0 // scores偏移→160字节处 // 尾部区 (动态数据) 0000000000000000000000000000000000000000000000000000000000000005 // name长度=5 416c696365000000000000000000000000000000000000000000000000000000 // "Alice" + 填充 0000000000000000000000000000000000000000000000000000000000000002 // scores长度=2 000000000000000000000000000000000000000000000000000000000000005a // 90 0000000000000000000000000000000000000000000000000000000000000055 // 85 ``` #### 5. 关键特性 1. **深度嵌套支持**:可处理任意深度的结构体/数组嵌套 2. **动态类型延迟绑定**:通过偏移量实现动态数据的按需加载 3. **32字节对齐**:所有数据块长度均为32字节的整数倍 4. **长度前缀**:每个动态类型数据都有明确长度标识 > 注:ABI 编码器 V2 自 Solidity 0.8.0 起默认启用,解决了旧版编码器对复杂类型的限制,特别适合处理包含动态类型成员的结构体数组[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值