#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()

我们知道结构体内存对齐字节可以通过#pragma pack(n) 的方式来指定。

但是,有没有想过一个问题,某些时候我想4字节对齐,有些时候我又想1字节或者8字节对齐,那么怎么解决这个问题呢?

此时,#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()应运而生。

看测试代码:(说明,64位GCC,默认8字节对齐)

屏蔽了的代码选别看,只看这个结构体,在默认8字节对齐的方式下,sizeof大小为24个字节,这不再做分析,之前随笔分析过了。

然后我加上强制4字节对齐之后:

那么现在,我再新建一个结构体B,内容和结构体C一样,只是对齐方式向分别采取不同的方式:

复制代码

#include <stdio.h>

#pragma   pack(4) 
struct C {
    double d;
    char b;
    int a;
    short c;
};
#pragma pack() 
struct B {
    double d;
    char b;
    int a;
    short c;
};

复制代码

像上面那样处理之后,输出:先打印结构C,再打印结构B

这说明了,在强制4字节对齐之后,我加上#pragma pack() ,能够让程序恢复默认对齐(这里是8字节)状态。

#pragma pack() 能够取消自定义的对齐方式,恢复默认对齐。

继续测试:

复制代码

#pragma   pack(4) 
struct CC {
    double d;
    char b;
    int a;
    short c;
};
#pragma pack(pop) 
struct BB{
    double d;
    char b;
    int a;
    short c;
};

复制代码

输出:

好像没什么作用的感觉,那么再加上一个#pragma pack(push)试试呢?

复制代码

#include <stdio.h>

#pragma pack(push) 
#pragma   pack(4) 
struct CC {
    double d;
    char b;
    int a;
    short c;
};
#pragma pack(pop) 
struct BB{
    double d;
    char b;
    int a;
    short c;
};
int main(void)
{
    
    printf("%u\n%u\n",sizeof(struct CC),sizeof(struct BB));
    return 0;
}

复制代码

这样似乎改变了,有不同的地方体现了出来。

#pragma pack(push):

英文单词push是“压入”的意思。编译器编译到此处时将保存对齐状态(保存的是push指令之前的对齐状态)。

#pragma pack(pop):

英文单词pop是”弹出“的意思。编译器编译到此处时将恢复push指令前保存的对齐状态(请在使用该预处理命令之前使用#pragma pack(push))。

push和pop是一对应该同时出现的名词,只有pop没有push不起作用,只有push没有pop可以保持之前对齐状态(但是这样就没有使用push的必要了)。

这样就可以知道,当我们想要一个结构体按照4字节对齐时,可以使用#pragma   pack(4) ,最后又想使用默认对齐方式时,可以使用#pragma pack() ;

也可以使用:

#pragma pack(push) 
#pragma pack(4)

struct。。。

#pragma pack(pop)

这样在push和pop之间的结构体就可以按照pack指定的字节(这里是4字节对齐方式),而pop之后的结构体按照#pragma pack(push) 前对齐方式。

eg:

 

复制代码

#include <stdio.h>
#pragma   pack(2) 
#pragma pack(push) 
#pragma   pack(4) 
struct CC {
    double d;
    char b;
    int a;
    short c;
};
 
#pragma   pack(1) 
struct BB{
    double d;
    char b;
    int a;
    short c;
};
#pragma pack(pop)
struct AA{
    double d;
    char b;
    int a;
    short c;
};
int main(void)
{
    
    printf("%u\n%u\n%u\n",sizeof(struct CC),sizeof(struct BB),sizeof(struct AA));
    return 0;
}

复制代码

 

先按照2字节对齐,然后push保存2字节对齐,然后又强制4字节对齐,打印CC为20字节,然后强制1字节对齐,打印BB为15字节,然后pop,pop会让编译器回到push之前的对齐方式(这里是2字节对齐),打印AA(按照2字节对齐)16字节。

 

 注意,#pragma pack() 取消自定义对齐方式,恢复默认方式,而push之后pop是回到push指令之前的对齐方式。

eg:

复制代码

#include <stdio.h>
#pragma   pack(2) 
#pragma pack(push) 
#pragma   pack(4) 
struct CC {
    double d;
    char b;
    int a;
    short c;
};
 
#pragma   pack(1) 
struct BB{
    double d;
    char b;
    int a;
    short c;
};
#pragma pack()
struct AA{
    double d;
    char b;
    int a;
    short c;
};
int main(void)
{
    
    printf("%u\n%u\n%u\n",sizeof(struct CC),sizeof(struct BB),sizeof(struct AA));
    return 0;
}

复制代码

只把pop改成pack()打印如下:

最后回到的不是2字节对齐,而是默认的8字节对齐。

还有延伸点:

上图红色处等价于下面屏蔽的两句。

语法:
#pragma pack( [show] | [push | pop] [, identifier], n )

说明:
1,pack提供数据声明级别的控制,对定义不起作用;
2,调用pack时不指定参数,n将被设成默认值;
3,一旦改变数据类型的alignment,直接效果就是占用memory的减少,但是performance会下降;

语法具体分析:
1,show:可选参数;显示当前packing aligment的字节数,以warning message的形式被显示;
2,push:可选参数;将当前指定的packing alignment数值进行压栈操作,这里的栈是the internal compiler stack,同时设置当前的packing alignment为n;如果n没有指定,则将当前的packing alignment数值压栈;
3,pop:可选参数;从internal compiler stack中删除最顶端的record;如果没有指定n,则当前栈顶record即为新的packing alignment数值;如果指定了n,则n将成为新的packing aligment数值;如果指定了identifier,则internal compiler stack中的record都将被pop直到identifier被找到,然后pop出identitier,同时设置packing alignment数值为当前栈顶的record;如果指定的identifier并不存在于internal compiler stack,则pop操作被忽略;
4,identifier:可选参数;当同push一起使用时,赋予当前被压入栈中的record一个名称;当同pop一起使用时,从internal compiler stack中pop出所有的record直到identifier被pop出,如果identifier没有被找到,则忽略pop操作;
5,n:可选参数;指定packing的数值,以字节为单位;

另外:

__attribute(aligned(n)),让所作用的数据成员对齐在n字节的自然边界上;如果结构中有成员的长度大于n,则按照最大成员的长度来对齐;
__attribute((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐

 

 

1.什么是对齐?为什么要对齐?

现代计算机中内存空间都是按照byte划分的,从理论上讲似乎对任何类型的变量的访问可以从任何地址开始,但实际情况是在访问特定变量的时候经常在特定的内存地址访问,这就需要各类型数据按照一定的规则在空间上排列,而不是顺序的一个接一个的排放,这就是对齐。
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对数据存放进行对齐,会在存取效率上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始的地方,那么一个读周期就可以读出,而如果存放在奇地址开始的地方,就可能会需要2个读周期,并对两次读出的结果的高低字节进行拼凑才能得到该int数据。显然在读取效率上下降很多。这也是空间和时间的博弈。
2.pragma pack语法

用sizeof运算符求算某结构体所占空间时,并不是简单地将结构体中所有元素各自占的空间相加,这里涉及到内存字节对齐的问题,有时候为了内存对齐需要补齐空字节。通常写程序的时候,不需要考虑对齐问题。编译器会替我们选择适合目标平台的对齐策略。当然,我们也可以通知给编译器传递预编译指令而改变对指定数据的对齐方法。
语法:#pragma pack( [show] | [push | pop] [, identifier], n )
作用:指定结构,联合和类的包对齐方式(pack alignment)
show(optical):显示当前packing aligment的字节数,以warning message的形式显示。
push(optical):
Pushes the current packing alignment value on the internal compiler stack, and sets the current packing alignment value to n. If n is not specified, the current packing alignment value is pushed.
pop(optical):
Removes the record from the top of the internal compiler stack. If n is not specified with pop, then the packing value associated with the resulting record on the top of the stack is the new packing alignment value. If n is specified, for example, #pragma pack(pop, 16), n becomes the new packing alignment value. If you pop with identifier, for example, #pragma pack(pop, r1), then all records on the stack are popped until the record that hasidentifier is found. That record is popped and the packing value associated with the resulting record on the top of is the stack the new packing alignment value. If you pop with an identifier that is not found in any record on the stack, then the pop is ignored.
identifier(optional):
When used with push, assigns a name to the record on the internal compiler stack. When used with pop, pops records off the internal stack until identifieris removed; if identifier is not found on the internal stack, nothing is popped.
n (optional):
Specifies the value, in bytes, to be used for packing. If the compiler option /Zp is not set for the module, the default value for n is 8. Valid values are 1, 2, 4, 8, and 16. The alignment of a member will be on a boundary that is either a multiple of n or a multiple of the size of the member, whichever is smaller.
3.结构体对齐规则

结构体中各个成员按照它们被声明的顺序在内存中顺序存储。
1)将结构体内所有数据成员的长度值相加,记为sum_a; 
2)将各数据成员内存对齐,按各自对齐模数而填充的字节数累加到和sum_a上,记为sum_b。对齐模数是【该数据成员所占内存】与【#pragma pack指定的数值】中的较小者。
3)将和sum_b向结构体模数对齐,该模数是【#pragma pack指定的数值】、【未指定#pragma pack时,系统默认的对齐模数8字节】和【结构体内部最大的基本数据类型成员】长度中数值较小者。结构体的长度应该是该模数的整数倍。
3.1 基本数据类型所占内存大小


