一、零长度数组概念
众所周知, GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展.
多数情况下, 其应用在变长数组中, 其定义如下
struct Packet
{
int state;
int len;
char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持
};
首先对 0长度数组, 也叫柔性数组 做一个解释 :
用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体
用法 : 在一个结构体的最后, 申明一个长度为0的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为0的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量
对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量!
二、0长度数组的用途
我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路
定长数据缓冲区, 设置一个足够大小 MAX_LENGTH 的数据缓冲区
设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间.
我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟, 释放和访问.
2.1 定长包(开辟空间, 释放, 访问)
比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度 MAX_LENGTH 为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费.
数据结构定义
// 定长缓冲区
struct max_buffer
{
int len;
char data[MAX_LENGTH];
};
数据结构大小
考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH
由于考虑到数据的溢出, 变长数据包中的 data 数组长度一般会设置得足够长足以容纳最大的数据, 因此 max_buffer 中的 data 数组很多情况下都没有填满数据, 因此造成了浪费
数据包的构造
假如我们要发送 CURR_LENGTH = 1024 个字节, 我们如何构造这个数据包呢:
一般来说, 我们会返回一个指向缓冲区数据结构 max_buffer 的指针.
/// 开辟
if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
{
mbuffer->len = CURR_LENGTH;
memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", mbuffer->len, mbuffer->data);
}
访问
这段内存要分两部分使用
前部分 4 个字节 p->len, 作为包头(就是多出来的那部分),这个包头是用来描述紧接着包头后面的数据部分的长度,这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包,那么这个包到底有多长呢,因此,我们就必须通过一个变量来表明这个数据包的长度,这就是len的作用),
而紧接其后的内存是真正的数据部分, 通过 p->data, 最后, 进行一个 memcpy() 内存拷贝, 把要发送的数据填入到这段内存当中
释放
那么当使用完毕释放数据的空间的时候, 直接释放就可以了
/// 销毁
free(mbuffer);
mbuffer = NULL;
小结
使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间 MAX_LENGTH, 而实际使用过程中, 达到 MAX_LENGTH 长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的.
但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作
2.2 指针数据包(开辟空间, 释放, 访问)
如果你将上面的长度为 MAX_LENGTH 的定长数组换为指针, 每次使用时动态的开辟 CURR_LENGTH 大小的空间, 那么就避免造成 MAX_LENGTH - CURR_LENGTH 空间的浪费, 只浪费了一个指针域的空间.
数据包定义
struct point_buffer
{
int len;
char *data;
};
数据结构大小
考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char *)
空间分配
但是也造成了使用在分配内存时,需采用两步
// =====================
// 指针数组 占用-开辟-销毁
// =====================
/// 占用
printf("the length of struct test3:%d\n",sizeof(struct point_buffer));
/// 开辟
if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
{
pbuffer->len = CURR_LENGTH;
if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
{
memcpy(pbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", pbuffer->len, pbuffer->data);
}
}
首先,需为结构体分配一块内存空间;
其次,再为结构体中的成员变量分配内存空间.
这样两次分配的内存是不连续的, 需要分别对其进行管理. 当使用长度为的数组时, 则是采用一次分配的原则, 一次性将所需的内存全部分配给它.
释放
相反, 释放时也是一样的.
/// 销毁
free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;
小结
使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH 长度的数组, 不会造成空间的大量浪费.
但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏
2.3 变长数据缓冲区(开辟空间, 释放, 访问)
定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢?
GNU C 的0长度数组, 也叫变长数组, 柔性数组就是这样一个扩展. 对于0长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:
数据结构定义
// 0长度数组
struct zero_buffer
{
int len;
char data[0];
};
数据结构大小
这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0]; 只是个数组名, 是不占用存储空间的,
即 sizeof(struct zero_buffer) = sizeof(int)
开辟空间
那么我们使用的时候, 只需要开辟一次空间即可
/// 开辟
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
{
zbuffer->len = CURR_LENGTH;
memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", zbuffer->len, zbuffer->data);
}
释放空间
释放空间也是一样的, 一次释放即可
/// 销毁
free(zbuffer);
zbuffer = NULL;
对齐 对零字节数组的影响
struct zero_buffer
{
int xxx; //4字节
char yyy; //1字节
char data[0]; //零字节数组
}
零字节数组是不占struct空间的,它针向所有结构体内容之后,但是这个之后是哪里呢?答案是紧挨着char yyy 后面。
一般情况下char yyy就是struct的最后一项,但是当结构体没有被明确要求对齐的时候会出现填充的情况,即编译器为了对齐结构体往里面填无效的内容。
此时如下:
struct zero_buffer
{
int xxx; //4字节
char yyy; //1字节
char 填充; //char data[0];//零字节数组
char 填充;
char 填充;
}
很遗憾现在data[0]指向的是填充部位。而我们期望的是它要指向zero_buffer之后。
这样会造成如下错误:
通过sizeof()
zero_buffer* p = new char[sizeof(zero_buffer)+3];//申请三个字节紧跟着zero_buffer结构体
memset((char*)p,0,sizeof(zero_buffer)+3);//在结构体里填零,方便对比
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;
不好意思,你的空间是申请到了,但你赋值都赋到了对齐的废字节上了,内存结构:
1 //xxx
2 //yyy
1 //实际的data[0]
2 //实际的data[1]
3 //实际的data[2]
0 //你以为的data[0]
0 //你以为的data[1]
0 //你以为的data[2]
在实际工作中你会发现你的数据结构后面始终有几个字节是0,而你通过sizeof(结构体)的偏移去读你的char[0]会因为偏移错误而读到错误的数据,而这件事情发生与否取决于你的结构体是否变动:幽灵BUG。
解决之道
非常简单:对有零字节数组参与的结构体进行1字节对齐:
#pragma pack(1)
struct zero_buffer
{
int xxx;//4字节
char yyy;//1字节
char data[0];//零字节数组
}
#pragma pack()
再执行上面的操作:
zero_buffer* p = new char[sizeof(zero_buffer)+3];//申请三个字节紧跟着zero_buffer结构体
memset((char*)p,0,sizeof(zero_buffer)+3);//在结构体里填零,方便对比
p->xxx =1;
p->yyy =2;
p->data[0]=1;
p->data[1]=2;
p->data[2]=3;
内存结果:
1 //xxx
2 //yyy
1 //实际的data[0]=你认为的data[0]
2 //实际的data[1]=你认为的data[1]
3 //实际的data[2]=你认为的data[2]
四、总结
// zero_length_array.c
#include <stdio.h>
#include <stdlib.h>
#define MAX_LENGTH 1024
#define CURR_LENGTH 512
// 0长度数组
struct zero_buffer
{
int len;
char data[0];
}__attribute((packed));
// 定长数组
struct max_buffer
{
int len;
char data[MAX_LENGTH];
}__attribute((packed));
// 指针数组
struct point_buffer
{
int len;
char *data;
}__attribute((packed));
int main(void)
{
struct zero_buffer *zbuffer = NULL;
struct max_buffer *mbuffer = NULL;
struct point_buffer *pbuffer = NULL;
// =====================
// 0长度数组 占用-开辟-销毁
// =====================
/// 占用
printf("the length of struct test1:%d\n",sizeof(struct zero_buffer));
/// 开辟
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
{
zbuffer->len = CURR_LENGTH;
memcpy(zbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", zbuffer->len, zbuffer->data);
}
/// 销毁
free(zbuffer);
zbuffer = NULL;
// =====================
// 定长数组 占用-开辟-销毁
// =====================
/// 占用
printf("the length of struct test2:%d\n",sizeof(struct max_buffer));
/// 开辟
if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
{
mbuffer->len = CURR_LENGTH;
memcpy(mbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", mbuffer->len, mbuffer->data);
}
/// 销毁
free(mbuffer);
mbuffer = NULL;
// =====================
// 指针数组 占用-开辟-销毁
// =====================
/// 占用
printf("the length of struct test3:%d\n",sizeof(struct point_buffer));
/// 开辟
if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
{
pbuffer->len = CURR_LENGTH;
if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
{
memcpy(pbuffer->data, "Hello World", CURR_LENGTH);
printf("%d, %s\n", pbuffer->len, pbuffer->data);
}
}
/// 销毁
free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;
return EXIT_SUCCESS;
}
- 长度为0的数组并不占有内存空间, 而指针方式需要占用内存空间.
- 对于长度为0数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 才申请空间时需分别进行, 释放时也需分别释放.
- 对于长度为0数组的访问可采用数组方式进行
- 0长度数组其实就是灵活的运用的数组指向的是其后面的连续的内存空间
- 必须将data[0]定义在struct的末尾,结构体的末尾, 就是指向了其后面的内存数据。
GNU手册还提供了另外两个结构体来说明,更容易看懂意思:
struct f1 {
int x;
int y[];
} f1 = { 1, { 2, 3, 4 } };
struct f2 {
struct f1 f1;
int data[3];
} f2 = { { 1 }, { 5, 6, 7 } };
我把f2里面的2,3,4改成了5,6,7以示区分。如果你把数据打出来。即如下的信息:
f1.x = 1
f1.y[0] = 2
f1.y[1] = 3
f1.y[2] = 4
也就是f1.y指向的是{2,3,4}这块内存中的数据。所以我们就可以轻易的得到,f2.f1.y指向的数据也就是正好f2.data的内容了。打印出来的数据:
f2.f1.x = 1
f2.f1.y[0] = 5
f2.f1.y[1] = 6
f2.f1.y[2] = 7
标签:struct,buffer,C语言,char,LENGTH,数组,长度,data
From: https://blog.51cto.com/lang13002/8361137