设为首页 加入收藏

TOP

C语言编程笔记丨失落的C语言结构体封装艺术(二)
2019-03-30 00:08:19 】 浏览:280
Tags:语言编程 笔记 失落 语言 结构 封装 艺术
上是3,64位机器上是7。

如果你想让那些变量占用更少的空间,你可以通过交换原序列中的x和c来达到效果。

char *p;     /* 8 bytes */

long x;      /* 8 bytes */

char c;      /* 1 byte

通常,对于C程序里少数的简单变量,你可以通过调整声明顺序来压缩掉极少几个字节数,不会有显著的节约。但当用于非标量变量(nonscalar variables),尤其是结构体时,这项技术会变得更有趣。

在我们讲到非标量变量之前,让我们讲一下标量数组。在一个有自对齐类型的平台上,字符、短整型、整型、长整型、指针数组没有内部填充。每个成员会自动自对齐到上一个之后(译者注:原文 self-aligned at the end of the next one 似有误)。

在下一章,我们会看到对于结构体数组,一样的规则并不一定正确。

 

5. 结构体的对齐和填充

总的来说,一个结构体实例会按照它最宽的标量成员对齐。编译器这样做,把它作为最简单的方式来保证所有成员是自对齐,为了快速访问的目的。

而且,在C语言里,结构体的地址与它第一个成员的地址是相同的——没有前置填充。注意:在C++里,看上去像结构体的类可能不遵守这个规则!(遵不遵守依赖于基类和虚拟内存函数如何实现,而且因编译器而不同。)

(当你不能确定此类事情时,ANSI C提供了一个offsetof()宏,能够用来表示出结构体成员的偏移量。)

考虑这个结构体:

struct foo1 {

    char *p;

    char c;

    long x;

};

假设一台64位的机器,任何struct foo1的实例会按8字节对齐。其中的任何一个的内存分布看上去无疑应该像这样:

struct foo1 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte

    char pad[7]; /* 7 bytes */

    long x;      /* 8 bytes */

};

它的分布就恰好就像这些类型的变量是单独声明的。但是如果我们把c放在第一个,这就不是了。

struct foo2 {

    char c;      /* 1 byte */

    char pad[7]; /* 7 bytes */

    char *p;     /* 8 bytes */

    long x;      /* 8 bytes */

};

如果成员是单独的变量,c可以起始于任何字节边界,并且pad的大小会不同。但因为struct foo2有按其最宽成员进行的指针对齐,那就不可能了。现在c必须于指针对齐,之后7个字节的填充就被锁定了。

现在让我们来说说关于在结构体成员的尾随填充(trailing padding)。要解释这个,我需要介绍一个基本概念,我称之为结构体的跨步地址(stride address)。它是跟随结构体数据后的第一个地址,与结构体拥有同样对齐方式。

结构体尾随填充的通常规则是这样的:编译器的行为就如把结构体尾随填充到它的跨步地址。这条规则决定了sizeof()的返回值。

考虑在64位的x86或ARM上的这个例子:

struct foo3 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte */

};

 

struct foo3 singleton;

struct foo3 quad[4];

你可能会认为,sizeof(struct foo3)应该是9,但实际上是16。跨步地址是(&p)[2]的地址。如此,在quad数组中,每个成员有尾随填充的7字节,因为每个跟随的结构体的第一个成员都要自对齐到8字节的边界上。内存分布就如结构体像这样声明:

struct foo3 {

    char *p;     /* 8 bytes */

    char c;      /* 1 byte */

    char pad[7];

};

作为对照,考虑下面的例子:

struct foo4 {

    short s;     /* 2 bytes */

    char c;      /* 1 byte */

};

因为s只需对齐到2字节, 跨步地址就只有c后面的一个字节,struct foo4作为一个整体,只需要一个字节的尾随填充。它会像这样分布

struct foo4 {

    short s;     /* 2 bytes */

    char c;      /* 1 byte */

    char pad[1];

};

并且sizeof(struct foo4)会返回4。

现在让我们考虑位域(bitfield)。它们是你能够声明比字符宽度还小的结构体域,小到1位,像这样:

struct foo5 {

    short s;

    char c;

    int flip:1;

    int nybble:4;

    int septet:7;

};

关于位域需要知道的事情是,它们以字或字节级别的掩码和移位指令来实现。从编译器的观点来看,struct foo5的位域看上去像2字节,16位的字符数组里只有12位被使用。接着是填充,使得这个结构体的字节长度成为sizeof(short)的倍数即最长成员的大小。

struct foo5 {

    short s;       /* 2 bytes */

    char c;        /* 1 byte */

    int flip:1;    /* total 1 bit */

    int nybble:4;  /* total 5 bits */

    int septet:7;  /* total 12 bits */

    int pad1:4;    /* total 16 bits = 2 bytes */

    char pad2;     /* 1 byte */

};

这里是最后一个重要的细节:如果你的结构体含有结构体的成员,里面的结构体也需要按最长的标量对齐。假设如果你写成这样:

struct foo6 {

    char c;

    struct foo5 {

        char *p;

        short x;

    } inner;

};

内部结构体的char *p成员使得外部的结构体与内部的一样成为指针对齐。在64位机器上,实际的分布是像这样的:

struct foo6 {

    char c;           /* 1 byte*/

    char pad1[7];     /* 7 bytes */

    struct foo6_inner {

        char *p;      /* 8 bytes */

        short x;      /* 2 bytes */

        char pad2[6]; /* 6 bytes */

    } inner;

};

这个结构体给了我们一个启示,重新封装结构体可能节省空间。24个字节中,有13个字节是用作填充的。超过50%的无用空间!

 

6. 结构体重排序(reordering)

现在你知道如何以及为何编译器要插入填充,在你的结构体之中或者之后,我们要考察你可以做些什么来挤掉这些“水坑”。这就是结构体封装的艺术。

第一件需要注意的事情是,“水坑”仅发生于两个地方。一个是大数据类型(有更严格的对齐要求)的存储区域紧跟在一个较小的数据类型的存储区域之后。另一个是结构体自然结束于它的跨步地址之前,需要填充,以使下一个实例可以正确对齐。

消除“水坑”的最简单的方法是按对齐的降序来对结构体成员重排序。就是说:所有指针对齐的子域在前面,因为在64位的机器上,它们会有8字节

首页 上一页 1 2 3 4 下一页 尾页 2/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇C语言编程笔记丨如何理解指向指针.. 下一篇C语言编程笔记丨使用C编译器编写s..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目