浅析长度为0的数组

Part1

转自: http://my.oschina.net/u/176416/blog/33054

     长度为0的数组在标准c和c++中是不允许的,如果使用长度为0的数组,编译时会产生错误,提示数组长度不能为0。但在GNUc中,这种用法却是合法的。它的最典型的用法就是位于数组中的最后一项,如上面所示,这样做主要是为了方便内存缓冲区的管理。如果你将上面的长度为0的数组换为指针,那么在分配内存时,需采用两步:首先,需为结构体分配一块内存空间;其次再为结构体中的成员变量分配内存空间。这样两次分配的内存是不连续的,需要分别对其进行管理。当使用长度为的数组时,则是采用一次分配的原则,一次性将所需的内存全部分配给它。相反,释放时也是一样的。

对于长度为0的数组,在gcc手册中,有如下一段代码片段:

1 struct line {
2 int length;
3 char contents[0];
4 };
5 
6 struct line *thisline = (struct line *)malloc (sizeof (struct line) + this_length);
7 thisline->length = this_length;

 

这段代码的主要含义是定义了一个结构体,并对其进行初始化,上面结构体的第二个成员变量contents[0]事实上是不占内存空间的,因此整个结构体的长度sizeof(struct line)为4。当采用malloc为其申请内存空间时,如上所示,申请了一段长度为结构体长度加可变长度的内存空间给结构体类型的指针,这时contents就指向申请的可变长度的内存空间。由于是一次申请的,所以这段可变长度的内存空间和前面的结构体长度的内存空间是连续的。对于这段可变长度的内存空间,可以采用数组的方式对其进行访问。对于整个结构体,当不再使用时,可以使用free函数一次性对其进行释放,而不必像指针那样分别释放。

 

下面举例进行说明:

 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 #define LENGTH 10
 4 
 5 struct test1
 6 {
 7 int a;
 8 int *b;
 9 }__attribute((packed));
10 
11 struct test2
12 {
13 int a;
14 int b[0];
15 }__attribute((packed));
16 
17 struct test3
18 {
19 int a;
20 int b[1];
21 }__attribute((packed));
22 
23 int main()
24 {
25 struct test1 *var1;
26 struct test2 *var2;
27 struct test3 *var3;
28 int i;
29 
30 printf("the length of struct test1:%d\n",sizeof(struct test1));
31 printf("the length of struct test2:%d\n",sizeof(struct test2));
32 printf("the length of struct test3:%d\n",sizeof(struct test3));
33 
34 var1=(struct test1*)malloc(sizeof(struct test1));
35 var1->a=1;
36 var1->b=(int *)malloc(sizeof(int));
37 *var1->b=1;
38 printf("\nvar1->a=%d,*(var1->b)=%d\n",var1->a,*var1->b);
39 
40 var2=(struct test2*)malloc(sizeof(struct test2)+sizeof(int)*LENGTH);
41 var2->a=2;
42 printf("\nvar2->a=%d\n",var2->a);
43 for(i=0;i<LENGTH;i++)
44 {
45 var2->b[i]=i;
46 printf("var2->b[i]=%d\t",var2->b[i]);
47 }
48 printf("\n\n"); 
49 
50 var3=(struct test3*)malloc(sizeof(struct test3));
51 var3->a=3;
52 (var3->b)[0]=3;
53 printf("var3->a=%d,(var3->b)[0]=%d\n",var3->a,(var3->b)[0]);
54 
55 
56 free(var1->b);
57 free(var1);
58 free(var2);
59 free(var3);
60 
61 }

 

这段程序的运行结果如下图所示:

从上面的结果可以看出:

l 长度为0的数组并不占有内存空间,而指针方式需要占用内存空间。

l 对于长度为0数组,在申请内存空间时,采用一次性分配的原则进行;对于包含指针的结构体,才申请空间时需分别进行,释放时也需分别释放。

l 对于长度为0的数组的访问可采用数组方式进行。

 

 Par2

转自:http://lijingzu7.blog.163.com/blog/static/21102828201021092641494/

在linuxforum上看见有人讨论数组大小为0的理解,觉得不错,特摘录下来,以备参考:
Q:数组大小为0应该怎么理解? 
比如: 

 1 struct page *page[0]; 
 2 unsigned long private[0] ____cacheline_aligned;
 3 
 4 //A:一个很好的例子就是 
 5 struct unix_address 
 6 { 
 7 atomic_t refcnt; 
 8 int len; 
 9 unsigned hash; 
10 struct sockaddr_un name[0]; 
11 }; 
12 
13 #define UNIX_PATH_MAX 108 
14 
15 struct sockaddr_un { 
16 sa_family_t sun_family; /* AF_UNIX */ 
17 char sun_path[UNIX_PATH_MAX]; /* pathname */ 
18 }; 

