#pragma pack(n)------内存对齐问题

在C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同

 

为了能使CPU对变量进行高效快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”。例如对于4字节的int类型变量,其起始地址应位于4字节边界上,即起始地址能够被4整除。变量的对齐规则如下(32位系统):


  Type                                                Alignment

 

  char                                        在字节边界上对齐
  short (16-bit)                          双字节边界上对齐     2
  int and long (32-bit)              4字节边界上对齐
  float                                        4字节边界上对齐
  double                                    8字节边界上对齐
      
 structures
      单独考虑结构体的个成员,它们在不同的字节边界上对齐。  其中最大的字节边界数就是该结构的字节边界数(LINUX下面不是这样的,下面我会解释)

      如果结构体中有结构体成员,那么这是一个递归的过程。
      设编译器设定的最大对齐字节边界数为n,对于结构体中的某一成员item,它相对于结构首地址的实际字节对齐数
      目X应该满足以下规则:
      X = min(n, sizeof(item))   // 也就是说虽然可以手动规定程序的字节对齐大小,但是还是和程序默认的取最小值

 

例如,对于结构体
      struct {
      char a;
      long b;
      } T;


      当位于32位系统,n=8时:
      a的偏移为0,
      b的偏移为4,中间填充了3个字节, b的X为4;


      当位于32位系统,n=2时:
      a的偏移为0,
      b的偏移为2,中间填充了1个字节,b的X为2;


      结构体的sizeof:
      设结构体的最后一个成员为LastItem,其相对于结构体首地址的偏移为offset(LastItem),其大小为
      sizeof(LastItem),结构体的字节对齐数为N,则:结构体的sizeof 为: 若offset(LastItem)+
      sizeof(LastItem)能够被N整除,那么就是offset(LastItem)+
      sizeof(LastItem),否则,在后面填充,直到能够被N整除。


      另外:
      1) 对于空结构体,sizeof == 1;因为必须保证结构体的每一个实例在内存中都有独一无二的地址.
      2)结构体的静态成员不对结构体的大小产生影响,因为静态变量的存储位置与结构体的实例地址无关。例如:
      struct {static int I;} T; struct {char a; static int I;} T1;
      sizeof(T) == 1; sizeof(T1) == 1;
      下面是CSDN上提出的问题(原文<>:http://community.csdn.net/Expert/TopicView3.asp?id=3804035)
      ---------------------------------------

 

 

#pragma pack(8)

      struct s1{
      short a;
      long b;
      };

      struct s2{
      char c;
      s1 d;
      long long e;
      };

      #pragma pack()

      问
      1.sizeof(s2) = ?
      2.s2的c后面空了几个字节接着是d?
      ---------------------------------------


      sizeof(S2)结果为24    //  linux下是20,这个就是和一般编程工具的区别,在linux下,它的对齐规则是上一个如果小于相邻的下一个,就按下一个的大小对齐,而不是按所有大小的里面最大的!!!
      成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.
      也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.
      S1中,成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;
      S2 中,c和S1中的a一样,按1字节对齐,而d
      是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e按8字节对齐)整除.这样,一共使用了24个字节.
      a b
      S1的内存布局:11**,1111,
      c S1.a S1.b d
      S2的内存布局:1***,11**,1111,****11111111

      这里有三点很重要:
      1.每个成员分别按自己的方式对齐,并能最小化长度
      2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度
      3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

  

  

