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);

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

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

qmlscene WARNING **: Unable to register app: Invalid application ID

编写 QML 的时候运行出现的警告:

$ /usr/bin/qmlscene hello.qml

unity::action::ActionManager::ActionManager(QObject*):
	Could not determine application identifier. HUD will not work properly.
	Provide your application identifier in $APP_ID environment variable.

** (qmlscene:12464): WARNING **: Unable to register app: GDBus.Error:org.freedesktop.DBus.Error.InvalidArgs: Invalid application ID

这个警告是因为没有指定应用 id(application ID),在命令的前面指明 APP_ID 这个环境变量即可:

$ APP_ID=hello /usr/bin/qmlscene hello.qml

如果是使用 ubuntu sdk 这个 IDE 编写的时候 Ctrl + R 运行报错。
请找到 Ubuntu SDK (其实就是定制版的 Qt Creator) 左侧的 【Projects】接着请找到Desktop下,Build旁边的 【Run】,然后在出来的界面中找到下方的 Rune Environment 下点击【Details】点添加,跟加 windows 的环境变量一样,加上一个 APP_ID 然后 value 比如项目名 hello 。
然后再来 Ctrl+R 就不会出现这个错误了。

另外 .desktop 文件中的 Exec 参数修改是不影响 Ubuntu sdk 运行的,那个文件中指明的是打包之后告诉你的 ubuntu 桌面如何运行的一个配置。

ubuntu sdk 安装 (ubuntu touch 开发环境)

按照官方的说法,最好使用 ubuntu 14.04 来进行开发。安装的过程很简单:

sudo add-apt-repository ppa:ubuntu-sdk-team/ppa
sudo apt-get update
sudo apt-get install ubuntu-sdk

其过程就是添加一个 SDK Release PPA 的地址,然后更新 apt-get,接着使用 apt-get 来直接安装 ubuntu sdk。
不过博主直接安装有报错:

W: Failed to fetch http://us.archive.ubuntu.com/ubuntu/dists/trusty-updates/universe/binary-amd64/Packages  Hash Sum mismatch
W: Failed to fetch http://us.archive.ubuntu.com/ubuntu/dists/trusty-updates/main/binary-i386/Packages  Hash Sum mismatch
W: Failed to fetch http://us.archive.ubuntu.com/ubuntu/dists/trusty-updates/universe/binary-i386/Packages  Hash Sum mismatch
E: Some index files failed to download. They have been ignored, or old ones used instead.

原因就如英文原文所说的 old ones used,有些库太老了,需要更新。这个时候你需要

sudo apt-get dist-upgrade

之后再 sudo apt-get install ubuntu-sdk 就可以了。等安装好了之后就可以在菜单里面搜索 ubuntu sdk 找到,或者直接在 terminal 上运行:

ubuntu-sdk

命令也可以运行该 IDE。

小Tip:Ctrl + N 新建项目,Ctrl + R 查看运行效果

源引官方文档:Installing the ubuntu sdk

ubuntu 12 升级到 14 错误记录 no suitable download server was found

博主是按官方文档走出的错,这里是官方文档出处:http://www.ubuntu.com/download/desktop/upgrade
所以最开始附上一种跟官方文档流程不一样的,各位也可以尝试一下:http://ubuntuserverguide.com/2014/06/how-to-upgrade-ubuntu-server-12-04-to-ubuntu-server-14-04-lts.html

按照官方文档,最初说是按下菜单键(win图标),然后输入 updater 选第一个就是了。会自动更新。
如果你找不到也可以用 terminal 运行 sudo update-manager -d。

不过等了一会之后报了很多错,错误内容是:no suitable download server was found 很多地址 404 之类的。要我检查网络。
后来研究了好一阵子,找到一个解决方案。ubuntu software center 上面选 Edit -> software source, 更改了更新的地址。
结果还是不行。然后通过自动【select best server】也弹出来说没有合适的,要我检查网络。

最后只能求助谷歌。找到如下方法:

sudo sed -i -e 's/archive.ubuntu.com|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list
sudo apt-get update && sudo apt-get dist-upgrade
sudo update-manager -d。

这个方法貌似人气很高,但是我试过了没用。log 上一堆 404 提示跟开始差别不大。

后来继续看,老外说是版本太久了所以默认设置的更新地址都没法起作用。
我了个去,Ubuntu 12.04 不是说维护 5 年吗,我才用了 2 年啊喂!
所以,附上最后找到的办法:
1.打开更新配置文件

gksudo gedit /etc/apt/sources.list

2.在出来的文件里面从上往下找到一个不是以 # 开头的那行. 假设你的编辑器是 Karmic Koala (Ubuntu 9.10) 看起来像是这样的:

deb <网站url> karmic main restricted

这里, 就是你的默认更新请求的地址,例如:http://gb.archive.ubuntu.com/ubuntu
或者向博主这样:

deb http://mirrors.163.com/ubuntu/ quantal main restricted

这是博主刚刚用 software center 改了以后的更新地址。这其中的 http://mirrors.163.com/ubuntu/ 就是我们要修改的部分。

3.按下 Ctrl + H 使用 http://old-releases.ubuntu.com/ubuntu 来替换你当前的 ,

如果你的ubuntu也是英文,看起来就像这样:
Search for: 例如 http://mirrors.163.com/ubuntu/;
Replace with: http://old-releases.ubuntu.com/ubuntu
然后按下 Replace All (替换全部)

4.再来一遍 (博主没有做直接跳过也成功了)

Search for: http://security.ubuntu.com/ubuntu (该 url 用于所有版本的 Ubuntu)
Replace with: http://old-releases.ubuntu.com/ubuntu
然后按下 Replace All

5.保存文件,然后打开 terminal 运行:

sudo apt-get update
sudo update-manager -d。

等了一会,好的,software updater 终于正常了!点击 install now,慢慢开始更新了!

更新完之后重启,我从 12.04 升级到了 12.10。并没有直接从 12 升级到 14。好吧,不过 updater 也终于正常了。于是再重复最开始的操作。

按照官方文档,最初说是按下菜单键(win图标),然后输入 updater 选第一个就是了。会自动更新。
如果你找不到也可以用 terminal 运行 sudo update-manager -d。

然后提示我已经没有 12 版本的更新了,提示升级到 13.10。自然点升级。接下来一路这个流程就升级上去了。

援引问答地址:how to install software or upgrade from old unsupported release