C&C++学习笔记(2)——字节对齐

很多编程语言的书中并未提到这个概念,但是如果不加注意有可能会出现问题。

本文仅供个人记录和复习,不用于其他用途

结构体的大小

书上所讲的结构体大小,一般来说就是结构体内所有成员所占内存之和。至于为什么说是一般情况,那就是因为系统会根据情况进行相应的调整,使得最终的内存大小和设想中的有所出入。那么,这种情况被称为字节对齐。一般来说,字节对齐有以下三条通用原则:

  • 结构体变量的大小能够被最宽基本类型成员大小所整除
  • 结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节
  • 结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要会在最末一个成员之后加上填充字节

注意,字节对齐一定要是基本类型,也就是诸如charshortintfloatdouble之类的内置数据类型。那么话不多说,让我们看看具体的例子:

1
2
3
4
5
6
struct info1
{
char c;
short sh;
char ch[9];
};

看看上面这个结构体,按照你的理解,是不是大小就是12呢?很显然不是,正确结果应该是14。首先,结构体字节对齐的第一个要点,就是找到最宽基本类型。注意,这里最宽的基本类型是short,而不是char[9]。看看第一个要求,结构体变量的大小能被short整除,也就是说char必须补充一个字节,而char[9]补充一个字节。这样一来,所有的变量都能被short整除。所以,结构体变量的大小就是2+2+10=14

1
2
3
4
5
6
struct info2
{
char c;
int sh;
char ch[9];
};

同样的,如果把short换成int,那么char就需要补充三个字节,而char[9]也需要补充三个字节。所以,总的大小就是4+4+12=20

1
2
3
4
5
6
struct info3
{
char c;
char ch[9];
short sh;
};

你是不是掌握了这个技巧呢?看看上面这个,是不是和第一个很像呢?不过我要告诉你,正确答案是12。你可能会觉得莫名其妙,这不过只是换了一个顺序,怎么就和第一个的大小不一样?让我们接着往下看:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct info4
{
char ch;
int num;
short sh;
};
struct info5
{
char ch;
short sh;
int num;
};

info4的大小很明显是12,然而info5的大小却是8。为什么会这样呢?其实关于那三条准则,我觉得有必要说一下,那就是第二条准则是我们进行字节对齐最关键的。具体的还是看看例子吧:

1
2
3
4
5
6
struct info3 // 首地址为300500
{
char c; // 首地址为300500 偏移量为0
char ch[9]; // 首地址为300501 偏移量为1
short sh; // 首地址为30050A 偏移量为10
};

我们可以看看第二条准则,每个成员相对于结构体首地址的偏移量都是该成员大小的整数倍,这里的成员类型必须是基本数据类型。我们可以看到,c的偏移量为0,是char的0倍。而ch的偏移量为1,是char的1倍。最后sh的偏移量为10,是short的5倍。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct info4 // 首地址为300500
{
char ch; // 首地址为300500 偏移量为0
int num; // 首地址为300504 偏移量为4
short sh; // 首地址为300508 偏移量为8
};
struct info5 // 首地址为300500
{
char ch; // 首地址为300500 偏移量为0
short sh; // 首地址为300502 偏移量为2
int num; // 首地址为300504 偏移量为4
};

对于info4,如果num的首地址为300501,偏移量就只有1,不是4的整数倍,所以要填充3个字节。而sh的偏移量为8,是2的4倍,满足准则二。但是呢,结构体的总大小必须是最宽基本类型的整数倍,不够的在末尾填充。那么这里是4+4+2=10,不满足准则3,所以要在sh后面填充两个字节。

同样的,对于info5ch满足准则二,sh的首地址如果为300501,那么偏移量就只有1,所以必须给ch填充一个字节。因此sh的首地址为300502,满足准则二。而num的偏移量为4,满足准则二。最后再看看总共的大小2+2+4=8,满足准则三。

1
2
3
4
5
6
7
8
struct info6 // 首地址为300500
{
char ch; // 首地址为300500 偏移量为0
short sh; // 首地址为300502 偏移量为2
double x; // 首地址为300508 偏移量为8
int num; // 首地址为300510 偏移量为16
char str[19]; // 首地址为300514 偏移量为20
};

注意一下,这里我用的是十六进制。chsh的首地址不多说,x的首地址如果为300504的话,偏移量就只有4,所以必须给sh填充4个字节。至于numstr的首地址都满足准则二。看看总体的大小2+6+8+4+19=39,不满足准则三,所以必须给str填充1个字节。

共用体的大小

共用体中只能有一个成员在占用内存,那么共用体的大小显然就是最大的那个成员所占的大小。不过我既然在这里提到了,那么显然共用体也需要遵循字节对齐。大致有以下几条:

  • 共用体变量的大小能够被其最宽基本类型成员的大小整除
  • 共用体的总大小是最宽基本类型成员大小的整数倍

我们来看看几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
union exp1
{
int num;
char str[11];
};
union exp2
{
double x;
char str[11];
};
union exp3
{
short x;
char str[11];
};

很简单,exp1的大小为12,exp2的大小为16,exp3的大小为12。如果我们把exp1中的int类型换成char类型,那么大小就是11。

类的大小

其实class和struct没有什么差别,它们的差别仅在以下几个地方:

  • class默认私有继承,struct默认公有继承
  • class默认成员为私有,struct默认成员为公有

除了上面两个地方,class和struct都是一样的。只不过,C++中习惯使用class而已。注意,C++中,如果类为空,那么默认大小为1,而不是0。其实这个也容易理解,因为必须要有一个空间来表示这个类的存在。

位字段的字节对齐

位字段的使用一般是在内存特别小,需要程序员精打细算分配内存的时候。比如,各种硬件设施的编程。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct data1
{
unsigned int int1: 5;
unsigned int int2: 4;
unsigned int int3: 10;
};
struct data2
{
unsigned char ch1: 1;
unsigned char ch2: 2;
unsigned int int1: 10;
};

对于包含相同类型的结构体,只需将位字段的位数简单相加,考虑该类型的整数倍能否容纳即可。看看data1,全部相加为19位,而int类型占32位(在win32中),所以必须填充至32位。如果超过32位,那么就必须保证是32的整数倍。这里data1就是4个字节。

对于不同类型的结构体,得考虑不同类型间的字节对齐,例如data2,首先是char之间的对齐,小于1个字节,用1个字节即可,然后比较intchar,进行对齐,char要填充成4个字节。这里data2是8个字节。

总而言之,我们对于不同类型的位字段对齐,要做的就是把不同的类型进行字节对齐,对于相同的类型就可以计算它们所占的内存之和,然后根据是不是该类型大小的整数倍来填充字节。

为什么要使用字节对齐

你可能会觉得字节对齐无形之中给自己添加了烦恼,一个不小心可能就会计算错误。事实上,计算机使用字节对齐是为了寻址的方便。如果一个结构体中的变量类型相同,寻址时就只要按照这个变量的大小递增即可,非常方便。但是呢,如果变量的类型不相同,自然也就无法像那样递增。那么计算机为了方便寻址,便首先寻找结构体中的最宽基本类型,然后对其他的成员变量进行字节填充。这样一来,计算机便可以按照最宽基本类型的大小进行递增。

有的人可能会问,填充空的字节是不是会浪费内存。事实上,如果不这么做,那么计算机势必不能按照最快的方式寻址。与其牺牲最重要的速度,还不如牺牲一些无关紧要的内存,这样能够大大地加快计算机的运行速度。

关于结构体字节对齐的补充

结构体的字节对齐可以说是最复杂的,准则一其实只是用于判断简单一点的结构体。对于复杂的结构体,我们还是采用准则2和准则3进行判断。