乐趣区

关于c:程序人生-C语言字节对齐问题详解-对齐字节序位序网络序等上

本文首发于 2014-07-21 15:32:28

1. 引言

思考上面的构造体定义:

typedef struct{
    char  c1;
    short s; 
    char  c2; 
    int   i;
}T_FOO;

假如这个构造体的成员在内存中是紧凑排列的,且 c1 的起始地址是 0,则 s 的地址就是 1,c2 的地址是 3,i 的地址是 4。

当初,咱们编写一个简略的程序:

int main(void){  
    T_FOO a; 
    printf("c1 -> %d, s -> %d, c2 -> %d, i -> %d\n", 
          (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
          (unsigned int)(void*)&a.s  - (unsigned int)(void*)&a, 
          (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a, 
          (unsigned int)(void*)&a.i  - (unsigned int)(void*)&a); 
    return 0;
}

运行后输入:

c1 -> 0, s -> 2, c2 -> 4, i -> 8

为什么会这样?这就是字节对齐导致的问题。

本文在参考诸多材料的根底上,具体介绍常见的字节对齐问题。因成文较早,材料起源大多已不可考,敬请体谅。

2. 什么是字节对齐

古代计算机中,内存空间依照字节划分,实践上能够从任何起始地址拜访任意类型的变量,但实际上在拜访特定类型变量时常常在特定的内存地址拜访,这就须要各种类型数据依照肯定的规定在空间上排列,而不是一个接一个地程序寄存,这就是对齐。

3. 对齐的起因和作用

  1. 不同硬件平台对存储空间的解决上存在很大的不同。某些平台对特定类型的数据只能从特定地址开始存取,而不容许其在内存中任意寄存。例如 Motorola 68000 处理器不容许 16 位的字寄存在奇地址,否则会触发异样,因而在这种架构下编程必须保障字节对齐。
  2. 如果不依照平台要求对数据寄存进行对齐,会带来存取效率上的损失 。比方 32 位的 Intel 处理器通过总线拜访(包含读和写) 内存数据。每个总线周期从偶地址开始拜访 32 位内存数据,内存数据以字节为单位寄存。如果一个 32 位的数据没有寄存在 4 字节整除的内存地址处,那么处理器就须要 2 个总线周期对其进行拜访,显然拜访效率降落很多。因而,通过正当的内存对齐能够进步拜访效率。 为使 CPU 可能对数据进行快速访问,数据的起始地址应具备“对齐”个性。比方 4 字节数据的起始地址应位于 4 字节边界上,即起始地址可能被 4 整除。
  3. 正当利用字节对齐还能够无效地节俭存储空间 。但要留神,在 32 位机中应用 1 字节或 2 字节对齐,反而会升高变量访问速度,因而, 须要思考处理器类型 。同时, 还应思考编译器的类型 在 VC/C++ 和 GNU GCC 中都是默认是 4 字节对齐

4. 对齐的分类和准则

本大节次要基于 Intel X86 架构介绍构造体对齐和栈内存对齐,位域 实质上为构造体类型。

对于 Intel X86 平台,每次分配内存应该是从 4 的整数倍地址开始调配,无论是对构造体变量还是简略类型的变量。

4.1. 构造体对齐

在 C 语言中,构造体是种复合数据类型,其形成元素既能够是根本数据类型(int、long、float 等)的变量,也能够是一些复合数据类型(数组、构造体、联结等)的数据单元。编译器为构造体的每个成员依照其天然边界(alignment)调配空间。各成员依照它们被申明的程序在内存中顺序存储,第一个成员的地址和整个构造的地址雷同。

字节对齐的问题次要就是针对构造体。

4.1.1. 简略示例

先看个简略的例子(32 位,X86 处理器,GCC 编译器):

【例 1】假如构造体定义如下:

struct A{
    int    a;
    char   b;
    short  c;
};

struct B{
    char   b;
    int    a;
    short  c;
};

已知 32 位机器上各数据类型的长度为:char 为 1 字节、short 为 2 字节、int 为 4 字节、long 为 4 字节、float 为 4 字节、double 为 8 字节。那么下面两个构造体大小如何呢?

后果是:sizeof(strcut A)值为 8;sizeof(struct B)的值却是 12

构造体 A 和 B 中字段一样,蕴含一个 4 字节的 int 数据,一个 1 字节 char 数据和一个 2 字节 short 数据,只是程序不同。按理说 A 和 B 大小应该都是 7 字节,之所以呈现上述后果,就是因为编译器要对数据成员在空间上进行对齐。

4.1.2. 对齐准则

先来看四个重要的基本概念:

1) 数据类型本身的对齐值:char 型数据本身对齐值为 1 字节,short 型数据为 2 字节,int/float 型为 4 字节,double 型为 8 字节。

2) 构造体或类的本身对齐值 其成员中本身对齐值最大的那个值