以下例子均按32bit编译器处理。
3.2 Test1

#pragma pack(4)
struct Test1
{
    char c;
    short sh;
    int a;
    float f;
    int *p;
    char *s;
    double d;
};
总共占28Bytes。 c的偏移量为0,占1个Byte。sh占2个Byte,它的对齐模数是2(2<4,取小者),存放起始地址应该是2的整数倍,因此c后填充1个空字符,sh的起始地址是2。a占4个Byte,对齐模数是4,因此接在sh后存放即可,偏移量为4。f占4个字节,对齐模数是4,存放地址是4的整数倍,起始地址是8。p,s的起始地址分别是12,16。d占8个字节,对齐模数是4(4<8),d从偏移地址为20处存放。存放后结构体占28个字节,是4的整数倍不用补空字符。

struct Test2
{
    char c;
    double d;
    int a;
    short sh;
    float f;
    int *p;
    char *s;        
};
将Test1个变量的顺序换一下位置,结构体Test2占用内存32Byte,可见写结构体时,将各个变量按所占内存从小到大排列所占结构体所占内存较小。


 
3.3关于静态变量static

静态变量的存放位置与结构体实例的存储地址无关,是单独存放在静态数据区的,因此用siezof计算其大小时没有将静态成员所占的空间计算进来。

#pragma pack(4)
struct Test3
{
    char c;
    short sh;
    int a;
    float f;
    int *p;
    char *s;
    double d;
    static double sb;
    static int sn;
};
sizeof(Test3)=28

3.4关于类

空类是会占用内存空间的,而且大小是1,原因是C++要求每个实例在内存中都有独一无二的地址。

(一)类内部的成员变量:

普通的变量:是要占用内存的,但是要注意对齐原则(这点和struct类型很相似)。
static修饰的静态变量:不占用内容,原因是编译器将其放在全局变量区。
(二)类内部的成员函数:

普通函数:不占用内存。
虚函数:要占用4个字节,用来指定虚函数的虚拟函数表的入口地址。所以一个类的虚函数所占用的地址是不变的,和虚函数的个数是没有关系的
#pragma pack(4)
class cBase{};
sizeof(cBase)=1

3.4.1 不包含虚函数的类

#pragma pack(4)
class CBase1
{
private:
    char c;
    short sh;
    int a;
public:
    void fOut(){ cout << "hello" << endl; }
};
不包含虚函数时,对于类中的成员变量按结构体对齐方式处理,普通函数函数不占内存。sizeof(CBase1)=8
3.4.2 包含虚函数的类

#pragma pack(4)
class CBase2
{
private:
    char c;
    short sh;
    int a;
public:
    virtual void fOut(){ cout << "hello" << endl; }
};
包含虚函数时,类中需要保存虚函数表的入口地址指针,即需要多保存一个指针。这个值跟虚函数的个数多少没有关系。sizeof(CBase2)=12
3.4.3 子类

子类所占内存大小是父类+自身成员变量的值。特别注意的是,子类与父类共享同一个虚函数指针,因此当子类新声明一个虚函数时,不必在对其保存虚函数表指针入口。
#pragma pack(4)
class CBase2
{
private:
    char c;
    short sh;
    int a;
public:
    virtual void fOut(){ cout << "virtual 1" << endl; }
};
class cDerive :public CBase
{
private :
    int n;
public:
    virtual void fPut(){ cout << "virtual 2"; }
};

sizeof(cDerive)= sizeof(cBase)+sizeof(int n) = 16
 

