标签 思考 下的文章

也谈C语言的Struct Hack

今天在浏览网友huangz编写的“Redis源码分析”时,看到如下redis中的代码:

struct sdshdr {
    int len;
    int free;
    char buf[];
};

说实话,这类代码我见过很多,但直到这次我才知道这种coding trick的真实英文称谓是:Struct Hack。

到底什么是Struct Hack?其实倒也没有什么明确定义。首先它是一种coding trick;其次一定是与struct相关的;关键是struct中要仅有一个变长的字段,且该字段是struct中最后的一个字段,就像上面 sdshdr中的buf那样。这样的coding trick到底有何作用呢?

我们来看看redis中是如何利用这种coding trick的。sds是redis string的一种实现,全称是Simple Dynamic Strings,从字面意义来看,这是一种动态字符串,是可以在运行时确定其大小并创建的。我们来看看其创建代码:

typedef char *sds;

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    if (init) {
        sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
    }

    if (sh == NULL) return NULL;

    sh->len = initlen;
    sh->free = 0;

    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';

    return (char*)sh->buf;
}

sdsnewlen在分配内存时,一次分配的内存大小不仅仅是sizeof(struct sdshdr),而是加上了真正存储字符串的buf的大小,并将buf作为返回值返回,sds就是buf,buf就是sds。这样通过sdshdr实例, 我们可以直接获得其对应的sds,也就是buf。更为关键的一点是,如果我已知sds,我们还可以获得其对应的sdshdr(huangz在文中称 sdshdr是sds handler的缩写,我倒是觉得hdr更像是header的缩写),见下面代码:

static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
    return sh->len;
}

这种trick给代码带来的极大的效率。想象一下如果redis的sdshdr定义是这样的:

struct sdshdr {
    int len;
    int free;
    char *buf;
};

/*  sdsnewlen */
struct sdshdr *sh;
sh = zmalloc(sizeof(struct sdshdr));
memset(sh, 0, sizeof(*sh));
sh->buf = zmalloc(initlen+1);

看起来似乎也能在运行时实现buf的动态size指定,但sdshdr与sds之间的纽带就被彻底割裂了(当然你也可以在 malloc sh时将buf内存也一并分配出来,然后手工将buf指向struct外的内存首地址,不过一旦这么做,就显得不那么tricky了)。

另外这里要探讨的是最后那个字段buf,是声明为buf[]好,还是buf[0]好,又或是buf[1]呢?redis使用的是buf[],在C99中这 是绝对合法的,这种定义被称为variable-length arrays(变长数组)。由于下标为空,这里的buf就好像是一个占位符,只有符号意义,但却并不实际占用空间。32bit平台下 sizeof(struct sdshdr) = 8,显然没有buf的份儿。不过在C99以前的标准中,是不允许变长数组出现的,你的Gcc很可能出现如下警告:“ISO C90 不允许可变数组成员”。不过C99以前很多编译器的扩展默认都是支持变长数组的,这也是这种trick之前就大行其道的原因之一,只不过是在C99之后变 得名正言顺了罢了。

如果将buf[]改为buf[0]呢?在C99以及支持变长数组扩展的编译器下也都是等同于buf[]的,不过C99以前的标准编译器还是会警告:ISO C 不允许大小为 0 的数组‘buf’ [-pedantic]。

用buf[1]替代buf[]则是一个兼容性最好的方案。在一些其他开源代码中,你也会常见buf[1]这种情形,如果以redis hds代码为例,我们用buf[1]替代buf[0]:

struct sdshdr {
    int len;
    int free;
    char buf[1];
};

相应的,sdsnewlen的代码以及sdslen中通过sds获取sdshdr的代码就应该做相应的修改了,简要修改如下:

/* sdsnewlen */

sds sdsnewlen(const void *init, size_t initlen) {
    struct sdshdr *sh;

    if (init) {
        sh = zmalloc(sizeof(struct sdshdr) – 1 + initlen + 1);
    } else {
        sh = zcalloc(sizeof(struct sdshdr) – 1 + initlen + 1);
    }

    if (sh == NULL) return NULL;

    sh->len = initlen;
    sh->free = 0;

    if (initlen && init)
        memcpy(sh->buf, init, initlen);
    sh->buf[initlen] = '\0';

    return (char*)sh->buf;
}


static inline size_t sdslen(const sds s) {
    struct sdshdr *sh = (void*)(s-(offsetof(struct sdshdr, buf)));
    return sh->len;
}

注意:使用这种coding trick为的就是获得一种运行时的动态行为,struct的大小也是动态的(这种struct的声明是一种incomplete type),所以这种struct都是在堆上分配内存的,在栈上分配显然是没有标准可移植的方法的;同样,由于是size不确定的incomplete type,这种struct一般不用于声明struct数组。

果果的蛇年春节独白

我叫果果,现在两岁零9个月了。我的身高快到1米了,人家都说我长得又高又大^_^,我比邻居家的小哥哥还要高,要知道他可比我年长8个月呢。

