Linux C中的零长度数组

在做Linux C开发时会遇到这样的代码:

struct var_data {
        int len;
        char data[0];
};

结构体var_data的最后一个成员是一个长度为0的数组。这不禁让人产生疑问,这样的语法正确吗?定义一个长度为0的数组有什么意义?它该如何使用?
接下来分析一下这个0长度数组。

代码举例

参考https://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html 里面的用法,整理了一段代码:

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

struct f1 {
  int x;
  int y[];
} f1 = {1, { 2, 3, 4 }};

struct f2 {
  struct f1 f1;
  int data[3];
} f2 = {{ 5 }, { 6, 7, 8 }};

struct f3 {
    int x;
    int *y;
};

int main(void)
{
    printf("sizeof(f1) = %ld\n", sizeof(struct f1));
    printf("sizeof(f2) = %ld\n", sizeof(struct f2));
    printf("szieof(f3) = %ld\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\n", f1.y[2]);

    printf("f2.f1.x = %d\n", f2.f1.x);
    printf("f2.f1.y[0] = %d		f2.data[0] = %d\n", f2.f1.y[0], f2.data[0]);
    printf("f2.f1.y[1] = %d		f2.data[1] = %d\n", f2.f1.y[1], f2.data[1]);
    printf("f2.f1.y[2] = %d		f2.data[2] = %d\n", f2.f1.y[2], f2.data[2]);

    return 0;
}

执行结果如下:

sizeof(f1) = 8
sizeof(f2) = 16
szieof(f3) = 16

f1.x = 1
f1.y[0] = 2
f1.y[1] = 3
f1.y[2] = 4

f2.f1.x = 5
f2.f1.y[0] = 6          f2.data[0] = 6
f2.f1.y[1] = 7          f2.data[1] = 7
f2.f1.y[2] = 8          f2.data[2] = 8

note :此测试仅仅是为了验证0长度数组的作用,并不适用于实际应用,具体应用可以参考下文。

首先看一下sizeof计算得到的长度:

可以看到sizeof(f1) = 4, szieof(f3) = 16f3就相当于把f1中的int y[]int *y进行替换,
根据sizeof(int) = 4 可以看出int y[] 其实是不占用空间的,也就是说 int *y 是不能替代 int y[] 的。(这里不介绍结构体大小的计算方法)

再看一下打印的数据信息:
  1. f1的成员y的长度为0,但是给它进行了赋值,而且也成功的打印出了数据信息。
  2. 在给f2赋值时,并没有对f2成员里f1的成员y进行赋值(结构体套嵌用法),但是打印的结果却是f2.f1.y[0], f2.f1.y[1], f2.f1.y[2]f2.data[0], f2.data[1], f2.data[2] 相同。
看到此处可能会有三个疑问:
  • 疑问一 为什么struct f1 所占的空间是4,int y[] 不应该是相当于指针吗?
  • 疑问二 如果struct f1中数组y的长度是0,为什么还能对其进行赋值?
  • 疑问三 f2.f1.y的值为什么与f2.data是相同的。

这些疑问会在接下来的介绍中进行解答。

Arrays of Length Zero

0长度数组是GNU C是对标准的C的一个扩展, 还有其他诸如case范围语句表达式等扩展。

既然是GNU C的实现,因此并不是所有的编译器都支持,移植有风险。但是在C99之后,也加了类似的扩展,这种扩展被称为柔性数组。

关于疑问一其实本质上涉及到的是一个C语言里面的数组和指针的区别问题。char a[]里面的a和char *b的b相同吗?
《Programming Abstractions in C》中有这样一句话:“arr is defined to be identical to &arr[0].”
也就是说,char a[]的a实际是一个常量,等于&a[0]。而char *b则是指针变量b。 所以,a = b是不允许的,而b = a是允许的。
对于编译器来说,此时长度为0的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量。
总的来说,数组名都不是指针,也就是int y[]在结构体中并不是指针而是代表这结构体的一个偏移量,size为0,所以sizeof(f1) = 4。

0长度数组其实就是灵活的运用的数组指向的是其后面的连续的内存空间,这也就同时解释了疑问二和疑问三:
既然是偏移量那就可以后面的地址进行赋值,也就是给y赋值。同样的f2.f1.y指向的就是f2.data的首地址,这就解释了为什么f2.f1.y的值与f2.data是相同的。

关于0长度数组为什么不占用空间的原因,可以通过汇编验证一下,后续会添加上。

0长度数组的用途

总是能看到这样的定义:

struct buffer {
    int     len;
    char    data[1024];
};

显而易见定义了一个固定size的结构体,但是如果用不到1024长度的数据则会造成浪费,那一种改进的方法是:

struct buffer {
    int     len;
    char    *data;
};

这种方法可以在使用时进行空间分配,用法如下:

struct buffer pbuffer;
if ((pbuffer = (struct buffer *)malloc(sizeof(struct buffer))) != NULL) {
    pbuffer->len = 12;
    if ((pbuffer->data = (char *)malloc(sizeof(char) * 12)) != NULL) {
        memcpy(pbuffer->data, "Hello World", 12);
    }
}
free(pbuffer->data);
free(pbuffer);
pbuffer = NULL;

可以看出,使用指针定义结构体,只多使用了一个指针大小的空间,无需使用1024长度的数组,不会造成空间的大量浪费。

但那是开辟空间时,需要额外开辟数据域的空间,施放时候也需要释放数据域的空间,但是实际使用场景可能比较复杂,
往往在函数中开辟空间, 然后返回给使用者指向 struct buffer 的指针, 这时候我们并不能假定使用者了解我们开辟的细节, 并按照约定的操作释放空间, 因此使用起来多有不便, 甚至造成内存泄漏。

基于以上两个问题,使用0长度数组有很显著的优势,既可以解决空间浪费也可以让代码逻辑更简单:

struct zero_buffer {
	int     len;
	char    data[0];
};

struct zero_buffer zbuffer;
if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * 12)) != NULL) {
    zbuffer->len = 12;
    memcpy(zbuffer->data, "Hello World", 12);
}

free(zbuffer);
zbuffer = NULL;

总结

GNU C允许使用零长度数组,它们非常有用。如下是一个零长度数组的应用。

struct var_data {
int len;
char data[0];
};

零长数组使用的优缺点:

优点

  1. 不需要使用指针来分配内存,节约一个指针变量所占内存大小,也使内存申请方式更加便捷;
  2. 分配的内存连续,管理与释放简单,只需要一次操作。

缺点

  1. 零长数组是GNU C的实现,非标准,因此并不是所有的编译器都支持,有移植风险。
  2. 数组的定义必须放在结构体的最后,应用场景比较局限。

猜你喜欢

转载自blog.csdn.net/weixin_38056448/article/details/83188271