内存对齐问题

本文围绕C语言结构体的内存对齐展开。介绍了#pragma pack编译指示指定结构和联合成员紧凑对齐的方法,阐述了内存对齐的概念、原因及编译器的处理方式。还给出避免内存对齐影响的技巧,以及VC6中对齐选项的使用方法,并提供了验证代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

typedef struct
{
  UINT32  NumElements;
  union
  {
    UINT32  ObjectHandle;
  } Entry;
} STR_ARRAY, *PSTR_ARRAY;

还有这两句
#pragma pack(push, 1)
#pragma pack(pop)

#pragma pack([ n ])
该指令指定结构和联合成员的紧凑对齐。而一个完整的转换单元的结构和联合的紧凑对齐由/Zp选项设置。紧凑对齐用pack编译指示在数据说明层设置。该编译指示在其出现后的第一个结构或联合说明处生效。该编译指示对定义无效。当你使用 #pragma pack(n) 时, 这里n1、2、4、816 。第一个结构成员之后的每个结构成员都被存储在更小的成员类型或n 字节界限内。如果你使用无参量的#pragma pack,结构成员被紧凑为以/Zp 指定的值。该缺省/Zp紧凑值为/Zp8。

编译器也支持以下增强型语法:
#pragma pack([[{push|pop},][标识符,]][n])

若不同的组件使用pack 编译指示指定不同的紧凑对齐, 这个语法允许你把程序组件组合为一个单独的转换单元。带push 参量的pack编译指示的每次出现将当前的紧凑对齐存储到一个内部编译器堆栈中。编译指示的参量表从左到右读取。如果你使用push , 则当前紧凑值被存储起来;如果你给出一个n的值, 该值将成为新的紧凑值。若你指定一个标识符,即你选定一个名称,则该标识符将和这个新的的紧凑值联系起来。带一个pop参量的pack编译指示的每次出现都会检索内部编译器堆栈顶的值,并且使该值为新的紧凑对齐值。如果你使用pop 参量且内部编译器堆栈是空的,则紧凑值为命令行给定的值, 并且将产生一个警告信息。若你使用pop 且指定一个n的值,该值将成为新的紧凑值。若你使用pop且指定一个标识符,所有存储在堆栈中的值将从栈中删除,直到找到一个匹配的标识符,这个与标识符相关的紧凑值也从栈中移出,并且这个仅在
标识符入栈之前存在的紧凑值成为新的紧凑值。如果未找到匹配的标识符,将使用命令行设置的紧凑值,并且将产生一个一级警告。缺省紧凑对齐为8 。

pack编译指示的新的增强功能让你编写头文件, 确保在遇到该头文件的前后的紧凑值是一样的。

什么是内存对齐?

考虑下面的结构:
 struct foo
 {
   char c1;
   short s;
   char c2;
   int i;
 };

假设这个结构的成员在内存中是紧凑排列的,假设c1的地址是0,那么s的地址就应该是1,c2的地址就是3i的地址就是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》。
 字(word),双字(dword),和四字在自然边界上不需要在内存中对齐。(对“字“,“双字“,和“四字“来说,自然边界分别是偶数地址,可以被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;
}

回答: 在设计结构体时,我们可以通过调整成员的顺序和使用#pragma pack指令来实现内存对齐和节省空间的目标。为了满足对齐要求,我们可以将占用空间小的成员尽量集中在一起,这样可以减少内存浪费。例如,可以调整结构体成员的顺序,将占用空间小的成员放在一起,以便在内存中占用连续的空间。另外,我们还可以使用#pragma pack指令来修改编译器的默认对齐数,以实现更灵活的内存对齐方式。通过设置合适的对齐数,我们可以在满足对齐要求的同时,尽量减少结构体的总大小。内存对齐的存在是为了提高处理器读写数据的效率。处理器通常以块为单位进行数据读写,如果结构体没有进行内存对齐,可能需要多次读写才能完成一个操作,从而降低了效率。因此,通过进行内存对齐,可以减少处理器的读写次数,提高数据访问的效率。 #### 引用[.reference_title] - *1* *2* [struct结构体的内存对齐](https://blog.youkuaiyun.com/chirrupy_hamal/article/details/102634695)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [关于struct内存对齐问题](https://blog.youkuaiyun.com/element137/article/details/69075284)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值