C语言 零长度数组的应用

注:本文大部分源引: What is the advantage of using zero length arrays in C 。但是并非逐句翻译,而是加上了很多自己的理解,除了其中的内容之外,还有加上关于国内一些说法的澄清,以及 windows 方面的例子,并补充了一些运用。转载请标明出处 @lellansin

零长度(zero length)的数组这听起来有点让人疑惑。笔者也百度过不少目前国内关于这个概念的博文,不过发现大都只是知道这个概念,却不知道它的应用。那么这里就让笔者当个搬运工给大家科普一下。

首先请大家看看下面的例子。你想要一个结构体来描述 email 数据:

struct email 
{
    int send_time;
    int flags;
    int length;
    char body[EMAIL_BODY_MAX];
};

为了方便讨论,上述的例子中已经省略了很多字段。请大家不要在意,我们需要在意的是,这个结构体中有一个很常见的问题等待我们解决:

即使你的邮件只有一两个字,依旧不得不消耗 EMAIL_BODY_MAX 个字节的内存。不幸的是,email 的长度总是有着很大的不确定性:有的很大,而有的可能只有几个字。我们不仅常常浪费 EMAIL_BODY_MAX 个字节中的大部分,而且为了保险总是会把 EMAIL_BODY_MAX 设置的很大。更糟糕的是,目前并没有明确的标准来限定 email 内容的长度,也就是说如果你的应用发送不了这么长的邮件,但是别人可以的话,将是一个很难堪的情况。所以,我们很希望停止这个恶性循环。

而零长度的数组则正好可以解决这个问题:

struct email 
{
    int send_time;
    int flags;
    int length;
    char body[];
};

如果你在普通情况下使用这个结构体,你将会发现 body 这个成员并不占用任何内存:

struct email *email = malloc( sizeof(struct email) );

这里的 body 就是零长度的。 你不能合法的访问 body 所在的内存。

实际上在 32位系统上这个结构体只占用了 12个字节的内存,这些都是由前三个 int 成员占用的。就这个现象而言,也看到不少国人讨论,那么让笔者在这里用一个例子总结一下他们的讨论结果:

#include <stdio.h>

struct email
{
    int send_time;
    int flags;
    int length;
    char body[];
};

int main()
{
    struct email e;
    printf("%d n", sizeof(e));
    printf("%p n", &e);
    printf("%p n", &(e.body) );
    printf("%p n", ((int)&e + sizeof(e)) );
    printf("%d n", ((int)&e + sizeof(e)) == ((int)&e.body) );
}

结果是发现,零长度数组 body 不占用结构体的大小,且其值指向结构体的末尾。目前一些相关文章中,有说这种写法只在 GNU C 中才有,实际上只是 char body[0] 这种直接写 0 的写法不兼容,省略不写的零长度数组定义方式是兼容的。

不过要利用零长度数组,不应该用普通的方式来获得内存,而应该这样做:

struct email *email = malloc (sizeof (struct email) + email_length);
email->length = email_length;

由于零长度数组刚好指向结构体的末尾,也就正好对应多申请的内存。所以当你的邮件内容为 16 字节长度的时候,body 的长度也只有 16字节,而整个结构体的长度则是 28 字节。

通过这个方式,零长度数组与普通数组同样拥有足够有效的内存,区别只是普通数组因为类型可以通过 sizeof 获取其长度,而零长度数组则不能。

精明的读者现在会问,为什么不用直接在结构体中使用指针存储这个长度不固定的 body?如果你觉得你的情况中使用指针就可以解决问题,那么也非常推荐你直接用指针。

实际上,零长度数组只在特定清下非常有用:当你有一个很大的结构体,并且该结构体中包含如上述 body 的动态长度数组,同时你需要把这个结构体的数据共享给其他的进程或者发送给其他的计算机。例如,在 linux 内核提供的监控文件 inotify 接口中就使用了零长度数组:

struct inotify_event {  
    int      wd;       /* Watch descriptor */  
    uint32_t mask;     /* Mask of events */  
    uint32_t cookie;   /* Unique cookie associating related 
                          events (for rename(2)) */  
    uint32_t len;      /* Size of ’name’ field */  
    char     name[];   /* Optional null-terminated name */  
};  

