一.内存对齐的初步解说
内存对齐能够用一句话来概括:
“数据项仅仅能存储在地址是数据项大小的整数倍的内存位置上”
比如int类型占用4个字节,地址仅仅能在0,4,8等位置上。
例1:
#include <stdio.h>
struct xx{
char b;
int a;
int c;
char d;
};
struct xx{
char b;
int a;
int c;
char d;
};
int main()
{
struct xx bb;
printf("&a = %p ", &bb.a);
printf("&b = %p ", &bb.b);
printf("&c = %p ", &bb.c);
printf("&d = %p ", &bb.d);
printf("sizeof(xx) = %d ", sizeof(struct xx));
{
struct xx bb;
printf("&a = %p ", &bb.a);
printf("&b = %p ", &bb.b);
printf("&c = %p ", &bb.c);
printf("&d = %p ", &bb.d);
printf("sizeof(xx) = %d ", sizeof(struct xx));
return 0;
}
}
运行结果例如以下:
&a = ffbff5ec
&b = ffbff5e8
&c = ffbff5f0
&d = ffbff5f4
sizeof(xx) = 16
&b = ffbff5e8
&c = ffbff5f0
&d = ffbff5f4
sizeof(xx) = 16
会发现b与a之间空出了3个字节。也就是说在b之后的0xffbff5e9,0xffbff5ea,0xffbff5eb空了出来,a直接存储在了0xffbff5ec。 由于a的大小是4。仅仅能存储在4个整数倍的位置上。打印xx的大小会发现,是16。有些人可能要问,b之后空出了3个字节,那也应该是13啊?其余的3个 呢?这个往后阅读本文会理解的更深入一点,这里简单说一下就是d后边的3个字节。也会浪费掉。也就是说,这3个字节也被这个结构体占用了.
能够简单的改动结构体的结构。来减少内存的使用,比如能够将结构体定义为:
struct xx{
char b;
char d;
int a;
int c;
};
struct xx{
char b;
char d;
int a;
int c;
};
这样打印这个结构体的大小就是12。省了非常多空间,能够看出。在定义结构体的时候。一定要考虑要内存对齐的影响,这样能使我们的程序占用更小的内存。
二.操作系统的默认对齐系数
每 个操作系统都有自己的默认内存对齐系数,假设是新版本号的操作系统,默认对齐系数一般都是8,由于操作系统定义的最大类型存储单元就是8个字节,比如 long long(为什么一定要这样。在第三节会解说)。不存在超过8个字节的类型(比如int是4,char是1,long在32位编译时是4,64位编译时是 8)。当操作系统的默认对齐系数与第一节所讲的内存对齐的理论产生冲突时。以操作系统的对齐系数为基准。
比如:
如果操作系统的默认对齐系数是4,那么对与long long这个类型的变量就不满足第一节所说的,也就是说long long这样的结构,能够存储在被4整除的位置上,也能够存储在被8整除的位置上。
能够通过#pragma pack()语句改动操作系统的默认对齐系数,编敲代码的时候不建议改动默认对齐系数,在第三节会解说原因
例2:
#include <stdio.h>
#pragma pack(4)
struct xx{
char b;
long long a;
int c;
char d;
};
#pragma pack()
#pragma pack(4)
struct xx{
char b;
long long a;
int c;
char d;
};
#pragma pack()
int main()
{
struct xx bb;
printf("&a = %p ", &bb.a);
printf("&b = %p ", &bb.b);
printf("&c = %p ", &bb.c);
printf("&d = %p ", &bb.d);
printf("sizeof(xx) = %d ", sizeof(struct xx));
{
struct xx bb;
printf("&a = %p ", &bb.a);
printf("&b = %p ", &bb.b);
printf("&c = %p ", &bb.c);
printf("&d = %p ", &bb.d);
printf("sizeof(xx) = %d ", sizeof(struct xx));
return 0;
}
打印结果为:
}
打印结果为:
&a = ffbff5e4
&b = ffbff5e0
&c = ffbff5ec
&d = ffbff5f0
sizeof(xx) = 20
&b = ffbff5e0
&c = ffbff5ec
&d = ffbff5f0
sizeof(xx) = 20
发现占用8个字节的a,存储在了不能被8整除的位置上。存储在了被4整除的位置上。採取了操作系统的默认对齐系数。
三.内存对齐产生的原因
内存对齐是操作系统为了高速訪问内存而採取的一种策略,简单来说,就是为了放置变量的二次訪问。操作系统在訪问内存 时,每次读取一定的长度(这个长度就是操作系统的默认对齐系数,或者是默认对齐系数的整数倍)。假设没有内存对齐时,为了读取一个变量是,会产生总线的二 次訪问。
比如如果没有内存对齐。结构体xx的变量位置会出现例如以下情况:
struct xx{
char b; //0xffbff5e8
int a; //0xffbff5e9
int c; //0xffbff5ed
char d; //0xffbff5f1
};
char b; //0xffbff5e8
int a; //0xffbff5e9
int c; //0xffbff5ed
char d; //0xffbff5f1
};
操作系统先读取0xffbff5e8-0xffbff5ef的内存,然后在读取0xffbff5f0-0xffbff5f8的内存,为了获得值c,就须要将两组内存合并,进行整合。这样严重减少了内存的訪问效率。(这就涉及到了老生常谈的问题,空间和效率哪个更重要?这里不做讨论)。
这样大家就能理解为什么结构体的第一个变量,无论类型怎样,都是能被8整除的吧(由于訪问内存是从8的整数倍開始的,为了添加读取的效率)!
内存对齐的问题主要存在于理解struct等复合结构在内存中的分布。
首先要明确内存对齐的概念。
很多实际的计算机系统对基本类型数据在内存中存放的位置有限制。它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数。这就是所谓的内存对齐。
这个k在不同的cpu平台下,不同的编译器下表现也有所不同。比方32位字长的计算机与16位字长的计算机。这个离我们有些远了。
我们的开发主要涉及两大平台。windows和linux(unix)。涉及的编译器也主要是microsoft编译器(如cl),和gcc。
内存对齐的目的是使各个基本数据类型的首地址为相应k的倍数,这是理解内存对齐方式的终极法宝。另外还要区分编译器的分别。
明确了这两点基本上就能搞定全部内存对齐方面的问题。
不同编译器中的k:
1、对于microsoft的编译器,每种基本类型的大小即为这个k。大体上char类型为8。int为32,long为32。double为64。
2、对于linux下的gcc编译器,规定大小小于等于2的。k值为其大小,大于等于4的为4。
1、对于microsoft的编译器,每种基本类型的大小即为这个k。大体上char类型为8。int为32,long为32。double为64。
2、对于linux下的gcc编译器,规定大小小于等于2的。k值为其大小,大于等于4的为4。
明确了以上的说明对struct等复合结构的内存分布就应该非常清楚了。
以下看一下最简单的一个类型:struct中成员都为基本数据类型。比如:
struct test1
{
char a;
short b;
int c;
long d;
double e;
};
struct test1
{
char a;
short b;
int c;
long d;
double e;
};
在windows平台,microsoft编译器下:
如果从0地址開始,首先a的k值为1,它的首地址能够使任何位置。所以a占用第一个字节。即地址0;然后b的k值为2,他的首地址必须是2的倍数,不能是1,所以地址1那个字节被填充。b首地址为地址2,占用地址2,3;然后到c,c的k值为4,他的首地址为4的倍数。所以首地址为4,占用地址4,5。6。7。再然后到d,d的k值也为4。所以他的首地址为8,占用地址8,9。10,11。
最后到e,他的k值为8。首地址为8的倍数,所以地址12,13,14。15被填充。他的首地址应为16,占用地址16-23。显然其大小为24。
这就是 test1在内存中的分布情况。我们建立一个test1类型的变量,a、b、c、d、e分别赋值2、4、8、16、32。
然后从低地址依次打印出内存中每一个字节相应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 0 0 0 0 40 40
验证:
显然判断是正确的。
显然判断是正确的。
在linux平台,gcc编译器下:
如果从0地址開始。首先a的k值为1,它的首地址能够使任何位置,所以a占用第一个字节。即地址0。然后b的k值为2,他的首地址必须是2的倍数。不能是1。所以地址1那个字节被填充,b首地址为地址2,占用地址2。3;然后到c。c的k值为4。他的首地址为4的倍数,所以首地址为4。占用地址4。5,6。7;再然后到d,d的k值也为4。所以他的首地址为8,占用地址8,9。10。11。
如果从0地址開始。首先a的k值为1,它的首地址能够使任何位置,所以a占用第一个字节。即地址0。然后b的k值为2,他的首地址必须是2的倍数。不能是1。所以地址1那个字节被填充,b首地址为地址2,占用地址2。3;然后到c。c的k值为4。他的首地址为4的倍数,所以首地址为4。占用地址4。5,6。7;再然后到d,d的k值也为4。所以他的首地址为8,占用地址8,9。10。11。
最后到e,从这里開始与microsoft的编译器開始有所差异,他的k值为不是8,仍然是4,所以其首地址是12,占用地址12-19。显然其大小为20。
验证:
我们建立一个test1类型的变量。a、b、c、d、e分别赋值2、4、8、16、32。
我们建立一个test1类型的变量。a、b、c、d、e分别赋值2、4、8、16、32。
然后从低地址依次打印出内存中每一个字节相应的16进制数为:
2 0 4 0 8 0 0 0 10 0 0 0 0 0 0 0 0 0 40 40
显然判断也是正确的。
接下来,看一看几类特殊的情况。为了避免麻烦。不再描写叙述内存分布,仅仅计算结构大小。
第一种:嵌套的结构
struct test2
{
char f;
struct test1 g;
};
struct test2
{
char f;
struct test1 g;
};
在windows平台。microsoft编译器下:
这样的情况下假设把test2的第二个成员拆开来,研究内存分布,那么能够知道,test2的成员f占用地址0。g.a占用地址1,以后的内存分布不变,仍然满足全部基本数据成员的首地址都为其相应k的倍数这一原则,那么test2的大小就还是24了。可是实际上test2的大小为32。这是由于:不能由于test2的结构而改变test1的内存分布情况。所以为了使test1种各个成员仍然满足对齐的要求,f成员后面须要填充一定数量的字节,不难发现。这个数量应为7个,才干保证test1的对齐。所以test2相对于test1来说添加了8个字节,所以test2的大小为32。
在linux平台,gcc编译器下:
相同,这样的情况下假设把test2的第二个成员拆开来,研究内存分布,那么能够知道,test2的成员f占用地址0,g.a占用地址1,以后的内存分布不变,仍然满足全部基本数据成员的首地址都为其相应k的倍数这一原则,那么test2的大小就还是20了。可是实际上test2的大小为24,相同这是由于:不能由于test2的结构而改变test1的内存分布情况,所以为了使test1种各个成员仍然满足对齐的要求,f成员后面须要填充一定数量的字节,不难发现,这个数量应为3个。才干保证test1的对齐。
所以test2相对于test1来说添加了4个字节,所以test2的大小为24。
另外一种:位段对齐
struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
或者
struct test3
{
unsigned int a:4;
int b:4;
char c;
};
{
unsigned int a:4;
unsigned int b:4;
char c;
};
或者
struct test3
{
unsigned int a:4;
int b:4;
char c;
};
在windows平台,microsoft编译器下:
相邻的多个同类型的数(带符号的与不带符号的,仅仅要基本类型同样。也为同样的数),假设他们占用的位数不超过基本类型的大小。那么他们可作为一个总体来看待。不同类型的数要遵循各自的对齐方式。
如:test3中。a、b可作为一个总体。他们作为一个int型数据来看待,所以test3的大小为8字节。而且a与b的值在内存中从低位開始依次排列,位于4字节区域中的前0-3位和4-7位
如:test3中。a、b可作为一个总体。他们作为一个int型数据来看待,所以test3的大小为8字节。而且a与b的值在内存中从低位開始依次排列,位于4字节区域中的前0-3位和4-7位
假设test4位下面格式
struct test4
{
unsigned int a:30;
unsigned int b:4;
char c;
};
那么test4的大小就为12个字节,而且a与b的值分别分布在第一个4字节的前30位。和第二个4字节的前4位。
struct test4
{
unsigned int a:30;
unsigned int b:4;
char c;
};
那么test4的大小就为12个字节,而且a与b的值分别分布在第一个4字节的前30位。和第二个4字节的前4位。
如过test5是下面形式
struct test5
{
unsigned int a:4;
unsigned char b:4;
char c;
};
struct test5
{
unsigned int a:4;
unsigned char b:4;
char c;
};
那么因为int和char不同类型。他们分别以各自的方式对齐,所以test5的大小应为8字节,a与b的值分别位于第一个4字节的前4位和第5个字节的前4位。
在linux平台。gcc编译器下:
struct test3
{
unsigned int a:4;
unsigned int b:4;
char c;
};
gcc下,相邻各成员。无论类型是否同样。占的位数之和超过这些成员中第一个的大小的时候,在结构中以k值为1对齐,在结构外k值为其基本类型的值。
{
unsigned int a:4;
unsigned int b:4;
char c;
};
gcc下,相邻各成员。无论类型是否同样。占的位数之和超过这些成员中第一个的大小的时候,在结构中以k值为1对齐,在结构外k值为其基本类型的值。
不超过的情况下在内存中依次排列。
如test3。其大小为4。a,b的值在内存中依次排列分别为第一个四字节中的0-3和4-7位。
假设test4位下面格式
struct test4
{
unsigned int a:20;
unsigned char b:4;
char c;
};
test4的大小为4个字节,而且a与b的值分别分布在第一个4字节的0-19位,和20-23位,c存放在第4个字节中。
如过test5是下面形式
struct test5
{
unsigned int a:10;
unsigned char b:4;
short c;
};
struct test4
{
unsigned int a:20;
unsigned char b:4;
char c;
};
test4的大小为4个字节,而且a与b的值分别分布在第一个4字节的0-19位,和20-23位,c存放在第4个字节中。
如过test5是下面形式
struct test5
{
unsigned int a:10;
unsigned char b:4;
short c;
};
那么test5的大小应为4字节,a。b的值为0-9位和10-13位。c存放在后两个字节中。
假设a的大小变成了20
那么test5的大小应为8字节。
即
struct test6
{
unsigned int a:20;
unsigned char b:4;
short c;
};
{
unsigned int a:20;
unsigned char b:4;
short c;
};
此时,test6的a、b共占用0,1,2共3字节,c的k值为2,事实上能够4位首位置,可是在结构外,a要以int的方式对齐。也就是说连续两个test6对象在内存中存放的话,a的首位置要保证为4的倍数。那么c后面必须多填充2位。所以test6的大小为8个字节。
关于位段结构的部分是比較复杂的。临时我就知道这么多。