其中name实际上指向unix_address结构之后的空间。 
好处就是sockaddr_un是变长的,为了不浪费空间,就这样定义。到实际分配unix_address结构的时候按实际的地址大小一并分配空间,那样就可以通过unix_address->name来访问实际的地址。

这样做的具体的优点:

1 struct unix_address 
2 { 
3 atomic_t refcnt; 
4 int len; 
5 unsigned hash; 
6 void *name; 
7 }; 

1。name占用空间(4字节) 
2。分配空间的时候可以一起分配,但是需要把p->name设置为=p++,访问name的值时需要指针中
转一下 

而 

1 struct unix_address 
2 { 
3 atomic_t refcnt; 
4 int len; 
5 unsigned hash; 
6 struct sockaddr_un name[0]; 
7 }; 

没有上面两个缺点 
1。它不占用结构的空间 
2。如果访问某个p->name,却可以访问紧接p的内存空间 
利用上面两个特性,就可以根据实际的unix域地址大小来一起分配空间,通过p->name访问地址


-------------------------------------------------------------------

| |实际地址|

-------------------------------------------------------------------

^ ^

| |

p name

它可以用来定义一个变长的结构体,比如你可以动态分配这个结构体,并把分配的长度等于sizeof(struct) + len,这样len这么长的内存就可以直接用最后的那个没有实际空间的指针来引用了。

 

有人提到这样用malloc函数开辟空间后malloc(sizeof(struct) + len)。再用free释放时会不会出问题?

找到的资料来看,都认为不会:

(1)如果你有一个malloc到的指针,可以看一下它所指的地址,在前面一点有一个指明分配的空间的大小的数字,这个就是所谓的“释放内存大小的量”,如果你不相信的话可以用vc试试,我的确看到过。   
  但是这个数字一般来说不是你malloc或者realloc时候传递的size参数,因为堆分配策略中分配的堆内存不能过于零散,所以一般实际分配的长度是某些特定的值:4B,12B,24B,36B……等等。   
  楼主可以自己再验证一下,关于嵌入式系统,应该比普通的系统更加严格。

(2)free释放空间   的大小存放在什么地方是由堆的实现决定的.   
  如sandrowjw所说,如果堆是采用的buddy算法,其释放大小由内存起始地址决定了   
  如果是其它策略,则这个值可能会存放在指针前的几个字节,也可能存在某个固定的内存区域(只能被堆的实现所访问).

(3)free只传一个指针,是怎么知道要释放的内存大小的呢,有学习过操作系统内存管理的人可能知道,我们申请一块内存的时候,(以下可能不同系统实现不一样):

申请成功,那么系统给我们是一个指针的地址,这个地址里面的某个地方存放着内存块的描述符(包括内存类型和大小)这样,free的时候系统会先读取这个描述符,返回再释放,所以能释放掉所有我们申请的内存,具体详细的还是的学习学习操作系统原理。

当我们得到一个指针的时候,我们可以对其类型做强制转换,这都不影响内存里面的实际空间大小,释放的时候系统还是能安全释放。

可是如果我们对指针地址做了修改,比如:

char *p = (char *)malloc(sizeof(char) * LEN);

int *i = (int *)p;

那么free(i),OK,没问题;

如果

i = (int *)p + 1;

free(i); 那么挂了,呵呵!

就是这样了,地址被你改变了,找不到内存块的描述符了,也就不知道内存的类型和大小,自然也就挂了!

不过这个存放描述符的地址每个系统都不一样(也有可能有一样的,^_^),里面的具体内容也不一样;

 

Part3

转自:http://www.cnblogs.com/winkyao/archive/2012/02/14/2351885.html

其实很早在看LINUX下就看到这个东西,后来在MFC内存池里同样也看到了类似的东西,还依照MFC写过一个类似的小内存池,(MFC用的是return this + 1)后来在李先静的《系统程序员成长计划》里看到了类似的定义,于是心里想着总结一下,结果发现网上已经有牛人总结的很好了,于是乎就转了过来,谢谢你们的分享,这是我前进的动力!

同时,需要引起注意的:ISO/IEC 9899-1999里面,这么写是非法的,这个仅仅是GNU C的扩展,gcc可以允许这一语法现象的存在。但最新的C/C++不知道是否可以,我没有测试过。(C99允许。微软的VS系列报一个WARNING,即非常的标准扩展。)