最近我很开心,因为我和爸爸妈妈一起回老家与爷爷奶奶过春节了。春节是什么,我还不是很清楚。但我的印象中一到春节周围的人都很开心,家家户户都挂上了红灯笼,门上都贴着福字和对联,还燃放那种让我怕怕的很响的鞭炮。春节最让我高兴的是爸爸妈妈都不用去上班了,可以天天陪我一起玩,给我买好吃的,还有新衣服穿,新书看,新玩具玩^_^。春节前爸爸妈妈都很忙,每天都加班,很晚才会来,我可想他们了,每天都盼望他们早点下班回家。

听爸爸说,这是他结婚后第一次回爷爷奶奶家过年,也是第一次带我回家过年,他很高兴。我也高兴,因为我的语言能力已经有了很大提高了,我可以用语言表达我的意思了,可以和爷爷奶奶聊天了^_^,只是我的吐字还不那么清晰,也不知道我说的话爷爷奶奶能不能听懂^_^。

记得我是坐爸爸的车回老家的。爸爸的车开得好快,要比平时我看到的马路上的车快多了,据妈妈说这是高速公路,让我有些小兴奋哦。奶奶和爷爷老早就在门口等着我们呢。我一年到头与爷爷奶奶见面的次数都很有限,不过不知道为什么,一见到爷爷奶奶我就感到很亲切,很喜欢让他们抱抱我^_^。爷爷准备了我最爱吃的菜,那天我没让妈妈喂我,我是自己吃的,让爷爷奶奶看看我长大了,我吃的很饱。

除夕那天我和爸爸妈妈又去奶奶家了,听说除夕是一大家人团圆的日子。这回我们一家五口人算是真正团圆了。这天爷爷做的菜比以往都要多得多,我们都没有吃完,还剩了好多菜,爷爷做的菜真好吃。晚上我还吃到爷爷奶奶包的饺子了,特别香。大人说除夕夜都要吃饺子。我还不会包,长大一定和奶奶学,给他们包上一顿饺子,他们一定很开心。

初一。爸爸妈妈给我穿上了新衣服。爸爸妈妈一见到奶奶爷爷就行礼拜年,他们拜后,还让我拜。这可是我第一次拜年,还不是很熟练。不过之前姥姥教我好几遍了,于是我就说:“爷爷奶奶过年好,祝你们身体健康,长命百岁”。说完后,我看爷爷奶奶都特别高兴,还给我压岁钱了呢。

初二。爸爸妈妈带我去了爸爸的姥姥家。爸爸的姥姥家地方不大,但里面的人可真多,大家都微笑着瞅我,弄得我怪不好意思的。妈妈爸爸让我拜年,我就把姥姥教给我到拜年话说了很多遍,说得我口都渴了,不过好多人给我压岁钱,我还是蛮高兴的。让我更高兴的是我的两个小姐姐也来了,这样我就可以和她们一起玩了,我们一起玩走迷宫,一起玩涂鸦,好开心的。今天的午饭好丰盛哦,比除夕那天爷爷做的菜种类还要多出一倍。不过我是小孩,不能上桌,于是妈妈就把我爱吃的菜挑出来喂我^_^。我特别喜欢喝露露,不过每次我说要喝露露,他们都开心的笑,似乎我的发音不准,把“露露”发音成“怒怒”了^_^。

初三和初四两天我比较忙,一直都跟着爸爸妈妈到处走亲访友,所到之处我都成为了大家的关注焦点。

好不容易会一趟老家,我要多陪陪奶奶和爷爷,于是我初五一整天都和奶奶爷爷在一起。爷爷还给我炖了一只笨鸡,不过我还是喜欢菜里的蘑菇,因为鸡肉我嚼不烂:(。晚上爸爸还实现了一个他对我的承诺:放烟花给我看。爸爸说小烟花是不出响的,不用害怕。爸爸一共给我放了8个小烟花,都特别好看。

一转眼就到初六了,年过得真快啊,爸爸妈妈的假期要结束了,今天我们就要返回沈阳了。爷爷奶奶和姥爷给我们拿了好多东西,大包小裹的,我也不知道是啥,估计都是吃的吧,把爸爸车的后备箱装得满满登登的。车开走了,也不知道下次何时回来探望爷爷奶奶和姥爷。初六中午我们就回到沈阳的家里了。家里可真是热,我只穿一套衬衣衬裤还冒汗呢,爸爸说屋里温度有28度。由于这几天每天都有好吃的,我脸上都起痘痘了,爸爸说可能是吃鱼虾多过敏了,于是让我改吃青菜了。

初七。爸爸似乎还想多在家里陪陪我,于是他又请了一天假,我真高兴。妈妈还有两天假,我还可以与爸爸妈妈一起玩。

初八。爸爸上班了。不过妈妈在家。妈妈这两天一直守着电视,看那个叫“甄寰传”的电视剧,从这个台调到那个台,看得不亦乐乎。还好,昨天爸爸给我买了新拼插玩具,够我玩上一阵子的。

最近天气也转暖了。爸爸说春天快来了,外面的冰雪也开始融化了。姥姥说下个月又要送我上幼儿园了,在家里待了这么多天,不知道还能不能适应幼儿园的环境了。唉,焦虑啊。

下面是爸爸妈妈在春节期间用手机给我拍的一些照片,大家看看我是不是长大了^_^。

出水芙蓉^_^

今儿真高兴儿!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats