参考:http://www.cnblogs.com/longlybits/articles/2385343.html

结构体中的字节对齐

在用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题。从理论上讲,对于任何 变量的访问都可以从任何地址开始访问,但是事实上不是如此,实际上访问特定类型的变量只能在特定的地址访问,这就需要各个变量在空间上按一定的规则排列, 而不是简单地顺序排列,这就是内存对齐。

内存对齐的原因

  1. 某些平台只能在特定的地址处访问特定类型的数据;

  2. 提高存取数据的速度。比如有的平台每次都是从偶地址处读取数据,对于一个int型的变量,若从偶地址单元处存放,则只需一个读取周期即可读取该变量;但是若从奇地址单元处存放,则需要2个读取周期读取该变量。

win32平台下的微软C编译器对齐策略:

1. 结构体的首地址能够被其最宽数据类型成员的大小整除(由编译器控制)

编译器在为结构体变量开辟空间时,首先找到结构体中最宽的数据类型,然后寻找内存地址能被该数据类型大小整除的位置,这个位置作为结构体变量的首地址。而将最宽数据类型的大小作为对齐标准。

2. 每个成员相对结构体首地址的偏移量都是当前成员本身大小的整数倍

编译器在为结构体成员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为该成员大小的整数倍,若是,则存放该成员;若不是,则填充若干字节,以达到整数倍的要求。

3. 结构体所占空间的大小必定是最宽数据类型大小的整数倍

如有需要会在最后一个成员末尾填充若干字节使得所占空间大小是最宽数据类型大小的整数倍。

字节对齐实例

下面看一下sizeof在计算结构体大小的时候具体是怎样计算的

test1 空结构体

typedef struct node
{
     
}S;

则sizeof(S)=1;或sizeof(S)=0;

在C++中占1字节,而在C中占0字节。

test2

	typedef struct node1
	{
	 	int a;
	 	char b;
	 	short c;
	}S1;

则sizeof(S1)=8。这是因为结构体node1中最长的数据类型是int,占4个字节,因此以4字节对齐,则该结构体在内存中存放方式为

	|--------int--------|   4字节

	|char|----|--short-|   4字节

总共占8字节

test3

	typedef struct node2
	{
		char a;
		int b;
		short c;
	}S2;

则siezof(S3)=12.最长数据类型为int,占4个字节。因此以4字节对齐,其在内存空间存放方式如下:

	|char|----|----|----|  4字节

	|--------int--------|  4字节

	|--short--|----|----|  4字节

总共占12个字节

test4 含有静态数据成员

	typedef struct node3
	{
	 	int a;
		short b;
		static int c;
	}S3;

则sizeof(S3)=8.这里结构体中包含静态数据成员,而静态数据成员的存放位置与结构体实例的存储地址无关(注意只有在C++中结构体中才能含有静态数据成员,而C中结构体中是不允许含有静态数据成员的)。其在内存中存储方式如下:

	|--------int--------|   4字节

	|--short-|----|----|    4字节

而变量c是单独存放在静态数据区的,因此用siezof计算其大小时没有将c所占的空间计算进来。

test5 结构体中含有结构体

	typedef struct node4
	{
		bool a;
		S1 s1;
		short b;
	}S4;

则sizeof(S4)=16。是因为s1占8字节,而s1中最长数据类型为int,占4个字节,bool类型1个字节,short占2字节,因此以4字节对齐,则存储方式为

	|-------bool--------|  4字节

	|-------s1----------|  8字节

	|-------short-------|  4字节

test6

	typedef struct node5
	{
 	   bool a;
		S1 s1;
		double b;
		int c;
	}S5;

则sizeof(S5)=32。是因为s1占8字节,而s1中最长数据类型为int,占4字节,而double占8字节,因此以8字节对齐,则存放方式为:

	|--------bool--------|    8字节

	|---------s1---------|    8字节

	|--------double------|    8字节

	|----int----|---------|     8字节 

test7

对齐是可以更改的,使用#pragmapack(x)可以改变编译器的对齐方式。C++固有类型的对齐取编译器对齐方式与自身大小中较小的一个。

  1. 使用伪指令#pragma pack(n),C编译器将按照n个字节对齐。
  2. 使用伪指令#pragma pack(),取消自定义字节对齐方式。

例如,指定编译器按2对齐,int类型的大小是4,则int的对齐为2和4中较小的2。在默认的对齐方式下,因为几乎所有的数据类型都不大于默认的对齐方式8 (除了 long double),所以所有的固有类型的对齐方式可以认为就是类型自身的大小。

补充:long double未规定确切精度,不同平台有不同实现,有的8字节,有的10,12字节.

另外,还有如下的一种方式_attribute((aligned(n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

如果在程序开头使用命令#pragma pack(4),对于下面的结构体

typedef struct node5
{
	bool a;
	S1 s1;
	double b;
	int c;
}S5;

则sizeof(S5)=24.因为强制以4字节对齐,而S5中最长数据类型为double,占8字节,因此以4字节对齐。在内存中存放方式为:

	  |-----------a--------|   4字节

	  |--------s1----------|   4字节

	  |--------s1----------|   4字节

	  |--------b-----------|   4字节

	  |--------b-----------|   4字节

	  |---------c----------|    4字节

总结

总结一下,在计算sizeof时主要注意一下几点:

  1. 若为空结构体,则只占1个字节的单元

  2. 若结构体中所有数据类型都相同,则其所占空间为 成员数据类型长度×成员个数

  3. 若结构体中数据类型不同,则取最长数据类型成员所占的空间为对齐标准,数据成员包含另一个结构体变量t的话,则取t中最 长数据类型与其他数据成员比较,取最长的作为对齐标准,但是t存放时看做一个单位存放,只需看其他成员即可。

  4. 若使用了#pragma pack(n)命令强制对齐标准,则取n和结构体中最长数据类型占的字节数两者之中的小者作为对齐标准。

另外除了结构体中存在对齐之外,普通的变量存储也存在字节对齐的情况,即自身对齐。编译器规定:普通变量的存储首地址必须能被该变量的数据类型宽度整除。

结构体的位域

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

位域的定义和位域变量的说明

位域定义与结构定义相仿,其形式为:

1
2
3
4
5
6
struct 位域结构名
{

 位域列表

};

其中位域列表的形式为:

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

位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者直接说明这三种方式。

1
2
3
4
5
6
struct bs
{
          int a:8;
          int b:2;
          int c:6;
}data;

说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。

结构体中含位域字段。位域成员不能单独被取sizeof值。C99规定int、unsigned int和bool可以作为位域类型,但编译器几乎都对此作了扩展,允许其它类型类型的存在。

位域对齐规则

使用位域的主要目的是压缩存储,其大致规则为:

  1. 如果相邻位域字段的类型相同,且其位宽之和小于类型的sizeof大小,则后面的字段将紧邻前一个字段存储,直到不能容纳为止;

  2. 如果相邻位域字段的类型相同,但其位宽之和大于类型的sizeof大小,则后面的字段将从新的存储单元开始,其偏移量为其类型大小的整数倍;

  3. 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式;

  4. 如果位域字段之间穿插着非位域字段,则不进行压缩;

  5. 整个结构体的总大小为最宽基本类型成员大小的整数倍。

特殊用法说明

一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。

1
2
3
4
5
6
    struct bs
    {
        unsigned a:4;
        unsigned b:5; /*从下一单元开始存放*/
        unsigned c:4;
    }

如果某一位域要从一个新的类型对齐处开始 可以在前面加一个长度为0的匿名位域 unsigned :0

1
2
3
4
5
6
    struct bs
    {    unsigned a:4;
        unsigned :0; /*空域表示下一个位段存储在一个新的字节中*/
        unsigned b:4; /*从下一单元开始存放*/
        unsigned c:4;
    }

由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度。

位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:

1
2
3
4
5
6
7
struct k
{
int a:1;
int :2; /*无位域名,该2位不能使用,只起占位的作用*/
int b:3;
int c:2;
};

位域的使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#include <iostream>
#include <memory.h>
using namespace std;
struct A
{
int a:5;
int b:3;
};
int main(void)
{
char str[100] = "0134324324afsadfsdlfjlsdjfl";
struct A d;
memcpy(&d, str, sizeof(A));
cout << d.a << endl;
cout << d.b << endl;
return 0;
}

在32位x86小端存储机器上输出:

-16
1

解析:由于是32位处理器,而且结构体中a和b元素类型均为int(也是4个字节),所以结构体的A占用内存为4个字节。 上例程序中定义了位域结构A,两个个位域为a(占用5位),b(占用3位),所以a和b总共占用了结构A一个字节(低位的一个字节)。 因为是小端存储,即存放的0123中用ascii码在机器中的表示是3210

高位 00110100     00110011   00110001     00110000 低位  
          '4'        '3'         '1'             '0'  

其中d.a和d.b占用d低位一个字节(00110000),因为是小端存储,从后往前取位

d.a : 10000, d.b : 001  

d.a内存中二进制表示为10000,由于d.a为有符号的整型变量,输出时要对符号位进行扩展,所以结果为-16(二进制为11111111111111111111111111110000)

d.b内存中二进制表示为001,由于d.b为有符号的整型变量,输出时要对符号位进行扩展,所以结果为1(二进制为00000000000000000000000000000001)