对于数组,比如:
      char a[3];这种,它的对齐方式和分别写3个char是一样的.也就是说它还是按1个字节对齐.
      如果写: typedef char Array3[3];
      Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度.
      不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个.

 

 

 

 一下是更多关于对齐的内容:

 如下一段代码:
      #pragma pack(4)
      class TestB
      {
      public:
      int aa;
      char a;
      short b;
      char c;
      };
      int nSize = sizeof(TestB);
      这里nSize结果为12,在预料之中。
        现在去掉第一个成员变量为如下代码:
      #pragma pack(4)
      class TestC
      {
      public:
      char a;
      short b;
      char c;
      };
      int nSize = sizeof(TestC);
      按照正常的填充方式nSize的结果应该是8,为什么结果显示nSize为6呢?
      事实上,很多人对#pragma pack的理解是错误的。
      #pragma pack规定的对齐长度,实际使用的规则是:
      结构,联合,或者类的数据成员,第一个放在偏移为0的地方,以后每个数据成员的对齐,按照#pragma
      pack指定的数值和这个数据成员自身长度中,比较小的那个进行。
      也就是说,当#pragma pack的值等于或超过所有数据成员长度的时候,这个值的大小将不产生任何效果。
      而结构整体的对齐,则按照结构体中最大的数据成员和 #pragma pack指定值 之间,较小的那个进行。

      具体解释


      #pragma pack(4)   //四个就换行
      class TestB
      {
      public:
      int aa; //第一个成员,放在[0,3]偏移的位置,
      char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
      short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
      char c; //第四个,自身长为1,放在[8]的位置。
      };
      这个类实际占据的内存空间是9字节
      类之间的对齐,是按照类内部最大的成员的长度,和#pragma pack规定的值之中较小的一个对齐的。
      所以这个例子中,类之间对齐的长度是min(sizeof(int),4),也就是4。
      9按照4字节圆整的结果是12,所以sizeof(TestB)是12。 4+

 

      如果
      #pragma pack(2)
      class TestB
      {
      public:
      int aa; //第一个成员,放在[0,3]偏移的位置,
      char a; //第二个成员,自身长为1,#pragma pack(4),取小值,也就是1,所以这个成员按一字节对齐,放在偏移[4]的位置。
      short b; //第三个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[6,7]的位置。
      char c; //第四个,自身长为1,放在[8]的位置。
      };
      //可以看出,上面的位置完全没有变化,只是类之间改为按2字节对齐,9按2圆整的结果是10。
      //所以 sizeof(TestB)是10。   4+2+2

 


      最后看原贴:
      现在去掉第一个成员变量为如下代码:
      #pragma pack(4)
      class TestC
      {
      public:
      char a;//第一个成员,放在[0]偏移的位置,
      short b;//第二个成员,自身长2,#pragma pack(4),取2,按2字节对齐,所以放在偏移[2,3]的位置。
      char c;//第三个,自身长为1,放在[4]的位置。
      };
      //整个类的大小是5字节,按照min(sizeof(short),4)字节对齐,也就是2字节对齐,结果是6
      //所以sizeof(TestC)是6。

      感谢 Michael 提出疑问,在此补充:
      #pragma pack


      当数据定义中出现__declspec( align()
      )时,指定类型的对齐长度还要用自身长度和这里指定的数值比较,然后取其中较大的。最终类/结构的对齐长度也需要和这个数值比较,然后取其中较大的。

      可以这样理解, __declspec( align() ) 和 #pragma
      pack是一对兄弟,前者规定了对齐的最小值,后者规定了对齐的最大值,两者同时出现时,前者拥有更高的优先级。
      __declspec ( align()
      )的一个特点是,它仅仅规定了数据对齐的位置,而没有规定数据实际占用的内存长度,当指定的数据被放置在确定的位置之后,其后的数据填充仍然是按照#pragma
      pack规定的方式填充的,这时候类/结构的实际大小和内存格局的规则是这样的:
      在__declspec( align () )之前,数据按照#pragma pack规定的方式填充,如前所述。当遇到__declspec(
      align() )的时候,首先寻找距离当前偏移向后最近的对齐点(满足对齐长度为 max(数据自身长度,指定值)
      ),然后把被指定的数据类型从这个点开始填充,其后的数据类型从它的后面开始,仍然按照#pragma pack填充,直到遇到下一个__declspec(
      align() )。
      当所有数据填充完毕,把结构的整体对齐数值和__declspec( align() )规定的值做比较,取其中较大的作为整个结构的对齐长度。
      特别的,当__declspec( align() )指定的数值比对应类型长度小的时候,这个指定不起作用。

 

 

       首先请大家先看下面代码:
          typedef struct
          {
            UINT32  NumElements;
            union
            {
               UINT32  ObjectHandle;
             }Entry;
           }STR_ARRAY, *PSTR_ARRAY;
          还有这两句#pragma pack(push, 1)
          #pragma pack(pop)
          #pragma  pack( [ n ] )
          该指令指定结构和联合成员的紧凑对齐。而一个完整的转换单元的结构和联合的紧凑对齐由/ Z p 选项设置。紧凑对齐用p a c e
      编译指示在数据说明层设置。该编译指示在其出现后的第一个结构或联合说明处生效。该编译指示对定义无效。当你使用#pragma  pack ( n )
      时, 这里n 为1 、2 、4 、8 或1 6 。第一个结构成员之后的每个结构成员都被存储在更小的成员类型或n
      字节界限内。如果你使用无参量的#pragma  pack , 结构成员被紧凑为以/ Z p 指定的值。该缺省/ Z p 紧凑值为/ Z p 8 。
          编译器也支持以下增强型语法:
          #pragma  pack( [ [ { p u s h | p o p } , ] [ 标识符, ] ] [ n] )若不同的组件使用p
      a c k 编译指示指定不同的紧凑对齐, 这个语法允许你把程序组件组合为一个单独的转换单元。带p u s h 参量的p a c k
      编译指示的每次出现将当前的紧凑对齐存储到一个内部编译器堆栈中。编译指示的参量表从左到右读取。如果你使用p u s h , 则当前紧凑值被存储起来;
      如果你给出一个n 的值, 该值将成为新的紧凑值。若你指定一个
      标识符, 即你选定一个名称, 则该标识符将和这个新的的紧凑值联系起来。带一个p o p 参量的p a c k
      编译指示的每次出现都会检索内部编译器堆栈顶的值,并且使该值为新的紧凑对齐值。如果你使用p o p
      参量且内部编译器堆栈是空的,则紧凑值为命令行给定的值, 并且将产生一个警告信息。若你使用p o p 且指定一
      个n 的值, 该值将成为新的紧凑值。若你使用p o p 且指定一个标识符,  所有存储在堆栈中的值将从栈中删除, 直到找到一个匹配的标识符,
      这个与标识符相关的紧凑值也从栈中移出, 并且这个仅在标识符入栈之前存在的紧凑值成为新的紧凑值。如果未找到匹配的标识符, 将使用命令行设置的紧凑值,
      并且将产生一个一级警告。缺省紧凑对齐为8 。p a c k 编译指示的新的增强功能让你编写头文件, 确保在遇到该头文件的前后的紧凑值是一样的。
          什么是内存对齐
          考虑下面的结构:
               struct foo
               {
                 char c1;
                 short s;
                 char c2;
                 int i;
                };
         
          假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3,i的地址就是4。也就是
          c1 00000000, s 00000001, c2 00000003, i 00000004。
          可是,我们在Visual c/c++ 6中写一个简单的程序:
               struct foo a;
          printf("c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
          运行,输出:
               c1 00000000, s 00000002, c2 00000004, i 00000008。
          为什么会这样?这就是内存对齐而导致的问题。
      为什么会有内存对齐
          以下内容节选自《Intel Architecture 32 Manual》。
         
字,双字,和四字在自然边界上不需要在内存中对齐。(对字,双字,和四字来说,自然边界分别是偶数地址,可以被4整除的地址,和可以被8整除的地址。)
         
      无论如何,为了提高程序的性能,数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。
         
      一个字或双字操作数跨越了4字节边界,或者一个四字操作数跨越了8字节边界,被认为是未对齐的,从而需要两次总线周期来访问内存。一个字起始地址是奇数但却没有跨越字边界被认为是对齐的,能够在一个总线周期中被访问。
         
      某些操作双四字的指令需要内存操作数在自然边界上对齐。如果操作数没有对齐,这些指令将会产生一个通用保护异常(#GP)。双四字的自然边界是能够被16
      整除的地址。其他的操作双四字的指令允许未对齐的访问(不会产生通用保护异常),然而,需要额外的内存总线周期来访问内存中未对齐的数据。
      编译器对内存对齐的处理
          缺省情况下,c/c++编译器默认将结构、栈中的成员数据进行内存对齐。因此,上面的程序输出就变成了:
      c1 00000000, s 00000002, c2 00000004, i 00000008。
      编译器将未对齐的成员向后移,将每一个都成员对齐到自然边界上,从而也导致了整个结构的尺寸变大。尽管会牺牲一点空间(成员之间有空洞),但提高了性能。
      也正是这个原因,我们不可以断言sizeof(foo) == 8。在这个例子中,sizeof(foo) == 12。
      如何避免内存对齐的影响
          那么,能不能既达到提高性能的目的,又能节约一点空间呢?有一点小技巧可以使用。比如我们可以将上面的结构改成:
          struct bar
          {
              char c1;
              char c2;
              short s;
              int i;
          };
          这样一来,每个成员都对齐在其自然边界上,从而避免了编译器自动对齐。在这个例子中,sizeof(bar) == 8。
         
      这个技巧有一个重要的作用,尤其是这个结构作为API的一部分提供给第三方开发使用的时候。第三方开发者可能将编译器的默认对齐选项改变,从而造成这个结构在你的发行的DLL中使用某种对齐方式,而在第三方开发者哪里却使用另外一种对齐方式。这将会导致重大问题。
          比如,foo结构,我们的DLL使用默认对齐选项,对齐为
      c1 00000000, s 00000002, c2 00000004, i 00000008,同时sizeof(foo) == 12。
      而第三方将对齐选项关闭,导致
          c1 00000000, s 00000001, c2 00000003, i 00000004,同时sizeof(foo) == 8。
      如何使用c/c++中的对齐选项
          vc6中的编译选项有 /Zp[1|2|4|8|16]
      ,/Zp1表示以1字节边界对齐,相应的,/Zpn表示以n字节边界对齐。n字节边界对齐的意思是说,一个成员的地址必须安排在成员的尺寸的整数倍地址上或者是n的整数倍地址上,取它们中的最小值。也就是:
          min ( sizeof ( member ),  n)
          实际上,1字节边界对齐也就表示了结构成员之间没有空洞。
          /Zpn选项是应用于整个工程的,影响所有的参与编译的结构。
          要使用这个选项,可以在vc6中打开工程属性页,c/c++页,选择Code Generation分类,在Struct member
      alignment可以选择。
          要专门针对某些结构定义使用对齐选项,可以使用#pragma pack编译指令。指令语法如下:
      #pragma pack( [ show ] | [ push | pop ] [, identifier ] , n  )
          意义和/Zpn选项相同。比如:
          #pragma pack(1)
          struct foo_pack
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          #pragma pack()
          栈内存对齐
          我们可以观察到,在vc6中栈的对齐方式不受结构成员对齐选项的影响。(本来就是两码事)。它总是保持对齐,而且对齐在4字节边界上。
          验证代码
          #include <stdio.h>
          struct foo
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          struct bar
          {
              char c1;
              char c2;
              short s;
              int i;
          };
          #pragma pack(1)
          struct foo_pack
          {
              char c1;
              short s;
              char c2;
              int i;
          };
          #pragma pack()

          int main(int argc, char* argv[])
          {
              char c1;
              short s;
              char c2;
              int i;
          struct foo a;
          struct bar b;
          struct foo_pack p;
          printf("stack c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&c1 - (unsigned int)(void*)&i,
              (unsigned int)(void*)&s - (unsigned int)(void*)&i,
              (unsigned int)(void*)&c2 - (unsigned int)(void*)&i,
              (unsigned int)(void*)&i - (unsigned int)(void*)&i);
          printf("struct foo c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&a.c1 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.s - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.c2 - (unsigned int)(void*)&a,
              (unsigned int)(void*)&a.i - (unsigned int)(void*)&a);
          printf("struct bar c1 %p, c2 %p, s %p, i %p/n",
              (unsigned int)(void*)&b.c1 - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.c2 - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.s - (unsigned int)(void*)&b,
              (unsigned int)(void*)&b.i - (unsigned int)(void*)&b);
          printf("struct foo_pack c1 %p, s %p, c2 %p, i %p/n",
              (unsigned int)(void*)&p.c1 - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.s - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.c2 - (unsigned int)(void*)&p,
              (unsigned int)(void*)&p.i - (unsigned int)(void*)&p);
          printf("sizeof foo is %d/n", sizeof(foo));
          printf("sizeof bar is %d/n", sizeof(bar));
          printf("sizeof foo_pack is %d/n", sizeof(foo_pack));
         
          return 0;

          } 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值