<think>嗯,用户问的是C语言中的#pragma pack(push)#pragma pack(pop),需要我用中文介绍一下。首先,我得回忆一下这两个预处理指令的作用。它们通常结构体的内存对齐有关。对吧?当编译器处理结构体时,默认会有对齐规则,比如按照4字节对齐,这样结构体成员之间可能会有填充字节,以优化访问速度。但有时候我们需要改变这种对齐方式,尤其是在处理硬件相关或需要特定内存布局的时候,比如网络协议包或者硬件寄存器映射。这时候#pragma pack就有用了。 那#pragma pack(push)pop的作用呢?我记得push是用来保存当前的对齐状态,然后可以设置新的对齐方式,而pop则是恢复之前保存的状态。这样可以在需要的地方临时改变对齐,而不会影响其他代码部分的对齐设置。比如在一个头文件中,可能需要改变结构体的对齐方式,但之后用pop恢复,这样其他结构体还是按照原来的对齐方式处理。这样可以避免全局改变对齐带来的潜在问题。 用户可能需要了解这两个指令的基本用法,以及它们的作用场景。也许他们遇到了结构体内存对齐的问题,或者在跨平台开发时遇到了数据布局不一致的情况。也有可能是在学习如何优化内存使用,或者与外部设备进行数据交互时需要精确控制结构体的布局。 需要解释清楚#pragma pack()的作用,它接受一个参数,通常是1、2、4等,来指定对齐的字节数。而pushpop则是用来保存恢复对齐状态的堆栈操作。例如: #pragma pack(push, 1) struct MyStruct { char a; int b; }; #pragma pack(pop) 这里,push将当前的对齐方式压入堆栈,并设置新的对齐为1字节,即无填充。结构体MyStruct的大小就会是1+4=5字节,而正常情况下可能是8字节(假设默认4字节对齐)。之后,pop恢复之前的对齐方式,确保后续的代码不受影响。 需要注意的是,不同的编译器可能有不同的实现,但大多数主流编译器如GCC、Clang、MSVC都支持这种语法。另外,过度使用或错误使用pack可能导致程序性能下降,因为非对齐的内存访问在某些架构上较慢,甚至引发错误。因此,只有在必要时才应使用,并且要确保理解其影响。 可能用户还不太清楚为什么需要pushpop,而不仅仅是直接设置pack(1)然后再设回原来的值。这时候需要解释,因为直接设置的话,如果原来的对齐值未知,或者代码被多次包含,可能会导致问题。而pushpop自动保存恢复,更加安全可靠。 另外,可以举一些实际应用例子,比如网络传输中数据包的封装,或者与硬件寄存器交互时需要的精确内存布局。这种情况下,必须确保结构体没有填充,以符合协议或硬件的要求。 总结来说,需要明确这两个指令的作用、使用场景、注意事项,以及正确使用的示例。同时提醒用户谨慎使用,避免不必要的性能问题。</think>在 C 语言中,`#pragma pack(push)` `#pragma pack(pop)` 是用于控制**结构体(或联合体)内存对齐方式**的预处理指令。它们通常用于需要精确控制内存布局的场景(例如网络协议、硬件交互或跨平台兼容性开发)。 --- ### **基本作用** 1. **`#pragma pack(push, n)`** - 将当前编译器的**默认对齐值**保存到内部堆栈中。 - 设置新的对齐值为 `n` 字节(`n` 通常是 1、2、4、8 等 2 的幂)。 - 结构体成员将按照 `n` 字节对齐,减少或消除填充字节,可能牺牲性能以换取紧凑内存布局。 2. **`#pragma pack(pop)`** - 从内部堆栈中弹出最近保存的对齐值,**恢复之前的对齐设置**。 - 避免对齐方式的修改影响后续代码。 --- ### **典型使用场景** ```c #pragma pack(push, 1) // 保存当前对齐,设置新对齐为 1 字节(无填充) struct Packet { char header; int data; short checksum; }; // 结构体大小 = 1 + 4 + 2 = 7 字节(默认对齐下可能为 12 字节) #pragma pack(pop) // 恢复之前的对齐方式 ``` - **作用**:强制 `struct Packet` 的成员紧密排列,无填充字节。 - **用途**:网络协议包、二进制文件格式、硬件寄存器映射等需要内存布局精确控制的场景。 --- ### **注意事项** 1. **性能影响** 对齐值过小(如 `1`)可能导致非对齐内存访问,在某些架构(如 ARM)上引发性能下降甚至硬件异常。 2. **编译器兼容性** 语法是编译器相关的,但主流编译器(GCC、Clang、MSVC)均支持 `#pragma pack`。 C11 标准提供了 `_Alignas` `alignof` 作为更标准的替代方案。 3. **作用域** `#pragma pack` 的作用域是从声明处开始,直到遇到 `#pragma pack(pop)`,或文件结束。 --- ### **对比直接设置对齐** ```c #pragma pack(1) // 直接设置对齐为 1 struct A { ... }; #pragma pack(4) // 强行修改对齐为 4 struct B { ... }; ``` - ❌ 直接修改对齐值可能导致不可预料的副作用(例如其他代码依赖原对齐方式)。 - ✅ **`push/pop` 更安全**:通过堆栈机制确保对齐方式的局部性。 --- ### **总结** | 指令 | 行为 | 用途 | |-----------------------|-----------------------------------|--------------------------| | `#pragma pack(push, n)` | 保存当前对齐,设置新对齐为 `n` | 临时修改对齐,控制内存布局 | | `#pragma pack(pop)` | 恢复最近保存的对齐方式 | 确保后续代码不受影响 | 合理使用 `push/pop` 可以在需要时精确控制内存布局,同时避免全局对齐设置的副作用。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值