结构体最后使用0或1的长度数组的原因,主要是为了方便的管理内存缓冲区,如果你直接使用指针而不使用数组,那么,你在分配内存缓冲区时,就必须分配结构体一次,然后再分配结构体内的指针一次,(而此时分配的内存已经与结构体的内存不连续了,所以要分别管理即申请和释放)而如果使用数组,那么只需要一次就可以全部分配出来,(见下面的例子),反过来,释放时也是一样,使用数组,一次释放,使用指针,得先释放结构体内的指针,再释放结构体。还不能颠倒次序。

其实就是分配一段连续的的内存,减少内存的碎片化。

 

标题   结构体最后的长度为0或者1的数组     选择自 googol4u 的 Blog

 

在Linux系统里,/usr/include/linux/if_pppox.h里面有这样一个结构:

1 struct pppoe_tag {
2     __u16 tag_type;
3     __u16 tag_len;
4     char tag_data[0];
5 } __attribute ((packed));

最后一个成员为可变长的数组,对于TLV(Type-Length-Value)形式的结构,或者其他需要变长度的结构体,用这种方式定义最好。使用起来非常方便,创建时,malloc一段结构体大小加上可变长数据长度的空间给它,可变长部分可按数组的方式访问,释放时,直接把整个结构体free掉就可以了。例子如下:

1 struct pppoe_tag *sample_tag;
2 __u16 sample_tag_len = 10;
3 sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag)+sizeof(char)*sample_tag_len);
4 sample_tag->tag_type = 0xffff;
5 sample_tag->tag_len = sample_tag_len;
6 sample_tag->tag_data[0]=....
7 ...
8 //释放时,
9 free(sample_tag)

 

是否可以用 char *tag_data 代替呢?其实它和 char *tag_data 是有很大的区别,为了说明这个问题,我写了以下的程序:

 1 //例1:test_size.c
 2  struct tag1
 3  {
 4      int a;
 5      int b;
 6  }__attribute ((packed));
 7 
 8  struct tag2
 9  {
10      int a;
11      int b;
12      char *c;
13  }__attribute ((packed));
14  struct tag3
15  {
16      int a;
17      int b;
18      char c[0];
19  }__attribute ((packed));
20  struct tag4
21  {
22      int a;
23      int b;
24      char c[1];
25  }__attribute ((packed));
26  int main()
27  {
28      struct tag2 l_tag2;
29      struct tag3 l_tag3;
30      struct tag4 l_tag4;
31 
32      memset(&l_tag2,0,sizeof(struct tag2));
33      memset(&l_tag3,0,sizeof(struct tag3));
34      memset(&l_tag4,0,sizeof(struct tag4));
35 
36      printf("size of tag1 = %d\n",sizeof(struct tag1));
37      printf("size of tag2 = %d\n",sizeof(struct tag2));
38      printf("size of tag3 = %d\n",sizeof(struct tag3));
39 
40      printf("l_tag2 = %p,&l_tag2.c = %p,l_tag2.c = %p\n",&l_tag2,&l_tag2.c,l_tag2.c);
41      printf("l_tag3 = %p,l_tag3.c = %p\n",&l_tag3,l_tag3.c);
42      printf("l_tag4 = %p,l_tag4.c = %p\n",&l_tag4,l_tag4.c);
43      exit(0);
44  }

__attribute ((packed)) 是为了强制不进行4字节对齐,这样比较容易说明问题。

程序的运行结果如下:

1 size of tag1 = 8
2 size of tag2 = 12
3 size of tag3 = 8
4 size of tag4 = 9
5 l_tag2 = 0xbffffad0,&l_tag2.c = 0xbffffad8,l_tag2.c = (nil)
6 l_tag3 = 0xbffffac8,l_tag3.c = 0xbffffad0
7 l_tag4 = 0xbffffabc,l_tag4.c = 0xbffffac4

从上面程序和运行结果可以看出:tag1本身包括两个32位整数,所以占了8个字节的空间。tag2包括了两个32位的整数,外加一个char *的指针,所以占了12个字节。tag3才是真正看出char c[0]和char *c的区别,char c[0]中的c并不是指针,是一个偏移量,这个偏移量指向的是a、b后面紧接着的空间,所以它其实并不占用任何空间。tag4更加补充说明了这一点。所以,上面的struct pppoe_tag的最后一个成员如果用char *tag_data定义,除了会占用多4个字节的指针变量外,用起来会比较不方便

方法一,创建时,可以首先为struct pppoe_tag分配一块内存,再为tag_data分配内存,这样在释放时,要首先释放tag_data占用的内存,再释放pppoe_tag占用的内存;

方法二,创建时,直接为struct pppoe_tag分配一块struct pppoe_tag大小加上tag_data的内存,从例一的420行可以看出,tag_data的内容要进行初始化,要让tag_data指向strct pppoe_tag后面的内存。

1 struct pppoe_tag {
2     __u16 tag_type;
3     __u16 tag_len;
4     char *tag_data;
5 } __attribute ((packed));
6 
7 struct pppoe_tag *sample_tag;
8 __u16 sample_tag_len = 10;

 

方法一:

1 sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag));
2 sample_tag->tag_len = sample_tag_len;
3 sample_tag->tag_data = malloc(sizeof(char)*sample_tag_len);
4 sample_tag->tag_data[0]=...
5 //释放时:
6 free(sample_tag->tag_data);
7 free(sample_tag);

方法二:

1 sample_tag = (struct pppoe_tag *)malloc(sizeof(struct pppoe_tag)+sizeof(char)*sample_tag_len);
2 sample_tag->tag_len = sample_tag_len;
3 sample_tag->tag_data = ((char *)sample_tag)+sizeof(struct pppoe_tag);
4 sample_tag->tag_data[0]=...
5 //释放时:
6 free(sample_tag);

所以无论使用那种方法,都没有char tag_data[0]这样的定义来得方便。

讲了这么多,其实本质上涉及到的是一个C语言里面的数组和指针的区别问题(也就是我们提到的内存管理问题,数组分配的是在结构体空间地址后一段连续的空间,而指针是在一个随机的空间分配的一段连续空间)。char a[1]里面的a和char *b的b相同吗?《Programming Abstractions in C》(Roberts, E. S.,机械工业出版社,2004.6)82页里面说:“arr is defined to be identical to &arr[0]”。也就是说,char a[1]里面的a实际是一个常量,等于&a[0]。而char *b是有一个实实在在的指针变量b存在。所以,a=b是不允许的,而b=a是允许的。两种变量都支持下标式的访问,那么对于a[0]和b[0]本质上是否有区别?我们可以通过一个例子来说明。

例二:

 1  #include <stdio.h>
 2  #include <stdlib.h>
 3 
 4  int main()
 5  {
 6      char a[10];
 7      char *b;
 8 
 9      a[2]=0xfe;
10      b[2]=0xfe;
11      exit(0);
12  }

编译后,用objdump可以看到它的汇编:

080483f0 <main>:
 80483f0:       55                      push   %ebp
 80483f1:       89 e5                   mov    %esp,%ebp
 80483f3:       83 ec 18                sub    $0x18,%esp
 80483f6:       c6 45 f6 fe             movb   $0xfe,0xfffffff6(%ebp)
 80483fa:       8b 45 f0                mov    0xfffffff0(%ebp),%eax
 80483fd:       83 c0 02                add    $0x2,%eax
 8048400:       c6 00 fe                movb   $0xfe,(%eax)
 8048403:       83 c4 f4                add    $0xfffffff4,%esp
 8048406:       6a 00                   push   $0x0
 8048408:       e8 f3 fe ff ff          call   8048300 <_init+0x68>
 804840d:       83 c4 10                add    $0x10,%esp
 8048410:       c9                      leave
 8048411:       c3                      ret
 8048412:       8d b4 26 00 00 00 00    lea    0x0(%esi,1),%esi
 8048419:       8d bc 27 00 00 00 00    lea    0x0(%edi,1),%edi

可以看出,a[2]=0xfe是直接寻址,直接将0xfe写入&a[0]+2的地址,而b[2]=0xfe是间接寻址,先将b的内容(地址)拿出来,加2,再0xfe写入计算出来的地址。所以a[0]和b[0]本质上是不同的。

但当数组作为参数时,和指针就没有区别了。

1 int do1(char a[],int len);
2 int do2(char *a,int len);

这两个函数中的a并无任何区别。都是实实在在存在的指针变量。

顺便再说一下,对于struct pppoe_tag的最后一个成员的定义是char tag_data[0],某些编译器不支持长度为0的数组的定义,在这种情况下,只能将它定义成char tag_data[1],使用方法相同。

在openoffice的源代码中看到如下数据结构,是一个unicode字符串结构,他的最后就用长度为1数组,可能是为了兼容或者跨编译器。

 1 typedef struct _rtl_uString
 2 {
 3     sal_Int32       refCount;
 4     sal_Int32       length;
 5     sal_Unicode     buffer[1];
 6 } rtl_uString;
 7 //这是不定长字符串。大概意思是这样:
 8 
 9 rtl_uString * str = malloc(256);
