先让我们看四个首要的根基概念:
1.数据类型自身的对齐值:
对于char型数据,其自身对齐值为1,对于short型为2,对于int,float,double类型,其自身对齐值为4,单位字节。
2.结构 体或者类的自身对齐值:其成员中自身对齐值最大的那个值。
3.指定对齐值:#pragma pack (value)时的指定对齐值value。
4.数据成员、结构 体和类的有效对齐值:自身对齐值和指定对齐值中小的那个值。
有 了这些值,我们就可以很方便的来讨论具体数据结构的成员和其自身的对齐方式。有效对齐值N是最终用来决定数据存放地址方式的值,最重要。有效对齐N,就是 表示“对齐在N上”,也就是说该数据的"存放起始地址%N=0".而数据结构中的数据变量都是按定义的先后顺序来排放的。第一个数据变量的起始地址就是数 据结构的起始地址。结构体的成员变量要对齐排放,结构体本身也要根据自身的有效对齐值圆整(就是结构体成员变量占用总长度需要是对结构体有效对齐值的整数 倍,
对齐原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
规则:
1、数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的对齐按照#pragma pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
2、结构(或联合)的整体对齐规则:在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照#pragma pack指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。
3、结合1、2可推断:当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果。
Win32平台下的微软C编译器(cl.exefor 80×86)的对齐策略:
1)结构体变量的首地址是其最长基本类型成员的整数倍;
备注:编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能是该基本数据类型的整倍的位置,作为结构体的首地址。将这个最宽的基本数据类型的大小作为上面介绍的对齐模数。
2)结构体每个成员相对于结构体首地址的偏移量(offset)都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节(internal adding);
备注:为结构体的一个成员开辟空间之前,编译器首先检查预开辟空间的首地址相对于结构体首地址的偏移是否是本成员的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充一定的字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节。
3)结构体的总大小为结构体最宽基本类型成员大小的整数倍,如有需要,编译器会在最末一个成员之后加上填充字节(trailing padding)。
备注:
a、结构体总大小是包括填充字节,最后一个成员满足上面两条以外,还必须满足第三条,否则就必须在最后填充几个字节以达到本条要求。
b、如果结构体内存在长度大于处理器位数的元素,那么就以处理器的倍数为对齐单位;否则,如果结构体内的元素的长度都小于处理器的倍数的时候,便以结构体里面最长的数据元素为对齐单位。
4) 结构体内类型相同的连续元素将在连续的空间内,和数组一样。
验证试验
我们通过一系列例子的详细说明来证明这个规则吧!
我试验用的编译器包括GCC 3.4.2和VC6.0的C编译器,平台为Windows XP + Sp2。
我们将用典型的struct对齐来说明。首先我们定义一个struct:
pragma pack(n) /* n = 1, 2, 4, 8, 16 */
struct test_t {
int a;
char b;
short c;
char d[6];
};
pragma pack(n)
首先我们首先确认在试验平台上的各个类型的size,经验证两个编译器的输出均为:
sizeof(char) = 1
sizeof(short) = 2
sizeof(int) = 4
我们的试验过程如下:通过#pragma pack(n)改变“对齐系数”,然后察看sizeof(struct test_t)的值。
1、1字节对齐(#pragma pack(1))
输出结果:sizeof(struct test_t) = 13[两个编译器输出一致]
分析过程:
1) 成员数据对齐
pragma pack(1)
struct test_t {
int a; /* int型,长度4 > 1 按1对齐;起始offset=0 0%1=0;存放位置区间[0,3] */
char b; /* char型,长度1 = 1 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* short型,长度2 > 1 按1对齐;起始offset=5 5%1=0;存放位置区间[5,6] */
char d[6]; /* char型,长度1 = 1 按1对齐;起始offset=7 7%1=0;存放位置区间[7,C] */
};/char d[6]要看成6个char型变量/
pragma pack()
成员总大小=13
2) 整体对齐
整体对齐系数 = min((max(int,short,char), 1) = 1
整体大小(size)=
(成员总大小)按
(整体对齐系数) 圆整 = 13 /13%1=0/ [注1]
2、2字节对齐(#pragma pack(2))
输出结果:sizeof(struct test_t) = 14 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
pragma pack(2)
struct test_t {
int a; /* int型,长度4 > 2 按2对齐;起始offset=0 0%2=0;存放位置区间[0,3] */
char b; /* char型,长度1 < 2 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* short型,长度2 = 2 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d[6]; /* char型,长度1 < 2 按1对齐;起始offset=8 8%1=0;存放位置区间[8,D] */
};
pragma pack()
成员总大小=14
2) 整体对齐
整体对齐系数 = min((max(int,short,char), 2) = 2
整体大小(size)=
(成员总大小)按
(整体对齐系数) 圆整 = 14 /* 14%2=0 */
3、4字节对齐(#pragma pack(4))
输出结果:sizeof(struct test_t) = 16 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
pragma pack(4)
struct test_t {
int a; /* int型,长度4 = 4 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* char型,长度1 < 4 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /short型, 长度2 < 4 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] /
char d[6]; /* char型,长度1 < 4 按1对齐;起始offset=8 8%1=0;存放位置区间[8,D] */
};
pragma pack()
成员总大小=14
2) 整体对齐
整体对齐系数 = min((max(int,short,char), 4) = 4
整体大小(size)=
(成员总大小)按
(整体对齐系数) 圆整 = 16 /16%4=0/
4、8字节对齐(#pragma pack(8))
输出结果:sizeof(struct test_t) = 16 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
pragma pack(8)
struct test_t {
int a; /* int型,长度4 < 8 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* char型,长度1 < 8 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* short型,长度2 < 8 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d[6]; /* char型,长度1 < 8 按1对齐;起始offset=8 8%1=0;存放位置区间[8,D] */
};
pragma pack()
成员总大小=14
2) 整体对齐
整体对齐系数 = min((max(int,short,char), 8) = 4
整体大小(size)=
(成员总大小)按
(整体对齐系数) 圆整 = 16 /16%4=0/
5、16字节对齐(#pragma pack(16))
输出结果:sizeof(struct test_t) = 16 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
pragma pack(16)
struct test_t {
int a; /* int型,长度4 < 16 按4对齐;起始offset=0 0%4=0;存放位置区间[0,3] */
char b; /* char型,长度1 < 16 按1对齐;起始offset=4 4%1=0;存放位置区间[4] */
short c; /* short型,长度2 < 16 按2对齐;起始offset=6 6%2=0;存放位置区间[6,7] */
char d[6]; /* char型,长度1 < 16 按1对齐;起始offset=8 8%1=0;存放位置区间[8,D] */
};
pragma pack()
成员总大小=14
2) 整体对齐
整体对齐系数 = min((max(int,short,char), 16) = 4
整体大小(size)=
(成员总大小)按
(整体对齐系数) 圆整 = 16 /16%4=0/
基本结论
8字节和16字节对齐试验证明了“规则”的第3点:“当#pragma pack的n值等于或超过所有数据成员长度的时候,这个n值的大小将不产生任何效果”。另外内存对齐是个很复杂的东西,读者不妨把上述结构体中加个double型成员进去练习一下,上面所说的在有些时候也可能不正确。呵呵^_^
[注1]
什么是“圆整”?
举例说明:如上面的8字节对齐中的“整体对齐”,整体大小=9 按 4 圆整 = 12
圆整的过程:从9开始每次加一,看是否能被4整除,这里9,10,11均不能被4整除,到12时可以,则圆整结束。
上面文字表述太不直观了,鄙人给段代码直观的体现出来,代码如下:
pragma pack(4) /* n = 1, 2, 4, 8, 16 */
struct test_t{
int a;
char b;
short c;
char d[6];
}ttt;
void print_hex_data(char *info, char *data, int len)
{
int i;
dbg_printf(“%s:\n\r”, info);
for(i = 0; i < len; i++){
dbg_printf(“%02x “, (unsigned char)data[i]);
if (0 == ((i+1) % 32))
dbg_printf(“\n”);
}
dbg_printf(“\n\r”);
}
int main()
{
ttt.a = 0x1a2a3a4a;
ttt.b = 0x1b;
ttt.c = 0x1c2c;
char *s = “123456”;
memcpy(ttt.d, s, 6);
print_hex_data(“struct_data”, (char *)&ttt, sizeof(struct test_t));
return 0;
}
pragma pack(1)的结果:
4a 3a 2a 1a 1b 2c 1c 31 32 33 34 35 36
pragma pack(2)的结果:
4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36
pragma pack(4)的结果:
4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00
pragma pack(8)的结果:
4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00
pragma pack(16)的结果:
4a 3a 2a 1a 1b 00 2c 1c 31 32 33 34 35 36 00 00
StructLayout特性
公共语言运行库利用StructLayoutAttribute控制类或结构的数据字段在托管内存中的物理布局,即类或结构需要按某种方式排列。如果要将类传递给需要指定布局的非托管代码,则显式控制类布局是重要的。它的构造函数中用 LayoutKind值初始化 StructLayoutAttribute 类的新实例。 LayoutKind.Sequential 用于强制将成员按其出现的顺序进行顺序布局。
StructLayout特性允许我们控制Structure语句块的元素在内存中的排列方式,以及当这些元素被传递给外部DLL时,运行库排列这些元素的方式。Visual Basic结构的成员在内存中的顺序是按照它们出现在源代码中的顺序排列的,尽管编译器可以自由的插入填充字节来安排这些成员,以便使得16位数值用子边界对齐,32位数值用双字边界对齐。
使用这种排列(未压缩布局)提供的性能最佳。
在Visual Basic 6的用户自定义结构是未压缩的,而且我们不可以改变这一默认设置。在VB.NET中可以改变这种设置,并且可以通过System.Runtime.InteropServices.StructLayout 特性精确的控制每一个结构成员的位置。
System.Runtime.InteropServices.StructLayout 允许的值有StructLayout.Auto StructLayout.Sequential StructLayout.Explicit.
1.Sequential,顺序布局,比如
struct S1
{
int a;
int b;
}
那么默认情况下在内存里是先排a,再排b
也就是如果能取到a的地址,和b的地址,则相差一个int类型的长度,4字节
[StructLayout(LayoutKind.Sequential)]
struct S1
{
int a;
int b;
}
这样和上一个是一样的.因为默认的内存排列就是Sequential,也就是按成员的先后顺序排列.
2.Explicit,精确布局
需要用FieldOffset()设置每个成员的位置
这样就可以实现类似c的公用体的功能
[StructLayout(LayoutKind.Explicit)]
struct S1
{
[FieldOffset(0)]
int a;
[FieldOffset(0)]
int b;
}
这样a和b在内存中地址相同
StructLayout特性支持三种附加字段:CharSet、Pack、Size。
· CharSet定义在结构中的字符串成员在结构被传给DLL时的排列方式。可以是Unicode、Ansi或Auto。
默认为Auto,在WIN NT/2000/XP中表示字符串按照Unicode字符串进行排列,在WIN 95/98/Me中则表示按照ANSI字符串进行排列。
· Pack定义了结构的封装大小。可以是1、2、4、8、16、32、64、128或特殊值0。特殊值0表示当前操作平台默认的压缩大小。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct LIST_OPEN
{
public int dwServerId;
public int dwListId;
public System.UInt16 wRecordSize;
public System.UInt16 wDummy;
public int dwFileSize;
public int dwTotalRecs;
public NS_PREFETCHLIST sPrefetch;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 24)]
public string szSrcMach;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 24)]
public string szSrcComp;
}
此例中用到MashalAs特性,它用于描述字段、方法或参数的封送处理格式。用它作为参数前缀并指定目标需要的数据类型。
例如,以下代码将两个参数作为数据类型长指针封送给 Windows API 函数的字符串 (LPStr):
[MarshalAs(UnmanagedType.LPStr)]
String existingfile;
[MarshalAs(UnmanagedType.LPStr)]
String newfile;
注意结构作为参数时候,一般前面要加上ref修饰符,否则会出现错误:对象的引用没有指定对象的实例。
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )]
public static extern bool GetVersionEx2( ref OSVersionInfo2 osvi );
C# 使用 StructLayoutAttribute 时 C# /C++ 内存空间分配与成员对齐问题
1. 使用场景
公共语言运行时控制数据字段的类或结构在托管内存中的物理布局。但是,如果想要将类型传递到非托管代码,需要使用 StructLayout 属性。
2. 内存分配问题。
如果不显示的设置内存对齐方式(通过StructLayout.Pack属性决定), C#默认是以4个字节(byte)为单位,会出现“多分配”内存的情况。 例如:
Class Example
{
public byte b1;
public char c2;
public int i3;
}
默认情况下(StructLayout.Pack = 4),Framework编译器会为example对象分配8个字节(字段c2后面会补齐2个byte )。每个成员的索引和大小结果为:
Size: 8
b1 Offset: 0, lenght =1,
c2 Offset: 1, length = 1,
i3 offset: 4, length = 4
C++ 编译器的分配方式则为:
Size: 6
b1 Offset: 0, lenght =1,
c2 Offset: 1, length = 1,
i3 offset: 2, length = 4
由于内存分配的大小不一致,导致在传递对象marshal的时候回出现问题!!
3. 解决方案。
3.1 通过设置StructLayout.Pack的值来达到内存大小分配一致。
例如在上面的例子中,设置StructLayout.Pack =2 或者 StructLayout.Pack =1. 但是这种方法可能会因为硬件约束导致性能或者其他问题。
3.2 通过预留字段来“补齐”内存分配。
这种做法在实际项目中使用较多,既保证了长度一致,也为以后扩展提供了一种容错的可能。 如果采取这种方式,重新定义如下:
Class Example
{
public byte b1;
public char c2;
public int i3;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 2)]
public byte[] reserved;
}
至此,C#和C++分配的内存大小同为8,问题解决 :)
struct实例字段的内存布局(Layout)和大小(Size)
在C/C++中,struct类型中的成员的一旦声明,则实例中成员在内存中的布局(Layout)顺序就定下来了,即与成员声明的顺序相同,并且在默认情况下总是按照结构中占用空间最大的成员进行对齐(Align);当然我们也可以通过设置或编码来设置内存对齐的方式.
然而在.net托管环境中,CLR提供了更自由的方式来控制struct中Layout:我们可以在定义struct时,在struct上运用StructLayoutAttribute特性来控制成员的内存布局。默认情况下,struct实例中的字段在栈上的布局(Layout)顺序与声明中的顺序相同,即在struct上运用[StructLayoutAttribute(LayoutKind.Sequential)]特性,这样做的原因是结构常用于和非托管代码交互的情形。
如果我们正在创建一个与非托管代码没有任何互操作的struct类型,我们很可能希望改变C#编译器的这种默认规则,因此LayoutKind除了Sequential成员之外,还有两个成员Auto和Explicit,给StructLayoutAttribute传入LayoutKind.Auto可以让CLR按照自己选择的最优方式来排列实例中的字段;传入LayoutKind.Explicit可以使字段按照我们的在字段上设定的FieldOffset来更灵活的设置字段排序方式,但这种方式也挺危险的,如果设置错误后果将会比较严重。下面就看几个示例,算下四个struct各占多少Byte?
1.[StructLayout(LayoutKind.Sequential)]
{
bool i; //1Byte
double c;//8byte
bool b; //1byte
}
sizeof(StructDeft)得到的结果是24byte!啊哈,本身只有10byte的数据却占有了24byte的内存,这是因为默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同(8+8+8=24),即按照结构中占用空间最大的成员进行对齐(Align)。10byte的数据却占有了24byte,严重地浪费了内存,所以如果我们正在创建一个与非托管代码没有任何互操作的struct类型,最好还是不要使用默认的StructLayoutAttribute(LayoutKind.Sequential)特性。
2.[StructLayout(LayoutKind.Explicit)]
struct BadStruct
{
[FieldOffset(0)]
public bool i; //1Byte
[FieldOffset(0)]
public double c;//8byte
[FieldOffset(0)]
public bool b; //1byte
}
sizeof(BadStruct)得到的结果是9byte,显然得出的基数9显示CLR并没对结构体进行任何内存对齐(Align);本身要占有10byte的数据却只占了9byte,显然有些数据被丢失了,这也正是我给struct取BadStruct作为名字的原因。如果在struct上运用了[StructLayout(LayoutKind.Explicit)],计算FieldOffset一定要小心,例如我们使用上面BadStruct来进行下面的测试:
e.c = 0;
e.i = true;
Console.WriteLine(e.c);
输出的结果不再是0了,而是4.94065645841247E-324,这是因为e.c和e.i共享同一个byte,执行“e.i = true;时”也改变了e.c,CPU在按照浮点数的格式解析e.c时就得到了这个结果.所以在运用LayoutKind.Explicit时千万别把FieldOffset算错了:)
3.[StructLayout(LayoutKind.Auto)]
sizeof(StructAuto)得到的结果是12byte。下面来测试下这StructAuto的三个字段是如何摆放的:
{
StructAuto s = new StructAuto();
Console.WriteLine(string.Format("i:{0}", (int)&(s.i)));
Console.WriteLine(string.Format("c:{0}", (int)&(s.c)));
Console.WriteLine(string.Format("b:{0}", (int)&(s.b)));
}
// 测试结果:
i:1242180
c:1242172
b:1242181
即CLR会对结构体中的字段顺序进行调整,将i调到c之后,使得StructAuto的实例s占有尽可能少的内存,并进行4byte的内存对齐(Align),字段顺序调整结果如下图所示:
4.空struct实例的Size
无论运用上面LayoutKind的Explicit、Auto还是Sequential,得到的sizeof(EmptyStct)都是1byte。
结论:
默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align);
使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset;
使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行4byte的内存对齐(Align)。