#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字节对齐)

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第1张图片

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

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

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第2张图片

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

复制代码

#include 

#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

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第3张图片

这说明了,在强制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) 和#pragma pack(pop) 以及#pragma pack()_第4张图片

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

复制代码

#include 

#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) 和#pragma pack(pop) 以及#pragma pack()_第5张图片

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

#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 
#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;
}

复制代码

 

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第6张图片

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

 

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

eg:

复制代码

#include 
#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()打印如下:

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第7张图片

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

还有延伸点:

#pragma pack(push) 和#pragma pack(pop) 以及#pragma pack()_第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
 

你可能感兴趣的:(计算机基础)