3) 指定对齐值#pragma pack (value) 指定对齐值 value。

4) 数据成员、构造体和类的无效对齐值 :本身对齐值和指定对齐值中较小者,即 无效对齐值 =min{本身对齐值,以后指定的 pack 值}

基于下面这些准则,就能够不便地探讨具体数据结构的成员和其本身的对齐形式。

其中,无效对齐值 N 是最终用来决定数据寄存地址形式的值。无效对齐值 N 示意“对齐在 N 上”,即该数据的 寄存起始地址 % N = 0。而数据结构中的数据变量都是按定义的先后顺序寄存。第一个数据变量的起始地址就是数据结构的起始地址。构造体的成员变量要对齐寄存,构造体自身也要依据本身的无效对齐值圆整 (即 构造体成员变量占用总长度为构造体无效对齐值的整数倍)。

以此剖析 3.1.1 节中的构造体 B:

假如 B 从地址空间 0x0000 开始寄存,且指定对齐值默认为 4(4 字节对齐)。成员变量 b 的本身对齐值是 1,比默认指定对齐值 4 小,所以其无效对齐值为 1,其寄存地址 0x0000 合乎 0x0000%1=0。

成员变量 a 本身对齐值为 4,所以无效对齐值也为 4,只能寄存在起始地址为 0x0004~0x0007 四个间断的字节空间中,合乎 0x0004%4= 0 且紧靠第一个变量。

变量 c 本身对齐值为 2,所以无效对齐值也是 2,可寄存在 0x0008~0x0009 两个字节空间中,合乎 0x0008%2=0。

所以从 0x0000~0x0009 寄存的都是 B 内容。

再看数据结构 B 的本身对齐值为其变量中最大对齐值(这里是 b),也就是 4,所以构造体的无效对齐值也是 4。依据构造体圆整的要求,0x0000~0x0009=10 字节,(10+2)%4=0

所以 0x0000A~0x000B 也为构造体 B 所占用。故 B 从 0x0000 到 0x000B,共有 12 个字节,sizeof(struct B)=12。

之所以编译器在前面补充 2 个字节,是为了实现构造数组的存取效率。试想如果定义一个构造 B 的数组,那么第一个构造起始地址是 0 没有问题,然而第二个构造呢?

依照数组的定义,数组中所有元素都紧挨着。如果咱们不把构造体大小补充为 4 的整数倍,那么下一个构造的起始地址将是 0x0000A,这显然不能满足构造的地址对齐。因而要把构造体补充成无效对齐大小的整数倍。

其实对于 char/short/int/float/double 等已有类型的本身对齐值也是基于数组思考的,只是因为这些类型的长度已知,所以他们的本身对齐值也就已知。

下面的概念十分便于了解,不过集体还是更喜爱上面的对齐准则。

构造体字节对齐的细节和具体编译器实现相干,但一般而言满足三个准则:

  1. 构造体变量的首地址可能被其最宽根本类型成员的大小所整除;
  2. 构造体每个成员绝对构造体首地址的偏移量 (offset) 都是成员大小的整数倍,如有须要编译器会在成员之间加上填充字节(internal adding);
  3. 构造体的总大小为构造体最宽根本类型成员大小的整数倍,如有须要编译器会在最末一个成员之后加上填充字节{trailing padding}。

