众所周知, GNU/GCC 在标准的 C/C++ 基础上做了有实用性的扩展, 零长度数组(Arrays of Length Zero) 就是其中一个知名的扩展.
多数情况下, 其应用在变长数组中, 其定义如下
struct Packet
{
int state;
int len;
char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持
};
首先对 0长度数组, 也叫柔性数组 做一个解释 :
我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个len字段和data字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路
我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟, 释放和访问.
比如我要发送 1024 字节的数据, 如果用定长包, 假设定长包的长度 MAX_LENGTH 为 2048, 就会浪费 1024 个字节的空间, 也会造成不必要的流量浪费.
1、数据结构定义
// 定长缓冲区
struct max_buffer
{
int len;
char data[MAX_LENGTH];
};
2、数据结构大小
sizeof(int) + sizeof(char) * MAX_LENGTH
3、数据包的构造
假如我们要发送 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、访问
这段内存要分两部分使用
5、释放
那么当使用完毕释放数据的空间的时候, 直接释放就可以了
/// 销毁
free(mbuffer);
mbuffer = NULL;
6、小结
如果你将上面的长度为 MAX_LENGTH 的定长数组换为指针, 每次使用时动态的开辟 CURR_LENGTH 大小的空间, 那么就避免造成 MAX_LENGTH - CURR_LENGTH 空间的浪费, 只浪费了一个指针域的空间.
1、数据包定义
struct point_buffer
{
int len;
char *data;
};
2、数据结构大小
考虑对齐, 那么数据结构的大小 >= sizeof(int) + sizeof(char *)
3、空间分配
但是也造成了使用在分配内存时,需采用两步
// =====================
// 指针数组 占用-开辟-销毁
// =====================
/// 占用
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);
}
}
这样两次分配的内存是不连续的, 需要分别对其进行管理.
4、释放
释放时也是一样的.
/// 销毁
free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;
5、总结
使用指针结果作为缓冲区, 只多使用了一个指针大小的空间, 无需使用 MAX_LENGTH
长度的数组, 不会造成空间的大量浪费.
但那是开辟空间时, 需要额外开辟数据域的空间, 施放时候也需要显示释放数据域的空间, 但是实际使用过程中, 往往在函数中开辟空间, 然后返回给使用者指向 struct point_buffer
的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏
定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢?
GNU C
的0长度数组, 也叫变长数组, 柔性数组就是这样一个扩展. 对于0长数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等:
1、数据结构定义
// 0长度数组
struct zero_buffer
{
int len;
char data[0];
};
2、数据结构大小
这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量, 因为char data[0]
; 只是个数组名, 是不占用存储空间的,
即 sizeof(struct zero_buffer) = sizeof(int)
3、开辟空间
那么我们使用的时候, 只需要开辟一次空间即可
/// 开辟
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);
}
4、释放空间
/// 销毁
free(zbuffer);
zbuffer = NULL;
// zero_length_array.c
#include
#include
#include
#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)); // 4
/// 开辟
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数组, 在申请内存空间时, 采用一次性分配的原则进行; 对于包含指针的结构体, 才申请空间时需分别进行, 释放时也需分别释放.
对于长度为的数组的访问可采用数组方式进行
在C90
之前, 并不支持0长度的数组, 0长度数组是 GNU C 的一个扩展, 因此早期的编译器中是无法通过编译的
对于 GNU C 增加的扩展, GCC 提供了编译选项来明确的标识出他们
1、-pedantic
选项,那么使用了扩展语法的地方将产生相应的警告信息
2、-Wall
使用它能够使GCC产生尽可能多的警告信息
3、-Werror
它要求GCC将所有的警告当成错误进行处理
gcc 1.c -Wall # 显示所有警告
#none warning and error
gcc 1.c -Wall -pedantic # 对GNU C的扩展显示警告
1.c: In function ‘main’:
1.c:7: warning: ISO C forbids zero-size array ‘a’
gcc 1.c -Werror -Wall -pedantic # 显示所有警告同时GNU C的扩展显示警告, 将警告用error显示
cc1: warnings being treated as errors
1.c: In function ‘main’:
1.c:7: error: ISO C forbids zero-size array ‘a’
0长度数组其实就是灵活的运用的数组指向的是其后面的连续的内存空间
struct buffer
{
int len;
char data[0];
};
在早期没引入0长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是
所以 GNU
就对其进行了0长度数组
的扩展. 当使用data[0]的时候, 也就是0长度数组的时候,0长度数组作为数组名, 并不占用存储空间.
在C99
之后,也加了类似的扩展,只不过用的是 char payload[]
这种形式(所以如果你在编译的时候确实需要用到-pedantic
参数,那么你可以将char payload[0]
类型改成char payload[]
, 这样就可以编译通过了,当然你的编译器必须支持C99标准的,如果太古老的编译器,那可能不支持了)
// 2.c payload
#include
#include
struct payload
{
int len;
char data[];
};
int main(void)
{
struct payload pay;
printf("%ld", sizeof(pay));
return EXIT_SUCCESS;
}
使用 -pedantic 编译后, 不出现警告, 说明这种语法是 C 标准的
gcc 2.c -pedantic -std=c99
所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式,并且最后一个成员变量,也就刚好是数据内容了.
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
如果你不是很确认其是否占用空间. 你可以用sizeof来计算一下。就可以知道sizeof(struct f1)=4,也就是int y[]其实是不占用空间的。但是这个0长度的数组,必须放在结构体的末尾。如果你没有把它放在末尾的话。编译的时候,会有如下的错误:
main.c:37:9: error: flexible array member not at end of struct
int y[];
^
到这边,你可能会有疑问,如果将struct f1中的int y[]替换成int *y,又会是如何?这就涉及到数组和指针的问题了. 有时候吧,这两个是一样的,有时候又有区别。
首先要说明的是,支持0长度数组的扩展,重点在数组,也就是不能用int *y指针来替换。sizeof的长度就不一样了。把struct f1改成这样:
struct f3 {
int x;
int *y;
};
在32/64位下, int均是4个字节, sizeof(struct f1)=4,而sizeof(struct f3)=16
因为 int *y 是指针, 指针在64位下, 是64位的, sizeof(struct f3) = 16, 如果在32位环境的话, sizeof(struct f3) 则是 8 了, sizeof(struct f1) 不变. 所以 int *y 是不能替代 int y[] 的.
// 3.c
#include
#include
struct f1 {
int x;
int y[];
} f1 = { 1, { 2, 3, 4 } };
struct f2 {
struct f1 f1;
int data[3];
} f2 = { { 1 }, { 5, 6, 7 } };
struct f3
{
int x;
int *y;
};
int main(void)
{
printf("sizeof(f1) = %d\n", sizeof(struct f1));
printf("sizeof(f2) = %d\n", sizeof(struct f2));
printf("szieof(f3) = %d\n\n", sizeof(struct f3));
printf("f1.x = %d\n", f1.x);
printf("f1.y[0] = %d\n", f1.y[0]);
printf("f1.y[1] = %d\n", f1.y[1]);
printf("f1.y[2] = %d\n", f1.y[2]);
printf("f2.f1.x = %d\n", f1.x);
printf("f2.f1.y[0] = %d\n", f2.f1.y[0]);
printf("f2.f1.y[1] = %d\n", f2.f1.y[1]);
printf("f2.f1.y[2] = %d\n", f2.f1.y[2]);
return EXIT_SUCCESS;
}
从上面可以看到,零长度数组可以实现一个长度可变的结构体
#include
#include
struct line {
int length;
char contents[0]; // 零长度数组,只能是结构体的最后一个成员
} ;
int main(void)
{
int i, count = 9;
char letter = 'A';
struct line *thisline = (struct line *)malloc(sizeof(struct line) + count);
thisline->length = count;
for (i = 0; i < count; i++)
thisline->contents[i] = letter++;
printf("sizeof(struct line) = %d\n", sizeof(struct line));
for (i = 0; i < thisline->length; i++)
printf("%c ", thisline->contents[i]);
printf("\n");
return 0;
}
在ISO C99中,使用变长数组也可以实现同样的功能(比如上面,优点在于大小可以在初始化的时候指定):
#include
#include
struct line {
int length;
char contents[];
};
struct line thisline = { 5, {'1', '2', '3', '4', '5' } };
int main(void)
{
int i;
printf("sizeof(struct line) = %d\n", sizeof(struct line));
printf("sizeof(thisline) = %d\n", sizeof(thisline));
for (i = 0; i < thisline.length; i++)
printf("%c ", thisline.contents[i]);
printf("\n");
return 0;
}
#include
#include
struct line {
int length;
char contents[];
};
int main(void)
{
int i;
struct line thisline = { 5, {'1', '2', '3', '4', '5' } };
printf("sizeof(struct line) = %d\n", sizeof(struct line));
printf("sizeof(thisline) = %d\n", sizeof(thisline));
for (i = 0; i < thisline.length; i++)
printf("%c ", thisline.contents[i]);
printf("\n");
return 0;
}
//----------------------
#include
#include
int main(void)
{
struct line {
int length;
char contents[];
};
int i;
struct line thisline = { 5, {'1', '2', '3', '4', '5' } };
printf("sizeof(struct line) = %d\n", sizeof(struct line));
printf("sizeof(thisline) = %d\n", sizeof(thisline));
for (i = 0; i < thisline.length; i++)
printf("%c ", thisline.contents[i]);
printf("\n");
return 0;
}
注意,不能是这样的结构
struct mystruct {
int arr[];
};