首页 > 代码库 > 结构体中的指针与零长度数组

结构体中的指针与零长度数组

0长度的数组在ISO C和C++的规格说明书中是不允许的,但是由于gcc 预先支持C99的这种玩法,所以,“零长度数组”在gcc环境下是合法的。

先看下面两个例子。

pzeroLengthArray.c

#include <stdio.h>

struct str
{
	int len;
	char *s;
};

struct foo
{
	struct str *a;
};

int main()
{
	struct foo f = {0};
	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 0;
}

zeroLengthArray.c

#include <stdio.h>

struct str
{
	int len;
	char s[0];
};

struct foo
{
	struct str *a;
};

int main()
{
	struct foo f = {0};
	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 0;
}


编译,运行如下

gcc -g -o ptest pzeroLengthArray.c

gcc -g -o test zeroLengthArray.c

[root@SPA tmp]# ./test
sizeof(struct str) = 4
before f.a->s.
before printf f.a->s.
Segmentation fault (core dumped)
[root@SPA tmp]# ./ptest 
sizeof(struct str) = 16
before f.a->s.
Segmentation fault (core dumped)

从上面的运行结果可以看出,sizeof的结果以及发生段错误的位置,均不相同。

 

从汇编分析原因

 

生成汇编代码,分析如下

gcc -S pzeroLengthArray.c

gcc -S zeroLengthArray.c

[root@SPA tmp]# diff pzeroLengthArray.s zeroLengthArray.s 
1c1
<  .file "pzeroLengthArray.c"
---
>  .file "zeroLengthArray.c"
21c21
<  movl $16, %esi
---
>  movl $4, %esi
27,30d26
<  movq -16(%rbp), %rax
<  movq 8(%rax), %rax
<  testq %rax, %rax
<  je .L2
34c30
<  movq 8(%rax), %rdi
---
>  leaq 4(%rax), %rdi
39d34
< .L2:

可以看到有

对于char s[0]来说,汇编代码用了leaq指令,leaq 4(%rax), %rdi
对于char*s来说,汇编代码用了movq指令,movq 8(%rax), %rdi

lea全称load effective address,是把地址放进去,而mov则是把地址里的内容放进去。
从这里可以看到,访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)。

访问相对地址,程序不会crash,但是,访问一个非法的地址中的内容,程序就会crash。

 

零长度数组存在的价值

 

第一,节省内存。从上面的例子中可以看出,零长度数组不占用内存空间,而指针却占用内存空间。

第二,方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。
第三,这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。

test.c

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

struct zerodemo{
        int num;
        char zero[0];
};

struct ptrdemo{
        int num;
        char *zero;
};

#define LEN (sizeof(char)*100)

int main(){
        struct zerodemo *zd =(struct zerodemo *)malloc(sizeof(struct zerodemo) + LEN);
        zd->num = 10;
        memset(zd->zero,‘a‘, LEN);

        struct ptrdemo *pd = (struct ptrdemo *)malloc(sizeof(struct ptrdemo));
        pd->zero = (char *)malloc(LEN);
        pd->num = 10;
        memset(pd->zero, ‘a‘, LEN);

        return 0;
}

gdb调试如下

(gdb) p zd
$1 = (struct zerodemo *) 0x601010
(gdb) p *zd
$2 = {num = 10, zero = 0x601014 ‘a‘ <repeats 100 times>, "!"}
(gdb) p zd->zero
$3 = 0x601014 ‘a‘ <repeats 100 times>, "!"
(gdb) p pd
$4 = (struct ptrdemo *) 0x601080
(gdb) p *pd
$5 = {num = 10, zero = 0x6010a0 ‘a‘ <repeats 100 times>}
(gdb) p pd->zero
$6 = 0x6010a0 ‘a‘ <repeats 100 times>
(gdb) x /20b zd
0x601010: 10 0 0 0 97 97 97 97
0x601018: 97 97 97 97 97 97 97 97
0x601020: 97 97 97 97
(gdb) x /20b pd
0x601080: 10 0 0 0 0 0 0 0
0x601088: -96 16 96 0 0 0 0 0
0x601090: 0 0 0 0
(gdb) x /20b zd->zero
0x601014: 97 97 97 97 97 97 97 97
0x60101c: 97 97 97 97 97 97 97 97
0x601024: 97 97 97 97
(gdb) x /20b pd->zero
0x6010a0: 97 97 97 97 97 97 97 97
0x6010a8: 97 97 97 97 97 97 97 97
0x6010b0: 97 97 97 97

 

结构体中的指针与零长度数组