10 str->length = 256;
11 //str->buffer现在就指向一个长度为256 - 8的缓冲区

 

总结:通过上面的转载的文章,可以清晰的发现,这种方法的优势其实就是为了简化内存的管理,我们假设在理想的内存状态下,那么分配的内存空间,可以是按序下来的(当然,实际因为内存碎片等的原因会不同的)我们可以利用最后一个数组的指针直接无间隔的跳到分配的数组缓冲区,这在LINUX下非常常见,在WINDOWS下的我只是在MFC里见过类似的,别的情况下记不清楚了,只记得MFC里的是这么讲的,可以用分配的结构体的指针(this)直接+1(详细的方法请看我的博客:CE分类里的:内存池技术的应用和详细说明),就跳到实际的内存空间,当初也是想了半天,所以说,很多东西看似很复杂,其实都是基础的东西,要好好打实基础,这才是万丈高楼拔地巍峨的前提和保障,学习亦是如是,切忌好高骛远,应该脚踏实地,一步一步的向前走,而且要不时的总结自己的心得和体会,理论和实践不断的相互印证,才能够走得更远,看到更美丽的风景。

最后,再次感谢网上无私共享的童鞋们!!!

 

柔性数组结构成员 收藏 
【柔性数组结构成员
  C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其 他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。】 
C语言大全,“柔性数组成员”

【柔性数组结构成员
  C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其 他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括柔性数组的内存。包含柔性数组成员的结构用malloc ()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。】 
C语言大全,“柔性数组成员”

看看 C99 标准中 灵活数组成员:

结构体变长的妙用——0个元素的数组
有时我们需要产生一个结构体,实现了一种可变长度的结构。如何来实现呢?
看这个结构体的定义:

 1 typedef struct st_type
 2 {
 3 int nCnt;
 4 int item[0];
 5 }type_a;
 6 //(有些编译器会报错无法编译可以改成:)
 7 typedef struct st_type
 8 {
 9 int nCnt;
10 int item[];
11 }type_a;

这样我们就可以定义一个可变长的结构,用sizeof(type_a)得到的只有4,就是sizeof(nCnt)=sizeof(int)那

个0个元素的数组没有占用空间,而后我们可以进行变长操作了。

//C语言版:
type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));
//C++语言版:
type_a *p = (type_a*)new char[sizeof(type_a)+100*sizeof(int)];

这样我们就产生了一个长为100的type_a类型的东西用p->item[n]就能简单地访问可变长元素,原理十分简单

,分配了比sizeof(type_a)多的内存后int item[];就有了其意义了,它指向的是int nCnt;后面的内容,是没

有内存需要的,而在分配时多分配的内存就可以由其来操控,是个十分好用的技巧。
而释放同样简单:

//C语言版:
free(p);
//C++语言版:
delete []p;

其实这个叫灵活数组成员(fleible array member)C89不支持这种东西,C99把它作为一种特例加入了标准。但

是,C99所支持的是incomplete type,而不是zero array,形同int item[0];这种形式是非法的,C99支持的

形式是形同int item[];只不过有些编译器把int item[0];作为非标准扩展来支持,而且在C99发布之前已经有

了这种非标准扩展了,C99发布之后,有些编译器把两者合而为一。
下面是C99中的相关内容:
6.7.2.1 Structure and union specifiers

As a special case, the last element of a structure with more than one named member may have

an incomplete array type; this is called a flexible array member. With two exceptions, the

flexible array member is ignored. First, the size of the structure shall be equal to the offset

of the last element of an otherwise identical structure that replaces the flexible array member

with an array of unspecified length.106) Second, when a . (or ->) operator has a left operand

that is (a pointer to) a structure with a flexible array member and the right operand names that

member, it behaves as if that member were replaced with the longest array (with the same element

type) that would not make the structure larger than the object being accessed; the offset of the

array shall remain that of the flexible array member, even if this would differ from that of the

replacement array. If this array would have no elements, it behaves as if it had one element but

the behavior is undefined if any attempt is made to access that element or to generate a pointer

one past it.
例如在VC++6里使用两者之一都能通过编译并且完成操作,而会产生warning C4200: nonstandard extension

used : zero-sized array in struct/union的警告消息。
而在DEVCPP里两者同样可以使用,并且不会有警告消息

本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/todototry/archive/2007/04/11/1560458.aspx

posted @ 2014-10-25 15:52  felove  阅读(8216)  评论(0编辑  收藏  举报