对于以上规定的阐明如下:

  • 第一条:编译器在给构造体开拓空间时,首先找到构造体中最宽的根本数据类型,而后寻找内存地址能被该根本数据类型所整除的地位,作为构造体的首地址。将这个最宽的根本数据类型的大小作为下面介绍的对齐模数
  • 第二条:为构造体的一个成员开拓空间之前,编译器首先查看预开拓空间的首地址绝对于构造体首地址的偏移是否是本成员大小的整数倍,若是,则寄存本成员,反之,则在本成员和上一个成员之间填充肯定的字节,以达到整数倍的要求,也就是将预开拓空间的首地址后移几个字节。
  • 第三条:构造体总大小是包含填充字节,最初一个成员满足下面两条以外,还必须满足第三条,否则就必须在最初填充几个字节以达到本条要求。

【例 2】假如 4 字节对齐,以下程序的输入后果是多少?

/* OFFSET 宏定义可获得指定构造体某成员在构造体外部的偏移 */
#define OFFSET(st, field) (size_t)&(((st*)0)->field)
typedef struct{
    char  a;
    short b;
    char  c;
    int   d;
    char  e[3];
}T_Test;
 
int main(void){printf("Size = %d\n  a-%d, b-%d, c-%d, d-%d\n  e[0]-%d, e[1]-%d, e[2]-%d\n",
           sizeof(T_Test), OFFSET(T_Test, a), OFFSET(T_Test, b),
           OFFSET(T_Test, c), OFFSET(T_Test, d), OFFSET(T_Test, e[0]),
           OFFSET(T_Test, e[1]),OFFSET(T_Test, e[2]));
    return 0;
}

执行后输入如下:

Size = 16
a-0, b-2, c-4, d-8
e[0]-12, e[1]-13, e[2]-14

上面来具体分析:

首先 char a 占用 1 个字节,没问题。

short b 自身占用 2 个字节,依据下面准则 2,须要在 b 和 a 之间填充 1 个字节。

char c 占用 1 个字节,没问题。

int d 自身占用 4 个字节,依据准则 2,须要在 d 和 c 之间填充 3 个字节。

char e[3];自身占用 3 个字节,依据准则 3,须要在其后补充 1 个字节。

因而,sizeof(T_Test) = 1 + 1 + 2 + 1 + 3 + 4 + 3 + 1 = 16 字节。

4.1.3. 对齐的隐患

4.1.3.1. 数据类型转换

代码中对于对齐的隐患,很多是隐式的。例如,在强制类型转换的时候:

int main(void){  
    unsigned int i = 0x12345678;
    unsigned char *p = (unsigned char *)&i;
    *p = 0x00;
    unsigned short *p1 = (unsigned short *)(p+1);
    *p1 = 0x0000;
    return 0;
}

最初两句代码,从奇数边界去拜访 unsigned short 型变量,显然不合乎对齐的规定。在 X86 上,相似的操作只会影响效率;但在 MIPS 或者 SPARC 上可能导致 error,因为它们要求必须字节对齐

又如对于 3.1.1 节的构造体 struct B,定义如下函数:

