首先,我们得知道为什么要进行内存对齐,它的意义何在?在这儿可以先看这样一张图。(手绘请见谅!!!)
我们知道,在32位CPU下,一个读取周期可以读取四个字节。一个字符变量在内存中占一个字节,而整型为在内存中占4个字节。
那么CPU一个读写周期刚好能读取一个整形,所以如上边左图,两个整型变量分别读写一次即可;而上面中间的图,第一个读写周期读取的四个字节为一个char变量和一个整形变量的前三个字节,第二个读写周期读取的是整型变量剩余的那一个字节及其后面的三个字节的空间。这样就导致该整型变量 用了两个读写周期才读写成功,而且还要对两个读写结果的高地地址拼凑才能得出该数据。大大降低了读写效率;上边右图是中间这幅图采用内存对齐后的效果,第一个读写周期读写一个字符变量和三个字节的无内容的空间,第二个读写周期读写了一个整型变量。这样,同样是读写连续定义的一个字符变量和一个整型变量,第二次整型变量需要读写两次,第三次却只用读写一次。
因此要采用字节对齐的方式,可以理解为用空间换时间。
字节对齐是一个小知识点,但并不容易掌握,先给出字节对齐的几种基本概念:
1、 学好字节对齐需要掌握的一些基本概念:
(1)基本数据类型的自身对齐值:
1字节:char型
2字节:short型
4字节:int,float,long、指针类型
8字节:doublel类型
对齐规则是:第一个变量从程序所占所占空间偏移量为0的位置开始存放,第二个及以后的变量从上一个变量所占空间的末尾之后找到的第一个偏移量为该变量所占空间大小的整数倍处开始存放,同时满足加起来的值是4的倍数(避免一个变量需要多次读写及拼接才能读写成功)。
(2)指定对齐:编译器提供#pragma pack(n)来设定变量以n字节对齐方式。(#pragma pack(0)可以恢复默认对齐)注:指定值必须是 2 的 N 次方。
(3)自定义类型的自身对齐值:结构体或类的成员中自身对齐值最大的值。(结构体的大小必须是最大对其数的整数倍结构体的成员中把所占字节较小的成员集中存放可以节省空间,缩小结构体所占空间大小,成员存放是的对其规则满足基本数据类型的对其规则)
(4)自定义类型的有效对齐值:自定义类型的自身对齐值和指定对齐值(或边界对齐值)中较小的值。
以上几条规则基本概括了字节对齐的情况,需要掌握,据此,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。
注:VS、VC编译器的默认对齐数是8、GCC编译器的默认对齐数是4
2、几种基本概念的对应实例:
(1)基本数据类型的自身对齐值:
#include<stdio.h>int main()
{int i;char c;return 0;
}
可以看到字符变量C开辟的空间和整形 i 开辟的空间是不连续的,而是隔了中间三个字节(i 和 c 中间隔了4个字节,但C只占一个字节,还有三个字节是空着的)。为什么呢?字节对齐很容易就解释了。此时没有指定对齐,所以按自然对齐来看,即边界对齐。c 是后定义的,自身对齐值为一个字节,而要满足 c 的自身对齐值加上和 i 之间隔的空间等于 i 的自身对齐值(4个字节)且是 4 的倍数,那么c 和 i 之间要隔三个字节才行。
(2)、自定义类型的自身对齐值(结构体或类的成员中自身对齐值最大的值):
对结构体来说,要满足结构体成员中已经定义的所有成员的自身对齐值加上其后面空出来的字节数之和是下一个要定义的成员的自身对齐值的倍数,且所有成员的自身对齐值加上其后面空出来的字节数(自定义的结构体类型所占字节数)是结构体自身对齐值的倍数。
#include<stdio.h>typedef struct Test
{char c;short s;int i;double d;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
这里结果为16,怎么来的?看下图即可:
(3)自定义类型的有效对齐值(自定义类型的自身对齐值和指定对齐值中较小的值):
对结构体来说,要满足每一个成员的自身对齐值加上其后面空出来的字节数是有效对齐值的倍数,且所有成员的自身对齐值加上其后面空出来的字节数(自定义的结构体类型所占字节数)是结构体有效对齐值的倍数。
#include<stdio.h>#pragma pack (4)typedef struct Test
{char c;double d;int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
按上一题的理解,这儿应该是24,但怎么又是16呢?
关键在于———–#pragma pack (4)
看图说话:
(4)关于自定义类型的自身对齐值再来看两个例子:
#include<stdio.h>#pragma pack (4)typedef struct Test
{short s;struct{int i;double d;char c;};long l;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
结果如下图:
#include<stdio.h>typedef struct Test
{short s;struct A{int i;double d;char c;};long l;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
结果如下图:
以上两段代码看着一样,但为什么结果却不一样呢?细心就会发现第二段代码在内嵌的struct后面加了一个 A。
当内嵌的struct后面没有 A 时,它是一个结构体
当内嵌的结果提后面有 A 是,它是一个类型,因为类型肯定是可以定义变量的,那么这个定义出来的变量肯定是有大小的,那样才能存储数据,这个大小就是又类型决定的,所以类型肯定是有大小的,而此时不知道为它分配多大空间合适,大了,浪费,小了,不够用,所以编译器就为其分配了程序一般变量的最小单位–>>一个字节。
注:空结构体的所占内存空间大小是一个字节。因为它是一个类型,而想要成为类型,那么必须要能定义变量,那变量就要占空间,而这个空间大了就太浪费了,因为不知道这个类型要定要什么类型的变量,所以编译器就自动为它分配了一个变量存储的最小单位(一个字节)。
(5)自定义类型中联合体的对齐方式:
因为联合体的对齐方式与联合体有不同,所以在单独提一下。
联合体的字节对齐也分自身对齐值与有效对齐值,联合体自身对齐值为其成员中自身对齐值最小的,有效对齐值为联合体的自身对齐值与边界对齐值(编译器决定,VC6.0默认为8字节,其值可在 工程·–>设置–>C/C++–>Code Generation–>Struct member Alignment 处修改)、指定对齐值中最小的,用数学表达式可以表示为:
有效对齐值 = min(min(成员1,成员2…成员n),边界对齐值,指定对齐值)
最后要满足,联合体成员中自身对齐值的最大值经过字节对齐后(N = 最大值+n,,所加的n最小且能使N成为联合体有效对齐值的倍数),就是自定义的联合体类型所占的字节数。
下面以几个例子来说明:
①有效对齐值为联合体成员中自身对齐值的最大值
#include<stdio.h>#pragma pack (6)typedef union Test
{char c;int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
结果如下图:
解析如下图:
②有效对齐值为边界对齐值
#include<stdio.h>#pragma pack (6)typedef union Test
{char c[13];int i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
表面看与第一段代码没区别,细心就会发现联合体中的字符变量变成了字符数组
结果如下图:
解析如下图:
③有效对齐值为指定对齐值
#include<stdio.h>#pragma pack (2)typedef union Test
{char c[13];short i;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
结果为:
解析如下图:
(6)自定义类型相互嵌套的字节对齐
①联合体嵌套结构体:
#include<stdio.h>#pragma pack(2)typedef union Test
{char c;int i;struct{short s;int i1;double d;char c1;};
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
解析如图:
结构体自嵌套(上面已给出(4))、联合体自嵌套、结构体嵌套联合体 ……….与此例道理都差不多,这里不给出示例。
究其根本,不管怎么嵌套,只要掌握了基本的自定义类型的对齐方式,都能很简单的做出来,所以,自定义类型的字节对齐方式才是本文的重难点。
(7)位域
所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
位域在内存中的存储遵循以下原则:
①一个位域必须存储在同一个字节中,不能跨字节,同样不能垮类型。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。也可以有意使某位域从下一单元开始。例如:
struct bs
{unsigned a:4unsigned :0 /空域/unsigned b:4 /从下一单元开始存放/unsigned c:4}
在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
②位域的长度不能大于指定类型固有长度,比如说int的位域长度不能超过32,bool的位域长度不能超过8。
③位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。例如:
struct k
{int a:1int :2 /该2位不能使用/int b:3int c:2};
从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。
④位域的内存分配仍然遵循字节对齐
用一张图来说明位域这个概念:
⑤位域的字节对齐
#include<stdio.h>typedef struct Test
{char a:2;char b:4;char c:3;int d:2;
}Test;int main()
{printf("%d\n",sizeof(Test));return 0;
}
结果为:
解析如下图:
常见的字节对齐基本就这些,当然其中的变化情况会很多很多。字节对齐是一个小而难的知识点,初学很不易掌握,写下这篇博客的过程中自己也学到了很多。希望读者与博主互勉共进,不吝纠正博客中的错误。