结构体内存对齐详解

本文详细解析了结构体内存对齐的意义与规则,并通过多个实例展示了如何利用编译器特性进行有效对齐。此外,还介绍了sizeof运算符的用法及注意事项。

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

一、意义
之所以要有结构体内存对齐,是因为处理器每次读写内存的时候从k倍数的地址开始,每次读取或写入k个字节的数据。如果能保证结构体内存对齐,那么每个成员数据就能在这k个字节里,而不会横跨在两个符号对齐要求的k字节内存上。加快读取速度。

二、对齐模数的规定
结构体中,各个成员按照被声明的顺序在内存中顺序存储,在成员之间可能会有插入空字节,整个结构体的地址和第一个成员的地址相同。

(一)缺省情况
在此情况下,编译器为每一个变量指定了默认对齐模数值。如下表:
这里写图片描述
(1)数据成员对齐规则:按上表所示的模数对每个数据成员进行对齐。若该成员的起始偏移不位于该成员的默认自然对齐条件上,则在上个成员后面添加适当个数的空字节。
(2)结构体的整体对齐规则:在通过第(1)所示对齐后,若结构体各成员所占内存之和不为成员变量中最大对齐模数的整数倍,则在最后一个成员后填充空字节。

例子如下(我的实验环境是linux64位):

例子1

struct A{
    char i;  //1字节对齐,offset为0
    char j;  //1字节对齐,offset为1
    float k; //4字节对齐,根据规则(1),编译器会在j之后填充2个空字节,offset为4
    char m;  //1字节对齐,offset为8
};

A x;

因为在结构体A中,最大的对齐模数是4,而目前总内存长度是1+1+2(这是填充的)+4+1=9,根据规则(2),最后内存布局为(1表示该位处有数据,0表示是空的):
1100 1111 1000
用GDB测试如下:

(gdb) p &x.i
$1 = 0x7fffffffcbd0 "\300\314\377\377\377\177"
(gdb) p &x.j
$2 = 0x7fffffffcbd1 "\314\377\377\377\177"
(gdb) p &x.k
$3 = (float *) 0x7fffffffcbd4
(gdb) p &x.m
$4 = 0x7fffffffcbd8 ""
(gdb) p sizeof(x)
$5 = 12

例子2

    struct A{
        int i;  //4字节对齐,offset=0
        char j; //1字节对齐,offset=4
        short k;//2字节对齐,offset=6
        char m; //1字节对齐,offset=8
    } ;

    A x;  //4字节对齐,内存布局为1111 1011 1000,共12字节
(gdb) p &x.i
$1 = (int *) 0x7fffffffcbd0
(gdb) p &x.j
$2 = 0x7fffffffcbd4 "\377\177"
(gdb) p &x.k
$3 = (short *) 0x7fffffffcbd6
(gdb) p &x.m
$4 = 0x7fffffffcbd8 ""
(gdb) p sizeof(x)
$5 = 12

例子3

    struct A{
        char i;  //1字节对齐,offset=0
        int j;   //4字节对齐,offset=4
        short k; //2字节对齐,offset=8
        char m;  //1字节对齐,offset=10
    } ;

    A x; //4字节对齐,内存布局为1000 1111 1110,共12字节
(gdb) p &x.i
$1 = 0x7fffffffcbd0 "\300\314\377\377\377\177"
(gdb) p &x.j
$2 = (int *) 0x7fffffffcbd4
(gdb) p &x.k
$3 = (short *) 0x7fffffffcbd8
(gdb) p &x.m
$4 = 0x7fffffffcbda ""
(gdb) p sizeof(x)
$5 = 12

例子4

    struct A{
        char i;  //1字节对齐,offset=0
        short j; //2字节对齐,offset=2
        long k;  //8字节对齐,offset=8
        double m;//8字节对齐,offset=16
    } ;

    A x; //8字节对齐,内存布局为10110000 11111111 11111111,共24字节 