void Func(struct B *p){//Code}

在函数体内如果间接拜访 p->a,则很可能会异样。因为 MIPS 认为 a 是 int,其地址应该是 4 的倍数,但 p ->a 的地址很可能不是 4 的倍数。

如果 p 的地址不在对齐边界上就可能出问题,比方 p 来自一个跨 CPU 的数据包(多种数据类型的数据被按程序搁置在一个数据包中传输),或 p 是通过指针移位算进去的。因而要特地留神跨 CPU 数据的接口函数对接口输出数据的解决,以及指针移位再强制转换为构造指针进行拜访时的安全性。

解决形式如下:

  1. 定义一个此构造的局部变量,用 memmove 形式将数据拷贝进来。

    void Func(struct B *p){
     struct B tData;
     memmove(&tData, p, sizeof(struct B));
      // 尔后可平安拜访 tData.a,因为编译器已将 tData 调配在正确的起始地址上
    }

    留神:如果能确定 p 的起始地址没问题,则不须要这么解决;如果不能确定(比方跨 CPU 输出数据、或指针移位运算进去的数据),则须要这样解决。

  2. #pragma pack (1) 将 STRUCT_T 定义为 1 字节对齐形式。

4.1.3.2. 处理器间数据通信

处理器间通过音讯(对于 C /C++ 而言就是构造体)进行通信时,须要留神字节对齐以及字节序的问题。

大多数编译器提供一些内存选项供用户应用。这样用户能够依据处理器的状况抉择不同的字节对齐形式。例如:C/C++ 编译器提供的 #pragma pack(n) n=1,2,4 等,让编译器在生成指标文件时,使内存数据依照指定的形式排布在 1,2,4 等字节整除的内存地址处

然而在不同编译平台或处理器上,字节对齐会造成音讯构造长度的变动。编译器为了使字节对齐可能会对音讯构造体进行填充,不同编译平台可能填充为不同的模式,大大增加处理器间数据通信的危险。

上面以 32 位处理器为例,提出一种内存对齐办法以解决上述问题。

对于本地应用的数据结构,为进步内存拜访效率,采纳 4 字节对齐形式;同时为了缩小内存的开销,合理安排构造体成员的地位,缩小 4 字节对齐导致的成员之间的空隙,升高内存开销。

对于处理器之间的数据结构,须要保障音讯长度不会因不同编译平台或处理器而导致音讯构造体长度发生变化,应用 1 字节对齐形式对音讯构造进行压缩;为保障处理器之间的音讯数据结构的内存拜访效率,采纳字节填充的形式本人对音讯中成员进行 4 字节对齐。

数据结构的成员地位要兼顾成员之间的关系、数据拜访效率和空间利用率。程序安顿准则是:4 字节的放在最后面,2 字节的紧接最初一个 4 字节成员,1 字节紧接最初一个 2 字节成员,填充字节放在最初。

举例如下:

typedef struct tag_T_MSG{
    long  ParaA;
    long  ParaB;
    short ParaC;char  ParaD;
    char  Pad;   // 填充字节
}T_MSG;

4.1.3.3. 排查对齐问题

如果呈现对齐或者赋值问题,可查看:

  1. 编译器的字节序大小端设置;
  2. 处理器架构自身是否反对非对齐拜访;

    如果反对,则看是否设置对齐;

    如果没有,则看拜访时是否须要加某些非凡的润饰来标记其非凡拜访操作。

4.1.4. 更改对齐形式

次要是 更改 C 编译器的缺省字节对齐形式

在缺省状况下,C 编译器为每一个变量或是数据单元按其天然对界条件调配空间。个别地,能够通过上面的办法来扭转缺省的对界条件:

  • 应用 伪指令 #pragma pack(n):C 编译器将依照 n 个字节对齐;
  • 应用 伪指令 #pragma pack():勾销自定义字节对齐形式。

另外,还有如下的一种形式(GCC 特有语法):

  • __attribute__((aligned (n))):让所作用的构造成员对齐在 n 字节天然边界上。如果构造体中有成员的长度大于 n,则依照最大成员的长度来对齐。
  • __attribute__((packed)):勾销构造在编译过程中的优化对齐,依照理论占用字节数进行对齐。

留神:

__attribute__机制是 GCC 的一大特色,能够设置函数属性 (Function Attribute)、变量属性(Variable Attribute) 和类型属性(Type Attribute)。

在编码时,可用 #pragma pack 动静批改对齐值。具体语法阐明见附录 5.3 节。

自定义对齐值后要用 #pragma pack() 来还原,否则会对前面的构造造成影响。

【例 3】剖析如下构造体 C:

#pragma pack(2)  // 指定按 2 字节对齐
struct C{
    char  b;
    int   a;
    short c;
};
#pragma pack()   // 勾销指定对齐,复原缺省对齐

变量 b 本身对齐值为 1,指定对齐值为 2,所以无效对齐值为 1,假如 C 从 0x0000 开始,则 b 寄存在 0x0000,合乎 0x0000%1=0;

变量 a 本身对齐值为 4,指定对齐值为 2,所以无效对齐值为 2,程序寄存在 0x0002~0x0005 四个间断字节中,合乎 0x0002%2=0。

变量 c 的本身对齐值为 2,所以无效对齐值为 2,程序寄存在 0x0006~0x0007 中,合乎 0x0006%2=0。

所以从 0x0000 到 0x00007 共 8 字节寄存的是 C 的变量。

C 的本身对齐值为 4,所以其无效对齐值为 2。又 8%2=0,C 只占用 0x0000~0x0007 的八个字节。所以 sizeof(struct C)=8。

留神:构造体对齐到的字节数并非齐全取决于以后指定的 pack 值,例如:

#pragma pack(8)
struct D{
    char  b;
    short a;
    char  c;
};
#pragma pack()

尽管 #pragma pack(8),但仍然依照 2 字节对齐,所以 sizeof(struct D) 的值为 6。所以, 对齐到的字节数 =min{以后指定的 pack 值,最大成员大小}

另外,GNU GCC 编译器中按 1 字节对齐可写为以下模式:

#define GNUC_PACKED __attribute__((packed))
struct C{
    char  b;
    int   a;
    short c;
}GNUC_PACKED;

此时 sizeof(struct C) 的值为 7。

4.2. 栈内存对齐

在 VC/C++ 中,栈的对齐形式不受构造体成员对齐选项的影响,总是放弃对齐在 4 字节边界上

【例 4】剖析栈内存对齐形式:

#pragma pack(push, 1)  // 前面可改为 1, 2, 4, 8
struct StrtE{
    char m1;
    long m2;
};
#pragma pack(pop)
 
int main(void){  
    char a;
    short b;
    int c;
    double d[2];
    struct StrtE s;
        
    printf("a    address:   %p\n", &a);
    printf("b    address:   %p\n", &b);
    printf("c    address:   %p\n", &c);
    printf("d[0] address:   %p\n", &(d[0]));
    printf("d[1] address:   %p\n", &(d[1]));
    printf("s    address:   %p\n", &s);
    printf("s.m2 address:   %p\n", &(s.m2));
    return 0;
}

后果如下:

a    address:   0xbfc4cfff
b    address:   0xbfc4cffc
c    address:   0xbfc4cff8
d[0] address:   0xbfc4cfe8
d[1] address:   0xbfc4cff0
s    address:   0xbfc4cfe3
s.m2 address:   0xbfc4cfe4

能够看出都是对齐到 4 字节,并且后面的 char 和 short 并没有被凑在一起(成 4 字节),这和构造体内的解决是不同的。

至于为什么输入的地址值是变小的,这是因为该平台下的栈是倒着“成长”的。

4.3. 位域对齐

4.3.1. 位域定义

有些信息在存储时,并不需要占用一个残缺的字节,而只需占几个或一个二进制位。例如在寄存一个开关量时,只有 0 和 1 两种状态,用一位二进位即可。为了节俭存储空间和解决简便,C 语言提供了一种数据结构,称为 位域 位段

位域是一种非凡的构造成员或联结成员(即只能用在构造或联结中),用于指定该成员在内存存储时所占用的位数,从而在机器内更紧凑地示意数据。每个位域有一个域名,容许在程序中按域名操作对应的位,这样就可用一个字节的二进制位域来示意几个不同的对象。

位域定义与构造定义相似,其模式为:

struct 位域构造名
       {位域列表};

其中位域列表的模式为:

类型说明符位域名:位域长度

位域的应用和构造成员的应用雷同,其个别模式为:

位域变量名. 位域名

位域容许用各种格局输入。

位域在实质上就是一种构造类型,不过其成员是按二进位调配的。位域变量的阐明与构造变量阐明的形式雷同,可先定义后阐明、同时定义阐明或间接阐明。

位域的应用次要为上面两种状况:

  1. 当机器可用内存空间较少而应用位域可大量节俭内存时。例如:把构造作为大数组的元素时。
  2. 当须要把一构造体或联结映射成某预约的组织构造时。例如:须要拜访字节内的特定位时。

4.3.2. 对齐准则

位域成员不能独自被取 sizeof 值。上面次要探讨含有位域的构造体的 sizeof。

C99 规定 int、unsigned int 和 bool 能够作为位域类型,但编译器简直都对此作了扩大,容许其它类型的存在。位域作为嵌入式零碎中十分常见的一种编程工具,长处在于压缩程序的存储空间。

其对齐规定大抵为:

  1. 如果相邻位域字段的类型雷同,且其位宽之和小于类型的 sizeof 大小,则前面的字段将紧邻前一个字段存储,直到不能包容为止;
  2. 如果相邻位域字段的类型雷同,但其位宽之和大于类型的 sizeof 大小,则前面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;
  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差别,VC6 采取不压缩形式,Dev-C++ 和 GCC 采取压缩形式
  4. 如果位域字段之间穿插着非位域字段,则不进行压缩;
  5. 整个构造体的总大小为最宽根本类型成员大小的整数倍,而位域则依照其最宽类型字节数对齐

【例 5】

struct BitField{
    char element1  : 1;
    char element2  : 4;
    char element3  : 5;
};

位域类型为 char,第 1 个字节仅能包容下 element1 和 element2,所以 element1 和 element2 被压缩到第 1 个字节中,而 element3 只能从下一个字节开始。因而 sizeof(BitField) 的后果为 2。

【例 6】

struct BitField1{
    char element1   : 1;
    short element2  : 5;
    char element3   : 7;
};

因为相邻位域类型不同,在 VC6 中其 sizeof 为 6,在 Dev-C++ 中为 2。

【例 7】

struct BitField2{
    char element1  : 3;
    char element2  ;
    char element3  : 5;
};

非位域字段穿插在其中,不会产生压缩,在 VC6 和 Dev-C++ 中失去的大小均为 3。

【例 8】

struct StructBitField{
    int element1   : 1;
    int element2   : 5;
    int element3   : 29;
    int element4   : 6;
    char element5  :2;
    char stelement;  // 在含位域的构造或联结中也可同时阐明一般成员
};

位域中最宽类型 int 的字节数为 4,因而构造体按 4 字节对齐,在 VC6 中其 sizeof 为 16。

4.3.3. 注意事项

对于位域操作有几点须要留神:

1)位域的地址不能拜访,因而不容许将 & 运算符用于位域。不能应用指向位域的指针也不能应用位域的数组(数组是种非凡指针)。例如,scanf 函数无奈间接向位域中存储数据:

int main(void){  
    struct BitField1 tBit;
    scanf("%d", &tBit.element2); //error: cannot take address of bit-field 'element2'
    return 0;
}

可用 scanf 函数将输出读入到一个一般的整型变量中,而后再赋值给 tBit.element2。

2)位域不能作为函数返回的后果

3)位域以定义的类型为单位,且位域的长度不可能超过所定义类型的长度。例如:定义 int a:33 是不容许的。

4)位域能够不指定位域名,但不能拜访无名的位域

位域能够无位域名,只用作填充或调整地位,占位大小取决于该类型。例如,char :0 示意整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始寄存,同理 short :0int :0 别离示意整个位域向后推两个和四个字节。

当空位域的长度为具体数值 N 时(如 int :2),该变量仅用来占位 N 位。

【例 9】

struct BitField3{
    char element1  : 3;
    char  :6;
    char element3  : 5;
};

构造体大小为 3。因为 element1 占 3 位,前面要保留 6 位而 char 为 8 位,所以保留的 6 位只能放到第 2 个字节。同样 element3 只能放到第 3 字节。

struct BitField4{
    char element1  : 3;
    char  :0;
    char element3  : 5;
};

长度为 0 的位域通知编译器将下一个位域放在一个存储单元的起始地位。如上,编译器会给成员 element1 调配 3 位,接着跳过余下的 4 位到下一个存储单元,而后给成员 element3 调配 5 位。所以,下面的构造体大小为 2。