该结构体代表一个 inotify 事件,事件类型例如 写入 目标文件名比如 /home/rlove/wolf.txt,你会发现这样一个问题————我们需要多大的字符数组来存储这个文件名?要知道,文件名可能是任意长度的,不同文件系统的最大文件名长度各异(PATH_MAX不是标准长度,只是个偷懒的常量)。现在,如果只是返回文件名,我们可以只是简单的返回一个动态申请的地址 char * 。但是我们必须返回的是一个巨大的结构体。而且,这里是在做系统调用,所以结构体中如果存储的是 char * 地址,传过去的时候其所指向的值不会再结构体中展开,而且传过去之后这个地址就会失效(系统调用的内存空间与普通进程的用户空间不同,属于内核空间)。所以在这些限制条件下,使用零长度数组是最好的解决方案。

windows 程序员可能会问, 这是否是个只在 linux 平台是个常见的技巧。当然不是,windows 上也是可以运用的。比如当你在使用 socket 或者管道的时候,零长度数组也能起到一个不错的作用:

// 使用零长度数组定义一个消息结构体
typedef struct
{
    int length;
    char content[];
} MyMsg;

// socket 发送结构体数据
msg = (MyMsg *)malloc(sizeof(MyMsg) + 12);
msg->length = 12;
strcpy(msg->content, "hello world");
send(sockClient, (char *)msg, (sizeof(MyMsg) + 12), 0);

// socket 接收数据
recv(sockClient, recvBuf, 100, 0);
recvMsg = (MyMsg *)recvBuf;
printf("length: %d, content:%s n", recvMsg->length, recvMsg->content);

与在结构体中使用指针相比较,这种方式可以让你的结构体在传输的时候更加方便。有些人可能会说,在传输的过程中直接发送结构体可以说是明文发送数据,这是很不安全的。但是笔者想说的是,如果你使用的是对一块数据进行加密的通用方法(而不是针对某一类结构体写的加密方法),那么使用零长度数组也是很方便的。

而且,值得注意的是,由于这种运用方式中,其内存是通过 malloc 申请的一段连续内存,所以 free 函数可以一次性对其进行释放,而不必像指针那样分别释放。

所以,笔者这里想说,零长度数组在普通的情况下也不是一种鸡肋的写法,这里附上例子各位可以比较一下:

非常基础的存储学生信息:

// 学生 结构体
typedef struct student
{
    int id;
    char name[100];
} student;

使用零长度数组来写一个班级结构体的话可以这样:

// 班级 结构体
typedef struct class 
{
    char *teacher;
    int class_id;
    student students[];
} class;

// 使用班级
class *classA;
classA = (class *) malloc (sizeof (class) + sizeof (student) * number_of_students);

// 遍历学生
for(int i=0; i < number_of_students; i++)
    class->students[i].id = i;

// 释放资源
free(classA);

而, 这个例子如果使用指针来写的话则会变成:

typedef struct class 
{
    char *teacher;
    int class_id;
    student *students;
} class;

class *classA;

classA = (class *) malloc (sizeof (class));
classA->students = (student *) malloc(sizeof (student) * number_of_students );

// 遍历 ...

// 释放
free(classA->students);
free(classA);

不知道各位有没有感同身受, 至少笔者是能感觉到, 使用指针没有用零长度数组那么优雅, 申请和释放都多一步。

最后提一点,请合理利用零长度数组,出于可读性和可维护性考虑,在波动数值较小的范围内还是建议使用指定长度的数组。另外,使用包含零长度数组的结构体作为另一个结构体的成员是很不推荐的。

Advertisements

6 thoughts on “C语言 零长度数组的应用

  1. 读到一半的时候我就想到了一个问题,果然你最后一句话就是“另外,使用包含零长度数组的结构体作为另一个结构体的成员是很不推荐的。”,这个问题怎么解决?另外问一个问题,就是结构体中为什么零长数组的指针是最后?因为你把他申明在最后了?

    • 是的就是最后,如果要使用这种结构体作为其他结构体的成员,那么也必须保持这个结构体作为成员的时候是在另外一个结构体的最后一个。如果结构层次多得话,或者不清楚细节的话很容易乱所以很不推荐。

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s