(gdb) p &x.i
$1 = 0x7fffffffcbc0 " \\UUUU"
(gdb) p &x.j
$2 = (short *) 0x7fffffffcbc2
(gdb) p &x.k
$3 = (long *) 0x7fffffffcbc8
(gdb) p &x.m
$4 = (double *) 0x7fffffffcbd0
(gdb) p sizeof(x)
$5 = 24

(二)自定义#pragma pack (k)
注意:k必须是1,2,4,8,16之一
对齐规则:
(1)数据成员对齐规则:结构体的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员的按照min(#pragma pack指定的数值 , 这个数据成员自身长度)来对齐。
(2)结构体的整体对齐规则:在数据成员完成各自对齐之后,结构体本身也要进行对齐,按照min(#pragma pack指定的数值 , 结构体中最大数据成员长度)来对齐。

也就是说,当#pragma pack的k值超过所有数据成员长度的时候,将不产生任何效果。
还是上面那些例子,不过都加上#pragma pack(2)
例子1

    struct A{
        char i;     //1字节,offset=0
        char j;     //1字节,offset=1
        float k;   //4字节,offset=2 
        char m; //1字节,offset=6
    } ;
    A x;  //2字节对齐,内存布局为11 11 11 10,共8字节
(gdb) p &x.i
$1 = 0x7fffffffcbd0 "\300\314\377\377\377\177"
(gdb) p &x.j
$2 = 0x7fffffffcbd1 "\314\377\377\377\177"
(gdb) p &x.k
$3 = (float *) 0x7fffffffcbd2
(gdb) p &x.m
$4 = 0x7fffffffcbd6 ""
(gdb) p sizeof(x)
$5 = 8

例子2

    struct A{
        int i;    //2字节对齐,offset=0
        char j;   //1字节对齐,offset=4
        short k;  //2字节对齐,offset=6
        char m;   //1字节对齐,offset=8
    } ;

    A x;  //2字节对齐,内存布局为11 11 10 11 10,共10字节
(gdb) p &x.i
$1 = (int *) 0x7fffffffcbd0
(gdb) p &x.j
$2 = 0x7fffffffcbd4 "\377\177"
(gdb) p &x.k
$3 = (short *) 0x7fffffffcbd6
(gdb) p &x.m
$4 = 0x7fffffffcbd8 ""
(gdb) p sizeof(x)
$5 = 10

例子3

    struct A{
        char i;   //1字节对齐,offset=0
        int j;    //2字节对齐,offset=2
        short k;  //2字节对齐,offset=6
        char m;   //1字节对齐,offset=8
    } ;

    A x;  //2字节对齐,内存布局为10 11 11 11 10,共10字节
(gdb) p &x.i
$1 = 0x7fffffffcbd0 "\300\314\377\377\377\177"
(gdb) p &x.j
$2 = (int *) 0x7fffffffcbd2
(gdb) p &x.k
$3 = (short *) 0x7fffffffcbd6
(gdb) p &x.m
$4 = 0x7fffffffcbd8 ""
(gdb) p sizeof(x)
$5 = 10

例子4

    struct A{
        char i;    //1字节对齐,offset=0
        short j;   //2字节对齐,offset=2
        long k;    //2字节对齐,offset=4
        double m;  //2字节对齐,offset=12
    } ;

    A x;  //2字节对齐,内存布局为10 11 11 11 11 11 11 11 11 11,共20字节
(gdb) p &x.i
$1 = 0x7fffffffcbc0 " \\UUUU"
(gdb) p &x.j
$2 = (short *) 0x7fffffffcbc2
(gdb) p &x.k
$3 = (long *) 0x7fffffffcbc4
(gdb) p &x.m
$4 = (double *) 0x7fffffffcbcc
(gdb) p sizeof(x)
$5 = 20

相信看了上面的规则+例子,下次再遇到结构体内存对齐问题就不再是问题了。

——————————————————————2017.4.4——————————————————————
今天看了下《C++Primer》中对sizeof运算符的介绍,有些体会,所以也补充在这里了。

sizeof运算符返回一条表达式或一个类型名字所占的字节数。
sizeof的值在编译时得出,不会实际求运算对象的值,所以在sizeof的运算对象中解引用一个无效指针仍然是一种安全的行为,因为指针实际上并没有被真正使用,sizeof不需要真的解引用指针也能知道它所指对象的类型。如下测试代码,p和q都是空指针。

    char *p;
    cout<<sizeof(*p)<<endl;   //输出1
    int *q;
    cout<<sizeof(*q)<<endl;   //输出4

sizeof运算符的结果部分地依赖于其作用的类型:

  1. 对char或者类型为char的表达式执行sizeof运算,结果得1。
  2. 对引用类型执行sizeof运算得到被引用对象所占空间的大小。
  3. 对指针执行sizeof运算得到指针本身所占空间的大小。
  4. 对解引用指针执行sizeof运算得到指针指向的对象所占空间的大小,指针不需有效。
  5. 对数组执行sizeof运算得到整个数组所占空间的大小,等价于对数组中所有的元素各执行一次sizeof运算并将所得结果求和。注意,sizeof运算不会把数组转换成指针来处理。
  6. 对string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。
  7. 对于函数,则是求函数返回类型所占的空间大小。注意此时函数的返回类型是void时,sizeof得到的结果是1。

    对于第6点,应该这样理解:

class Model
{
    int a,b,c;
    int *p;
    Model()
    {
        p=new int [10];
    }
    ~Model()
    {
        delete[] p;
    }
};

直观上,它有四个成员:a,b,c,p,不过从用途上,它也可以分为三个固定分配成员a,b,c和一组动态分配成员p[10],它生成的对象实际占有的内存空间为13个int和一个int*的空间(即14*4=56字节)。然而,sizeof返回的则只是这个类“直观上”(也就是写在成员声明里的)的占用空间,也就是3个int和一个int*的空间(即16字节)。

string类和这个类很相像,它有一个动态分配的成员来存储字符串数据(因为字符串变长所以才要动态分配),而sizeof是检测不到这个内存长度的。vector同理。
那么,sizeof(string)的结果是什么呢?这个要看具体实现了。在我机子上的测试结果是32字节。

对于第7点,测试如下:

int f() { return 1; }
void g() {}

int main() {
    cout << sizeof(f()) << endl;    //4
    cout << sizeof(g()) << endl;    //1
}

再来比较比较sizeof和strlen。
strlen是函数,要在运行时才能计算。参数必须是字符型指针(char*),当数组名作为参数传入时,数组名就退化为指针了。这个函数的功能是返回结束符’\0’之前字符串的长度。

    char str[] = "hello\n";
    char *p = "hi\n";
    char ptr[] = {'h','e','l','l','o'};
    char ptr2[10] = {0};


    cout << sizeof(str) << endl;    //7
    cout << strlen(str) << endl;    //6
    cout << sizeof(str + 1) << endl;//8 (此时已经退化成指针了,64位下是8,32位是4)
    cout << strlen(str + 1) << endl;//5

    cout << sizeof(p) << endl;      //8 (求的是指针所用的空间)
    cout << strlen(p) << endl;      //3

    cout << sizeof(ptr) << endl;    //5
    cout << strlen(ptr) << endl;    //这个输出结果不一定,因为不知道什么时候遇到'\0'
    cout << sizeof(ptr[1]) << endl; //1

    cout << sizeof(ptr2) << endl;   //10
    cout << strlen(ptr2) << endl;   //0
    ptr2[0] = 'h';
    ptr2[2] = 'l';
    cout << sizeof(ptr2) << endl;   //10
    cout << strlen(ptr2) << endl;   //1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值