5)位域的示意范畴:

  • 位域的赋值不能超过其能够示意的范畴。
  • 位域的类型决定该编码能示意的值的后果。

对于第二点,若位域为 unsigned 类型,则间接转化为负数;若非 unsigned 类型,则先判断最高位是否为 1,若为 1,则示意补码,则对其除符号位外的所有位取反再加一失去最初的后果数据(原码)。例如:

unsigned int p:3 = 111;   // p 示意 7
int p:3 = 111;            //p 示意 -1,对除符号位之外的所有位取反再加一

6)带位域的构造在内存中各个位域的存储形式取决于编译器,既可从左到右也可从右到左存储。

【例 10】在 VC6 下执行上面的代码:

int main(void){  
    union{
        int i;
        struct{
            char a : 1;
            char b : 1;
            char c : 2;
        }bits;
    }num;
 
    printf("Input an integer for i(0~15):");
    scanf("%d", &num.i);
    printf("i = %d, cba = %d %d %d\n", num.i, num.bits.c, num.bits.b, num.bits.a); 
    return 0;

输出 i 值为 11,则输入为 i = 11, cba = -2 -1 -1。

Intel x86 处理器按小字节序存储数据,所以 bits 中的位域在内存中搁置程序为 ccba。当 num.i 置为 11 时,bits 的最低无效位 (即位域 a) 的值为 1,a、b、c 按低地址到高地址别离存储为 10、1、1(二进制)。

但为什么最初的打印后果是 a =- 1 而不是 1?

因为位域 a 定义的类型 signed char 是有符号数,所以只管 a 只有 1 位,仍要进行符号扩大。1 做为补码存在,对应原码 -1。

如果将 a、b、c 的类型定义为 unsigned char,即可失去 cba = 2 1 1。1011 即为 11 的二进制数。

注:C 语言中,不同的成员应用独特的存储区域的数据构造类型称为联结(或共用体)。联结占用空间的大小取决于类型长度最大的成员。联结在定义、阐明和应用模式上与构造体类似。

7)位域的实现会因编译器的不同而不同,应用位域会影响程序可移植性。因而如无必要,最好不要应用位域。

8)只管应用位域能够节俭内存空间,但却减少了解决工夫。当拜访各个位域成员时,须要把位域从它所在的字中合成进去或反过来把一值压缩存到位域所在的字位中。

5. 总结

让咱们回到引言局部的问题。

缺省状况下,C/C++ 编译器默认将构造、栈中的成员数据进行内存对齐。因而,引言程序输入就变成 ”c1 -> 0, s -> 2, c2 -> 4, i -> 8″。

编译器将未对齐的成员向后移,将每一个都成员对齐到天然边界上,从而也导致整个构造的尺寸变大。只管会就义一点空间(成员之间有空洞),但进步了性能。

也正是这个起因,引言例子中 sizeof(T_ FOO)为 12,而不是 8。

总结说来,就是:在构造体中,综合思考变量自身和指定的对齐值;在栈上,不思考变量自身的大小,对立对齐到 4 字节

阐明: 本文转载自 https://www.cnblogs.com/clove…


欢送关注我的微信公众号【数据库内核】:分享支流开源数据库和存储引擎相干技术。

题目 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/…
思否(SegmentFault) https://segmentfault.com/u/db…
掘金 https://juejin.im/user/5e9d3e…
开源中国(oschina) https://my.oschina.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel
退出移动版