博客类网站源码,电子商务网站建设总结与体会,凡科互动游戏,保山市住房和城乡建设局网站点击蓝字关注我们零长度数组概念#xff1a;众所周知, GNU/GCC 在标准的 C/C 基础上做了有实用性的扩展, 零长度数组#xff08;Arrays of Length Zero#xff09; 就是其中一个知名的扩展.多数情况下, 其应用在变长数组中, 其定义如下#xff1a;struct Packet
{int state… 点击蓝字关注我们零长度数组概念众所周知, GNU/GCC 在标准的 C/C 基础上做了有实用性的扩展, 零长度数组Arrays of Length Zero 就是其中一个知名的扩展.多数情况下, 其应用在变长数组中, 其定义如下struct Packet
{int state;int len;char cData[0]; //这里的0长结构体就为变长结构体提供了非常好的支持
};首先对 0 长度数组, 也叫柔性数组做一个解释 用途 : 长度为0的数组的主要用途是为了满足需要变长度的结构体用法 : 在一个结构体的最后,声明一个长度为 0 的数组, 就可以使得这个结构体是可变长的. 对于编译器来说, 此时长度为 0 的数组并不占用空间, 因为数组名本身不占空间, 它只是一个偏移量, 数组名这个符号本身代表了一个不可修改的地址常量注意 : 数组名永远都不会是指针!, 但对于这个数组的大小, 我们可以进行动态分配。注意 如果结构体是通过 calloc、malloc 或 者 new 等动态分配方式生成在不需要时要释放相应的空间。优点 比起在结构体中声明一个指针变量、再进行动态分 配的办法这种方法效率要高。因为在访问数组内容时不需要间接访问避免了两次访存。缺点 在结构体中数组为 0 的数组必须在最后声明使用上有一定限制。对于编译器而言, 数组名仅仅是一个符号, 它不会占用任何空间, 它在结构体中, 只是代表了一个偏移量, 代表一个不可修改的地址常量0 长度数组的用途我们设想这样一个场景, 我们在网络通信过程中使用的数据缓冲区, 缓冲区包括一个 len 字段和 data 字段, 分别标识数据的长度和传输的数据, 我们常见的有几种设计思路定长数据缓冲区, 设置一个足够大小 MAX_LENGTH 的数据缓冲区设置一个指向实际数据的指针, 每次使用时, 按照数据的长度动态的开辟数据缓冲区的空间我们从实际场景中应用的设计来考虑他们的优劣. 主要考虑的有, 缓冲区空间的开辟、释放和访问。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-gt;len, 作为包头(就是多出来的那部分)这个包头是用来描述紧接着包头后面的数据部分的长度这里是 1024, 所以前四个字节赋值为 1024 (既然我们要构造不定长数据包那么这个包到底有多长呢因此我们就必须通过一个变量来表明这个数据包的长度这就是 len 的作用)而紧接其后的内存是真正的数据部分, 通过 p-gt;data, 最后, 进行一个 memcpy() 内存拷贝, 把要发送的数据填入到这段内存当中释放那么当使用完毕释放数据的空间的时候, 直接释放就可以了// 销毁
free(mbuffer);
mbuffer NULL;2、小结:使用定长数组, 作为数据缓冲区, 为了避免造成缓冲区溢出, 数组的大小一般设为足够的空间 MAX_LENGTH, 而实际使用过程中, 达到 MAX_LENGTH 长度的数据很少, 那么多数情况下, 缓冲区的大部分空间都是浪费掉的但是使用过程很简单, 数据空间的开辟和释放简单, 无需程序员考虑额外的操作3、 指针数据包(开辟空间, 释放, 访问):如果你将上面的长度为 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 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。4、变长数据缓冲区(开辟空间, 释放, 访问)定长数组使用方便, 但是却浪费空间, 指针形式只多使用了一个指针的空间, 不会造成大量空间分浪费, 但是使用起来需要多次分配, 多次释放, 那么有没有一种实现方式能够既不浪费空间, 又使用方便的呢?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;总结// 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 的数组的访问可采用数组方式进行GNU Document中 变长数组的支持参考6.17 Arrays of Length ZeroC Struct Hack – Structure with variable length array在 C90 之前, 并不支持 0 长度的数组, 0 长度数组是 GNU C 的一个扩展, 因此早期的编译器中是无法通过编译的对于 GNU C 增加的扩展, GCC 提供了编译选项来明确的标识出他们:-pedantic 选项那么使用了扩展语法的地方将产生相应的警告信息-Wall 使用它能够使 GCC 产生尽可能多的警告信息-Werror, 它要求 GCC 将所有的警告当成错误进行处理// 1.c
#include stdio.h
#include stdlib.h
int main(void)
{char a[0];printf(%ld, sizeof(a));return EXIT_SUCCESS;
}我们来编译# 显示所有警告
gcc 1.c -Wall
#none warning and error# 对GNU C的扩展显示警告
gcc 1.c -Wall -pedantic
1.c: In function ‘main’:
1.c:7: warning: ISO C forbids zero-size array ‘a’# 显示所有警告同时GNU C的扩展显示警告, 将警告用 error 显示
gcc 1.c -Werror -Wall -pedantic
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 长度数组的时候, 大家是通过定长数组和指针的方式来解决的, 但是定长数组定义了一个足够大的缓冲区, 这样使用方便, 但是每次都造成空间的浪费指针的方式 要求程序员在释放空间时必须进行多次的 free 操作 而我们在使用的过程中往往在函数中返回了指向缓冲区的指针 我们并不能保证每个人都理解并遵从我们的释放方式。所以 GNU 就对其进行了 0 长度数组的扩展. 当使用 data[0] 的时候, 也就是 0 长度数组的时候0长度数组作为数组名, 并不占用存储空间。在 C99 之后也加了类似的扩展只不过用的是 char payload[] 这种形式所以如果你在编译的时候确实需要用到 -pedantic参数那么你可以将 char payload[0] 类型改成 char payload[] , 这样就可以编译通过了当然你的编译器必须支持 C99 标准的如果太古老的编译器那可能不支持了)// 2.c payload
#include stdio.h#
include stdlib.hstruct payload
{int len;char data[];
};int main(void)
{struct payload pay;printf(%ld, sizeof(pay));return EXIT_SUCCESS;
}使用 -pedantic 编译后, 不出现警告, 说明这种语法是 C 标准的gcc 2.c -pedantic -stdc99所以结构体的末尾, 就是指向了其后面的内存数据。因此我们可以很好的将该类型的结构体作为数据报文的头格式并且最后一个成员变量也就刚好是数据内容了.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 structint 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 stdio.h
#include stdlib.hstruct 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;
}0 长度数组的其他特征:1、为什么 0 长度数组不占用存储空间:0 长度数组与指针实现有什么区别呢, 为什么0长度数组不占用存储空间呢?其实本质上涉及到的是一个 C 语言里面的数组和指针的区别问题。char a[1] 里面的 a 和 char *b 的 b 相同吗《 Programming Abstractions in C》Roberts, E. S.机械工业出版社2004.682页里面说“arr is defined to be identical to arr[0]”.也就是说char a[1] 里面的 a 实际是一个常量等于 a[0] 。而 char *b 是有一个实实在在的指针变量 b 存在。所以ab 是不允许的而 ba 是允许的。两种变量都支持下标式的访问那么对于 a[0] 和 b[0]本质上是否有区别我们可以通过一个例子来说明。参见如下两个程序 gdb_zero_length_array.c和 gdb_zero_length_array.c// gdb_zero_length_array.c
#include stdio.h
#include stdlib.hstruct str
{int len;char s[0];
};struct foo
{struct str *a;
};int main(void)
{struct foo f { NULL };printf(sizeof(struct str) %d\n, sizeof(struct str));printf(before f.a-s.\n);if(f.a-s){printf(before printf f.a-s.\n);printf(f.a-s);printf(before printf f.a-s.\n);}return EXIT_SUCCESS;
}\// gdb_pzero_length_array.c
#include stdio.h#
include stdlib.hstruct str
{int len;char *s;
};struct foo
{struct str *a;
};int main(void)
{struct foo f { NULL };printf(sizeof(struct str) %d\n, sizeof(struct str));printf(before f.a-s.\n);if (f.a-s){printf(before printf f.a-s.\n);printf(f.a-s);printf(before printf f.a-s.\n);}return EXIT_SUCCESS;
}可以看到这两个程序虽然都存在访问异常, 但是段错误的位置却不同我们将两个程序编译成汇编, 然后 diff 查看他们的汇编代码有何不同gcc -S gdb_zero_length_array.c -o gdb_test.s
gcc -S gdb_pzero_length_array.c -o gdb_ptest
diff gdb_test.s gdb_ptest.s1c1.file gdb_zero_length_array.c
---.file gdb_pzero_length_array.c
23c23movl $4, %esi
---movl $16, %esi
30c30addq $4, %rax
---movq 8(%rax), %rax
36c36addq $4, %rax
---movq 8(%rax), %rax # printf(sizeof(struct str) %d\n, sizeof(struct str));
23c23movl $4, %esi #printf(sizeof(struct str) %d\n, sizeof(struct str));
---movl $16, %esi #printf(sizeof(struct str) %d\n, sizeof(struct str));从 64 位系统中, 汇编我们看出, 变长数组结构的大小为 4, 而指针形式的结构大小为 16f.a-s
30c30/36c36addq $4, %rax
---movq 8(%rax), %rax可以看到有对于 char s[0] 来说, 汇编代码用了 addq 指令, addq $4, %rax对于 char *s 来说汇编代码用了 movq 指令, movq 8(%rax), %raxaddq 对 %rax sizeof(struct str), 即 str 结构的末尾即是 char s[0] 的地址, 这一步只是拿到了其地址, 而 movq 则是把地址里的内容放进去, 因此有时也被翻译为 leap 指令, 参见下一例子从这里可以看到, 访问成员数组名其实得到的是数组的相对地址, 而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)访问相对地址程序不会 crash但是访问一个非法的地址中的内容程序就会 crash。// 4-1.c
#include stdio.h
#include stdlib.hint main(void)
{char *a;printf(%p\n, a);return EXIT_SUCCESS;
}4-2.c
#include stdio.h
#include stdlib.hint main(void)
{char a[0];printf(%p\n, a); return EXIT_SUCCESS;
}$ diff 4-1.s 4-2.s
1c1.file 4-1.c
---.file 4-2.c
13c13subl $16, %esp
---subl $32, %esp
15c15leal 16(%esp), %eax
---movl 28(%esp), %eax对于 char a[0] 来说, 汇编代码用了 leal 指令, leal 16(%esp), %eax对于 char *a 来说汇编代码用了 movl 指令, movl 28(%esp), %eax2、地址优化:// 5-1.c
#include stdio.h
#include stdlib.hint main(void)
{char a[0];printf(%p\n, a);char b[0];printf(%p\n, b);return EXIT_SUCCESS;
}img由于 0 长度数组是 GNU C 的扩展, 不被标准库任可, 那么一些巧妙编写的诡异代码, 其执行结果就是依赖于编译器和优化策略的实现的.比如上面的代码, a 和 b 的地址就会被编译器优化到一处, 因为 a[0] 和 b[0] 对于程序来说是无法使用的, 这让我们想到了什么?编译器对于相同字符串常量, 往往地址也是优化到一处, 减少空间占用// 5-2.c
#include stdio.h
#include stdlib.hint main(void)
{const char *a Hello;printf(%p\n, a);const char *b Hello;printf(%p\n, b);const char c[] Hello;printf(%p\n, c);return EXIT_SUCCESS;
}*声明本文于网络整理版权归原作者所有如来源信息有误或侵犯权益请联系我们删除或授权事宜。戳“阅读原文”我们一起进步